diff --git a/src/protocols/encoders/messages.ts b/src/protocols/encoders/messages.ts index d41d8004306..dc9ee2e4699 100644 --- a/src/protocols/encoders/messages.ts +++ b/src/protocols/encoders/messages.ts @@ -27,6 +27,7 @@ import { anthropicUsage, boundedReasoningIdentity, collectAnthropicMessage, + isContextOverflowError, isClaudeWebSearchToolName, messageSnapshot, sanitizeWebSearchInput, @@ -389,8 +390,9 @@ function createAnthropicMessageWriter( } const message = typeof error.message === "string" ? error.message : "upstream request failed"; const deterministic = error.code === UNDECLARED_TOOL_CALL_ERROR_CODE; + const overflow = isContextOverflowError(error, message); fail(anthropicFailedStatus(error, message), message, !deterministic, - deterministic || error.code === "context_length_exceeded" ? error.code as string : undefined); + deterministic ? error.code as string : overflow ? "context_length_exceeded" : undefined); }, overflow() { fail(413, "upstream translation buffer exceeded the safe limit", false, "translation_buffer_limit"); diff --git a/structure/data-planes/protocol-paths.md b/structure/data-planes/protocol-paths.md index 4b7ed7e8599..b399fa2788b 100644 --- a/structure/data-planes/protocol-paths.md +++ b/structure/data-planes/protocol-paths.md @@ -223,11 +223,11 @@ the wire-silence heartbeat and live-destination stall watchdog ([timeout policy] `chat.ts` (`encodeChatCompletionSse`, `foldChatCompletion`) and `messages.ts` (`encodeAnthropicMessageSse`, `foldAnthropicMessage`) are the writers; the Chat one also emits `reasoning_details` ([#7429](chat-reasoning-details.md)). They reuse the converters' own helpers, exported from `src/chat/outbound.ts` and `src/claude/outbound.ts` -(ids, chunk and frame builders, usage mapping, `chatCompletionsStreamErrorPayload`, -`chatCompletionsFailedResponse`, `chatCompletionsIncompleteOutcome`, -`anthropicIncompleteOutcome`, `anthropicFailedStatus`, the message snapshot, the web-search pair), -so the frames a client receives are the converters' frames. A fold is the encoded stream read by -the existing collector. Only producer-marked watchdog/EOF terminals retain local Chat wording; upstream copies of their reason or message follow [client error normalization](error-detail.md#client-error-bodies-chat). Explicit incomplete reasons use the [shared normalization contract](../transports/responses-wire-shapes.md#adapter-incomplete-reasons) before the writers classify the outcome. +(ids, chunk and frame builders, usage mapping, `chatCompletionsStreamErrorPayload`, `chatCompletionsFailedResponse`, `chatCompletionsIncompleteOutcome`, +`anthropicIncompleteOutcome`, `anthropicFailedStatus`, `isContextOverflowError`, the message snapshot, the web-search pair), +so the frames a client receives are the converters' frames. A fold is the encoded stream read by the existing collector. +Messages terminal errors use the shared context-overflow verdict for their code as well as their status: definite generic-coded refusals receive `context_length_exceeded` and `prompt is too long` wording even when marked retryable, while the deterministic undeclared-tool error keeps its code. `tests/responses/protocol-direct-encoders-messages.test.ts` pins stream and fold parity. +Only producer-marked watchdog/EOF terminals retain local Chat wording; upstream copies of their reason or message follow [client error normalization](error-detail.md#client-error-bodies-chat). Explicit incomplete reasons use the [shared normalization contract](../transports/responses-wire-shapes.md#adapter-incomplete-reasons) before the writers classify the outcome. Deliberate equivalences, pinned by the parity tests: a Chat function call is delivered as one complete tool-call chunk when it completes, as the converter always did; Messages streams diff --git a/tests/responses/protocol-direct-encoders-messages.test.ts b/tests/responses/protocol-direct-encoders-messages.test.ts index 666e8f4f7cc..f0595cb0e96 100644 --- a/tests/responses/protocol-direct-encoders-messages.test.ts +++ b/tests/responses/protocol-direct-encoders-messages.test.ts @@ -339,6 +339,20 @@ describe("direct Messages encoder matches bridge + converter (stream)", () => { }); describe("direct Messages fold matches bridge + collector (non-stream)", () => { + test.each(["server_error", "upstream_error", "api_error", "internal_error"])( + "generic-coded %s context overflow triggers client compaction on both paths", async code => { + const events: AdapterEvent[] = [{ type: "error", errorType: "server_error", + code, message: "Your input exceeds the context window of this model", retryable: true }]; + const frames = await expectStreamParity(events); + expect(frames).toEqual([{ event: "error", data: { type: "error", error: { + type: "invalid_request_error", code: "context_length_exceeded", + message: "prompt is too long: Your input exceeds the context window of this model", + } } }]); + const response = await expectFoldParity(events); + expect(response.status).toBe(400); + }, + ); + test.each([400, 413, undefined])("context rejection remains terminal with upstream status %s", async status => { const events: AdapterEvent[] = [{ type: "error", status, errorType: "invalid_request_error", code: "context_length_exceeded", message: "Synthetic input limit", retryable: false }];