mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
fix: handle structured context overflow errors (#213)
This commit is contained in:
parent
54590d3d46
commit
2388f20bb3
7 changed files with 100 additions and 23 deletions
7
.changeset/context-overflow-responses.md
Normal file
7
.changeset/context-overflow-responses.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
"@moonshot-ai/agent-core": patch
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
"@moonshot-ai/kosong": patch
|
||||
---
|
||||
|
||||
Handle context overflow errors consistently across provider responses.
|
||||
|
|
@ -7,6 +7,7 @@ import {
|
|||
APIStatusError,
|
||||
APITimeoutError,
|
||||
inputTotal,
|
||||
isContextOverflowStatusError,
|
||||
type ContentPart,
|
||||
type TokenUsage,
|
||||
} from '@moonshot-ai/kosong';
|
||||
|
|
@ -738,7 +739,7 @@ function classifyApiError(error: unknown, summary: KimiErrorPayload): ApiErrorCl
|
|||
if (statusCode === 429) return { errorType: 'rate_limit', statusCode };
|
||||
if (statusCode === 401 || statusCode === 403) return { errorType: 'auth', statusCode };
|
||||
if (statusCode >= 500) return { errorType: '5xx_server', statusCode };
|
||||
if (isContextOverflowMessage(summary.message)) {
|
||||
if (isContextOverflowStatusError(statusCode, summary.message)) {
|
||||
return { errorType: 'context_overflow', statusCode };
|
||||
}
|
||||
if (statusCode >= 400) return { errorType: '4xx_client', statusCode };
|
||||
|
|
@ -787,17 +788,6 @@ function isApiEmptyResponseError(error: unknown, summary: KimiErrorPayload): boo
|
|||
return error instanceof APIEmptyResponseError || summary.name === 'APIEmptyResponseError';
|
||||
}
|
||||
|
||||
function isContextOverflowMessage(message: string): boolean {
|
||||
const lower = message.toLowerCase();
|
||||
return (
|
||||
lower.includes('context length') ||
|
||||
lower.includes('context_length') ||
|
||||
lower.includes('max tokens') ||
|
||||
lower.includes('maximum context') ||
|
||||
lower.includes('too many tokens')
|
||||
);
|
||||
}
|
||||
|
||||
function currentTurnInputTokens(usage: TokenUsage | undefined): number | undefined {
|
||||
if (usage === undefined) return undefined;
|
||||
return inputTotal(usage);
|
||||
|
|
|
|||
|
|
@ -1056,6 +1056,17 @@ describe('Agent turn flow', () => {
|
|||
errorType: 'context_overflow',
|
||||
statusCode: 422,
|
||||
},
|
||||
{
|
||||
name: 'context overflow token count status',
|
||||
createError: () =>
|
||||
new APIStatusError(
|
||||
400,
|
||||
'input token count 131072 exceeds the maximum number of tokens allowed',
|
||||
'req-token-count',
|
||||
),
|
||||
errorType: 'context_overflow',
|
||||
statusCode: 400,
|
||||
},
|
||||
{
|
||||
name: 'connection error',
|
||||
createError: () => new APIConnectionError('socket hang up'),
|
||||
|
|
|
|||
|
|
@ -85,6 +85,10 @@ const CONTEXT_OVERFLOW_MESSAGE_PATTERNS = [
|
|||
/request.*exceed(?:ed|s|ing)?.*model token limit/,
|
||||
] as const;
|
||||
|
||||
export function isContextOverflowErrorCode(code: string | null | undefined): boolean {
|
||||
return code === 'context_length_exceeded';
|
||||
}
|
||||
|
||||
export function normalizeAPIStatusError(
|
||||
statusCode: number,
|
||||
message: string,
|
||||
|
|
@ -96,7 +100,7 @@ export function normalizeAPIStatusError(
|
|||
return new APIStatusError(statusCode, message, requestId);
|
||||
}
|
||||
|
||||
function isContextOverflowStatusError(statusCode: number, message: string): boolean {
|
||||
export function isContextOverflowStatusError(statusCode: number, message: string): boolean {
|
||||
if (statusCode !== 400 && statusCode !== 413 && statusCode !== 422) return false;
|
||||
const lowerMessage = message.toLowerCase();
|
||||
return CONTEXT_OVERFLOW_MESSAGE_PATTERNS.some((pattern) => pattern.test(lowerMessage));
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ export {
|
|||
APIStatusError,
|
||||
APITimeoutError,
|
||||
ChatProviderError,
|
||||
isContextOverflowStatusError,
|
||||
isRetryableGenerateError,
|
||||
} from './errors';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ModelCapability } from '#/capability';
|
||||
import { ChatProviderError } from '#/errors';
|
||||
import { APIContextOverflowError, ChatProviderError, isContextOverflowErrorCode } from '#/errors';
|
||||
import type { ContentPart, Message, StreamedMessagePart, ToolCall } from '#/message';
|
||||
import { extractText } from '#/message';
|
||||
import type {
|
||||
|
|
@ -217,12 +217,39 @@ function formatResponsesErrorEvent(
|
|||
return `${codeText}: ${message}${paramText}`;
|
||||
}
|
||||
|
||||
function formatResponsesFailedResponse(response: RawObject): string {
|
||||
function errorFromOpenAIResponsesEvent(
|
||||
prefix: string,
|
||||
code: string | null,
|
||||
message: string,
|
||||
param: string | null,
|
||||
): ChatProviderError {
|
||||
const formatted = formatResponsesErrorEvent(code, message, param);
|
||||
const fullMessage = `${prefix}: ${formatted}`;
|
||||
if (isContextOverflowErrorCode(code)) {
|
||||
return new APIContextOverflowError(400, fullMessage);
|
||||
}
|
||||
return new ChatProviderError(fullMessage);
|
||||
}
|
||||
|
||||
function readResponsesFailedResponseError(response: RawObject):
|
||||
| {
|
||||
code: string | null;
|
||||
message: string;
|
||||
}
|
||||
| undefined {
|
||||
const error = readObjectField(response, 'error');
|
||||
if (error !== undefined) {
|
||||
const code = readNullableStringField(error, 'code') ?? 'unknown';
|
||||
const message = readStringField(error, 'message') ?? 'no message';
|
||||
return `${code}: ${message}`;
|
||||
return { code, message };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function formatResponsesFailedResponse(response: RawObject): string {
|
||||
const error = readResponsesFailedResponseError(response);
|
||||
if (error !== undefined) {
|
||||
return formatResponsesErrorEvent(error.code, error.message, null);
|
||||
}
|
||||
|
||||
const incompleteDetails = readObjectField(response, 'incomplete_details');
|
||||
|
|
@ -777,16 +804,24 @@ export class OpenAIResponsesStreamedMessage implements StreamedMessage {
|
|||
}
|
||||
case 'error': {
|
||||
const message = requireStringField(chunk, 'message', type);
|
||||
throw new ChatProviderError(
|
||||
`OpenAI Responses stream error: ${formatResponsesErrorEvent(
|
||||
readNullableStringField(chunk, 'code') ?? null,
|
||||
message,
|
||||
readNullableStringField(chunk, 'param') ?? null,
|
||||
)}`,
|
||||
throw errorFromOpenAIResponsesEvent(
|
||||
'OpenAI Responses stream error',
|
||||
readNullableStringField(chunk, 'code') ?? null,
|
||||
message,
|
||||
readNullableStringField(chunk, 'param') ?? null,
|
||||
);
|
||||
}
|
||||
case 'response.failed': {
|
||||
const responseObject = requireObjectField(chunk, 'response', type);
|
||||
const error = readResponsesFailedResponseError(responseObject);
|
||||
if (error !== undefined) {
|
||||
throw errorFromOpenAIResponsesEvent(
|
||||
'OpenAI Responses response.failed',
|
||||
error.code,
|
||||
error.message,
|
||||
null,
|
||||
);
|
||||
}
|
||||
throw new ChatProviderError(
|
||||
`OpenAI Responses response.failed: ${formatResponsesFailedResponse(responseObject)}`,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ChatProviderError } from '#/errors';
|
||||
import { APIContextOverflowError, ChatProviderError } from '#/errors';
|
||||
import { generate } from '#/generate';
|
||||
import type { ContentPart, Message, StreamedMessagePart, ToolCall } from '#/message';
|
||||
import {
|
||||
|
|
@ -1635,6 +1635,35 @@ describe('OpenAIResponsesChatProvider', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('normalizes response.failed context overflow events', async () => {
|
||||
const events = [
|
||||
{
|
||||
type: 'response.failed',
|
||||
response: {
|
||||
id: 'resp_context_overflow',
|
||||
status: 'failed',
|
||||
error: {
|
||||
code: 'context_length_exceeded',
|
||||
message:
|
||||
'Your input exceeds the context window of this model. Please adjust your input and try again.',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const stream = new OpenAIResponsesStreamedMessage(makeAsyncIterable(events), true);
|
||||
|
||||
let caughtError: unknown;
|
||||
try {
|
||||
await collectStreamParts(stream);
|
||||
} catch (error) {
|
||||
caughtError = error;
|
||||
}
|
||||
|
||||
expect(caughtError).toBeInstanceOf(APIContextOverflowError);
|
||||
expect((caughtError as APIContextOverflowError).statusCode).toBe(400);
|
||||
expect((caughtError as Error).message).toMatch(/context_length_exceeded/);
|
||||
});
|
||||
|
||||
it('throws when a known stream event is missing a required field', async () => {
|
||||
const stream = new OpenAIResponsesStreamedMessage(
|
||||
makeAsyncIterable([{ type: 'response.output_text.delta' }]),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue