Browse Source

Merge pull request #958 from MyGO-Mujica/fix/foreground-fallback-auth-unavailable

fix(foreground-fallback): detect CliProxyAPI auth-unavailable errors (#957)
Alvin 1 week ago
parent
commit
fc1cec8c10

+ 57 - 0
src/hooks/foreground-fallback/index.test.ts

@@ -208,6 +208,27 @@ describe('isFailoverError', () => {
     ).toBe(true);
   });
 
+  test('returns true for CliProxyAPI "auth unavailable" error shapes', () => {
+    const message =
+      'auth_unavailable: no auth available (providers=cli-proxy-api, model=gemini-3.6-flash)';
+
+    expect(isRetryableError(message)).toBe(true);
+    expect(isRetryableError({ message })).toBe(true);
+    expect(
+      isRetryableError({
+        data: { statusCode: 400, responseBody: message },
+      }),
+    ).toBe(true);
+    expect(
+      isRetryableError({
+        data: {
+          responseBody:
+            '{"error":{"message":"auth_unavailable: no auth available","type":"server_error","code":"internal_server_error"}}',
+        },
+      }),
+    ).toBe(true);
+  });
+
   test('returns false for permanent channel-not-found errors', () => {
     expect(
       isRetryableError({
@@ -329,6 +350,42 @@ describe('ForegroundFallbackManager session.error', () => {
     expect(call[0].model.modelID).toBe('gpt-4o');
   });
 
+  test('triggers fallback on CliProxyAPI auth-unavailable session.error', async () => {
+    await mgr.handleEvent({
+      type: 'message.updated',
+      properties: {
+        info: {
+          sessionID: 'sess-1',
+          providerID: 'anthropic',
+          modelID: 'claude-opus-4-5',
+          role: 'assistant',
+        },
+      },
+    });
+
+    await mgr.handleEvent({
+      type: 'session.error',
+      properties: {
+        sessionID: 'sess-1',
+        error: {
+          message:
+            'auth_unavailable: no auth available (providers=cli-proxy-api, model=gemini-3.6-flash)',
+        },
+      },
+    });
+
+    expect(mocks.abort).not.toHaveBeenCalled();
+    expect(mocks.promptAsync).toHaveBeenCalledTimes(1);
+
+    const call = mocks.promptAsync.mock.calls[0] as [
+      {
+        model: { providerID: string; modelID: string };
+      },
+    ];
+    expect(call[0].model.providerID).toBe('openai');
+    expect(call[0].model.modelID).toBe('gpt-4o');
+  });
+
   test('marks the replayed user prompt as an internal initiator', async () => {
     await mgr.handleEvent({
       type: 'message.updated',

+ 6 - 0
src/hooks/foreground-fallback/index.ts

@@ -54,6 +54,12 @@ const RETRYABLE_ERROR_PATTERNS = [
   /\b403\b/,
   /forbidden/i,
   /blocked by gateway/i,
+  // Auth/credential availability (e.g. CliProxyAPI disables an exhausted
+  // upstream and returns 503 "auth_unavailable: no auth available ...").
+  // The provider is temporarily unavailable, so the next model should be
+  // tried instead of retrying the same dead model.
+  /no auth available/i,
+  /auth_unavailable/i,
 ];
 
 const OUTAGE_STATUS_CODES = new Set([500, 502, 503, 504]);