fix: surface provider auth error for unavailable models (#1506)

* fix: surface provider auth error for unavailable models

When an OAuth-managed model returns 401 after a forced token refresh, the token is valid but the provider rejected it for that model (the account lacks access). Emit provider.auth_error carrying the provider's message instead of auth.login_required with a misleading "OAuth login expired. Send /login" prompt.

* fix(agent-core): preserve provider auth errors through compaction

Treat provider.auth_error like auth.login_required in the compaction path so an auth rejection during compaction surfaces the provider's message instead of being wrapped as a generic compaction failure.
This commit is contained in:
7Sageer 2026-07-08 21:16:44 +08:00 committed by GitHub
parent 2394d013bc
commit e83511a711
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 22 additions and 10 deletions

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---
Fix a misleading "OAuth login expired" message shown when a model is not available for the current account; the CLI now shows the provider's actual error.

View file

@ -595,7 +595,12 @@ export class FullCompaction {
thinking_effort: this.agent.config.thinkingEffort,
error_type: error instanceof Error ? error.name : 'Unknown',
});
if (isKimiError(error) && error.code === ErrorCodes.AUTH_LOGIN_REQUIRED) throw error;
if (
isKimiError(error) &&
(error.code === ErrorCodes.AUTH_LOGIN_REQUIRED ||
error.code === ErrorCodes.PROVIDER_AUTH_ERROR)
)
throw error;
throw new KimiError(ErrorCodes.COMPACTION_FAILED, String(error), { cause: error });
}
}

View file

@ -203,9 +203,10 @@ export class ProviderManager implements ModelProvider {
} catch (error) {
if (!(error instanceof APIStatusError) || error.statusCode !== 401) throw error;
if (refreshed) {
const reason = error.message.replaceAll('\r', '');
throw new KimiError(
ErrorCodes.AUTH_LOGIN_REQUIRED,
'OAuth provider credentials were rejected. Send /login to login.',
ErrorCodes.PROVIDER_AUTH_ERROR,
reason.length > 0 ? reason : 'OAuth provider credentials were rejected.',
{
cause: error,
details: { statusCode: error.statusCode, requestId: error.requestId },

View file

@ -362,7 +362,7 @@ describe('FullCompaction', () => {
expect(messageText(compactionCall?.history[5])).toBe('lookup result');
});
it('force-refreshes OAuth credentials on compaction 401 and falls back to login_required when replay 401', async () => {
it('force-refreshes OAuth credentials on compaction 401 and treats replay 401 as provider auth error', async () => {
const tokenCalls: Array<boolean | undefined> = [];
const authKeys: string[] = [];
const oauthOptions = oauthTestAgentOptions(async (options) => {
@ -398,7 +398,7 @@ describe('FullCompaction', () => {
expect.objectContaining({
event: 'error',
args: expect.objectContaining({
code: 'auth.login_required',
code: 'provider.auth_error',
details: expect.objectContaining({
statusCode: 401,
requestId: 'req-compact-401',

View file

@ -1409,7 +1409,7 @@ describe('Agent turn flow', () => {
);
});
it('falls back to login_required when force-refresh and replay both 401', async () => {
it('treats 401 after force-refresh as provider auth error', async () => {
const tokenCalls: Array<boolean | undefined> = [];
const authKeys: string[] = [];
const oauthOptions = oauthAgentOptions(
@ -1447,7 +1447,7 @@ describe('Agent turn flow', () => {
args: expect.objectContaining({
reason: 'failed',
error: expect.objectContaining({
code: 'auth.login_required',
code: 'provider.auth_error',
details: expect.objectContaining({
statusCode: 401,
requestId: 'req-401',
@ -1644,7 +1644,7 @@ describe('Agent turn flow', () => {
expect(payloads[1]).toMatchObject({ turnStep: '0.1', attempt: '2/3' });
});
it('force-refreshes OAuth credentials on video upload 401 and falls back to login_required when replay 401', async () => {
it('force-refreshes OAuth credentials on video upload 401 and surfaces the provider auth error when replay 401', async () => {
const tokenCalls: Array<boolean | undefined> = [];
const authKeys: string[] = [];
const oauthOptions = oauthAgentOptions(
@ -1689,8 +1689,9 @@ describe('Agent turn flow', () => {
expect(result.isError).toBe(true);
expect(authKeys).toEqual(['fresh-token', 'forced-refresh-token']);
expect(tokenCalls).toEqual([undefined, true]);
expect(result.output).toContain('OAuth provider credentials were rejected');
expect(result.output).toContain('Send /login to login');
expect(result.output).toContain('Unauthorized');
expect(result.output).not.toContain('OAuth provider credentials were rejected');
expect(result.output).not.toContain('Send /login to login');
});
it('cancels an active turn', async () => {