From d44bfc300be04ede1763a4906da3ae2d5a283614 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 15 Feb 2026 19:50:15 -0800 Subject: [PATCH] Handle Z.AI textual 401 errors for auth diagnostics --- docs/plans/state.json | 4 ++-- src/models/openai.test.ts | 16 ++++++++++++++++ src/models/openai.ts | 3 ++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/docs/plans/state.json b/docs/plans/state.json index b0adcc2..8ef1a25 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -47,14 +47,14 @@ "status": "completed", "date": "2026-02-16", "updated": "2026-02-16", - "summary": "Unified Z.AI credential resolution so zhipuai model switches resolve credentials from config, env (including ZAI_API_KEY), and auth store regardless of use_oauth flag. Added clearer Z.AI-specific 401 diagnostics when API auth fails (including missing model.request scope hint).", + "summary": "Unified Z.AI credential resolution so zhipuai model switches resolve credentials from config, env (including ZAI_API_KEY), and auth store regardless of use_oauth flag. Added clearer Z.AI-specific 401 diagnostics when API auth fails (including missing model.request scope hint), including textual 401 errors where the SDK does not expose status.", "files_modified": [ "src/daemon/models.ts", "src/models/openai.ts", "src/daemon/clientFactory.test.ts", "src/models/openai.test.ts" ], - "test_status": "pnpm test:run src/daemon/clientFactory.test.ts src/models/openai.test.ts + pnpm typecheck passing" + "test_status": "pnpm test:run src/daemon/clientFactory.test.ts src/models/openai.test.ts + pnpm typecheck passing (updated to cover textual 401 without status field)" }, "deployment-port-env-override": { diff --git a/src/models/openai.test.ts b/src/models/openai.test.ts index c04a9c3..ac4f24a 100644 --- a/src/models/openai.test.ts +++ b/src/models/openai.test.ts @@ -152,4 +152,20 @@ describe('OpenAIClient tool use', () => { messages: [{ role: 'user', content: 'hello' }], })).rejects.toThrow(/Z\.AI authentication failed \(401\)/); }); + + it('rewrites Z.AI textual 401 errors when SDK status is absent', async () => { + mockCreate.mockRejectedValueOnce(new Error( + '401 You have insufficient permissions for this operation. Missing scopes: model.request.', + )); + + const client = new OpenAIClient({ + apiKey: 'zai-key', + model: 'glm-4.7', + baseURL: 'https://api.z.ai/api/paas/v4', + }); + + await expect(client.chat({ + messages: [{ role: 'user', content: 'hello' }], + })).rejects.toThrow(/The key lacks `model\.request` scope/); + }); }); diff --git a/src/models/openai.ts b/src/models/openai.ts index e4a33ac..3075648 100644 --- a/src/models/openai.ts +++ b/src/models/openai.ts @@ -260,9 +260,10 @@ export class OpenAIClient implements ModelClient { const message = error instanceof Error ? error.message : String(error); const isZai = (this.baseURL ?? '').includes('api.z.ai'); + const isUnauthorized401 = status === 401 || /\b401\b/.test(message); const missingModelRequestScope = message.includes('Missing scopes: model.request'); - if (isZai && status === 401) { + if (isZai && isUnauthorized401) { const hint = missingModelRequestScope ? 'The key lacks `model.request` scope.' : 'The API key is invalid, expired, or not allowed for this model/endpoint.';