Fix Z.AI credential resolution and improve 401 auth diagnostics
This commit is contained in:
@@ -43,6 +43,20 @@
|
||||
"test_status": "pnpm test:run + pnpm typecheck passing"
|
||||
},
|
||||
|
||||
"zai-auth-resolution-and-401-hints": {
|
||||
"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).",
|
||||
"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"
|
||||
},
|
||||
|
||||
"deployment-port-env-override": {
|
||||
"status": "completed",
|
||||
"date": "2026-02-16",
|
||||
|
||||
@@ -185,6 +185,26 @@ describe('createClientFromConfig', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('creates OpenAIClient for zhipuai using ZAI_API_KEY env var without use_oauth', async () => {
|
||||
const prev = process.env.ZAI_API_KEY;
|
||||
process.env.ZAI_API_KEY = 'zai-api-key';
|
||||
|
||||
try {
|
||||
const { createClientFromConfig } = await loadFactory();
|
||||
const client = createClientFromConfig({
|
||||
provider: 'zhipuai',
|
||||
model: 'glm-4.7',
|
||||
});
|
||||
expect(client.constructor.name).toBe('OpenAIClient');
|
||||
} finally {
|
||||
if (prev === undefined) {
|
||||
delete process.env.ZAI_API_KEY;
|
||||
} else {
|
||||
process.env.ZAI_API_KEY = prev;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('creates OpenAIClient for minimax provider', async () => {
|
||||
const prev = process.env.MINIMAX_API_KEY;
|
||||
process.env.MINIMAX_API_KEY = 'test-key';
|
||||
|
||||
+6
-22
@@ -33,17 +33,16 @@ function requireApiKey(cfg: ModelConfig, envVar: string): string {
|
||||
return key;
|
||||
}
|
||||
|
||||
function resolveAuthCredential(cfg: ModelConfig, apiKeyEnvVar: string, authTokenEnvVar?: string): string {
|
||||
function resolveZaiCredential(cfg: ModelConfig): string {
|
||||
const raw = cfg.api_key
|
||||
?? cfg.auth_token
|
||||
?? process.env[apiKeyEnvVar]
|
||||
?? (authTokenEnvVar ? process.env[authTokenEnvVar] : undefined);
|
||||
?? getZaiApiKey();
|
||||
|
||||
if (!raw) {
|
||||
const envHint = authTokenEnvVar ? `${apiKeyEnvVar} or ${authTokenEnvVar}` : apiKeyEnvVar;
|
||||
throw new Error(
|
||||
`Credential required for ${cfg.provider}. ` +
|
||||
`Set ${envHint} environment variable or provide api_key/auth_token in config.`,
|
||||
'Z.AI credential not configured. ' +
|
||||
'Run `flynn zai-auth` or set ZAI_API_KEY / ZHIPUAI_API_KEY / ZHIPUAI_AUTH_TOKEN, ' +
|
||||
'or provide api_key/auth_token in config.',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -197,24 +196,9 @@ export function createClientFromConfig(cfg: ModelConfig): ModelClient {
|
||||
baseURL: cfg.endpoint ?? 'https://ai-gateway.vercel.sh/v1',
|
||||
});
|
||||
case 'zhipuai':
|
||||
if (cfg.use_oauth) {
|
||||
const apiKey = getZaiApiKey();
|
||||
if (!apiKey) {
|
||||
throw new Error(
|
||||
'Z.AI credential not configured. ' +
|
||||
'Run `flynn zai-auth` or set ZAI_API_KEY / ZHIPUAI_API_KEY / ZHIPUAI_AUTH_TOKEN.',
|
||||
);
|
||||
}
|
||||
return new OpenAIClient({
|
||||
model: cfg.model,
|
||||
apiKey,
|
||||
baseURL: cfg.endpoint ?? 'https://api.z.ai/api/paas/v4',
|
||||
});
|
||||
}
|
||||
|
||||
return new OpenAIClient({
|
||||
model: cfg.model,
|
||||
apiKey: resolveAuthCredential(cfg, 'ZHIPUAI_API_KEY', 'ZHIPUAI_AUTH_TOKEN'),
|
||||
apiKey: resolveZaiCredential(cfg),
|
||||
baseURL: cfg.endpoint ?? 'https://api.z.ai/api/paas/v4',
|
||||
});
|
||||
case 'xai':
|
||||
|
||||
@@ -135,4 +135,21 @@ describe('OpenAIClient tool use', () => {
|
||||
|
||||
expect(response.stopReason).toBe('max_tokens');
|
||||
});
|
||||
|
||||
it('rewrites Z.AI 401 errors with actionable auth guidance', async () => {
|
||||
mockCreate.mockRejectedValueOnce({
|
||||
status: 401,
|
||||
message: '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(/Z\.AI authentication failed \(401\)/);
|
||||
});
|
||||
});
|
||||
|
||||
+26
-1
@@ -61,10 +61,12 @@ export class OpenAIClient implements ModelClient {
|
||||
private model: string;
|
||||
private defaultMaxTokens: number;
|
||||
private useOAuth: boolean;
|
||||
private baseURL?: string;
|
||||
|
||||
constructor(config: OpenAIClientConfig) {
|
||||
const timeoutMs = config.timeoutMs ?? 20_000;
|
||||
this.useOAuth = Boolean(config.useOAuth);
|
||||
this.baseURL = config.baseURL;
|
||||
|
||||
// OAuth mode uses a different backend (ChatGPT Codex) and a different API shape.
|
||||
// Only initialize the OpenAI SDK for API-key providers.
|
||||
@@ -248,7 +250,30 @@ export class OpenAIClient implements ModelClient {
|
||||
(params as any).reasoning_effort = 'medium';
|
||||
}
|
||||
|
||||
const response = await this.client.chat.completions.create(params);
|
||||
let response: OpenAI.ChatCompletion;
|
||||
try {
|
||||
response = await this.client.chat.completions.create(params);
|
||||
} catch (error) {
|
||||
const status = typeof (error as { status?: unknown })?.status === 'number'
|
||||
? (error as { status: number }).status
|
||||
: undefined;
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
const isZai = (this.baseURL ?? '').includes('api.z.ai');
|
||||
const missingModelRequestScope = message.includes('Missing scopes: model.request');
|
||||
|
||||
if (isZai && status === 401) {
|
||||
const hint = missingModelRequestScope
|
||||
? 'The key lacks `model.request` scope.'
|
||||
: 'The API key is invalid, expired, or not allowed for this model/endpoint.';
|
||||
throw new Error(
|
||||
`Z.AI authentication failed (401). ${hint} ` +
|
||||
'Run `flynn zai-auth` to update credentials, or set ZAI_API_KEY / ZHIPUAI_API_KEY / ZHIPUAI_AUTH_TOKEN.',
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const choice = response.choices[0];
|
||||
const content = choice?.message?.content ?? '';
|
||||
|
||||
Reference in New Issue
Block a user