From bd0c4d9714ecacd2e9074e744c8250dd99499a98 Mon Sep 17 00:00:00 2001 From: Epinephrine Date: Wed, 4 Nov 2026 09:56:26 +0900 Subject: [PATCH 1/3] fix(claude): normalize direct context overflow errors --- src/protocols/encoders/messages.ts | 4 +++- .../protocol-direct-encoders-messages.test.ts | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) 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/tests/responses/protocol-direct-encoders-messages.test.ts b/tests/responses/protocol-direct-encoders-messages.test.ts index 666e8f4f7cc..4853fb93496 100644 --- a/tests/responses/protocol-direct-encoders-messages.test.ts +++ b/tests/responses/protocol-direct-encoders-messages.test.ts @@ -339,6 +339,18 @@ describe("direct Messages encoder matches bridge + converter (stream)", () => { }); describe("direct Messages fold matches bridge + collector (non-stream)", () => { + test("generic-coded context overflow triggers client compaction on both paths", async () => { + const events: AdapterEvent[] = [{ type: "error", errorType: "server_error", + code: "server_error", 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 }]; From f13774704a72178e687f9304097002f2d10f3868 Mon Sep 17 00:00:00 2001 From: Epinephrine Date: Wed, 4 Nov 2026 10:21:27 +0900 Subject: [PATCH 2/3] test(claude): cover generic overflow parity and document direct errors Preserve the existing direct-encoder fix and cover four generic error codes across streaming and folded responses. Verified tree matches local commit 25f1104c7631e713199de631505c1dba01ff49d3. --- structure/data-planes/protocol-paths.md | 10 ++++---- .../protocol-direct-encoders-messages.test.ts | 24 ++++++++++--------- 2 files changed, 18 insertions(+), 16 deletions(-) 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 4853fb93496..f0595cb0e96 100644 --- a/tests/responses/protocol-direct-encoders-messages.test.ts +++ b/tests/responses/protocol-direct-encoders-messages.test.ts @@ -339,17 +339,19 @@ describe("direct Messages encoder matches bridge + converter (stream)", () => { }); describe("direct Messages fold matches bridge + collector (non-stream)", () => { - test("generic-coded context overflow triggers client compaction on both paths", async () => { - const events: AdapterEvent[] = [{ type: "error", errorType: "server_error", - code: "server_error", 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(["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", From 70d7968390f8649ed028baa7c94ed34c3e40d62b Mon Sep 17 00:00:00 2001 From: Epinephrine Date: Wed, 4 Nov 2026 18:49:15 +0900 Subject: [PATCH 3/3] test(messages): align generic overflow normalization expectations --- tests/claude-integration/messages-error-normalization.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/claude-integration/messages-error-normalization.test.ts b/tests/claude-integration/messages-error-normalization.test.ts index 30fd29c187a..02e5eadb6ad 100644 --- a/tests/claude-integration/messages-error-normalization.test.ts +++ b/tests/claude-integration/messages-error-normalization.test.ts @@ -63,13 +63,13 @@ for (const [index, error] of failures.entries()) { contained(text); expect(text).toContain("event: error\n"); const payload = JSON.parse(text.split("data: ").at(-1)!); - if (index === 2 || index === 3) expect(payload.error).toMatchObject({ type: "invalid_request_error", ...(direct && index === 3 ? {} : { code: "context_length_exceeded" }), message: direct && index === 3 ? "The request exceeds the model's context window." : "prompt is too long: The request exceeds the model's context window." }); + if (index === 2 || index === 3) expect(payload.error).toMatchObject({ type: "invalid_request_error", code: "context_length_exceeded", message: "prompt is too long: The request exceeds the model's context window." }); if (index === 4 || index === 5) expect(payload.error.type).toBe("rate_limit_error"); const foldBudget = createTestTranslatorBudget(); const fold = direct ? await foldAnthropicMessage((async function* () { yield { ...error, type: "error", errorType: error.type } as AdapterEvent; })(), { model: "fixture", translatorBudget: foldBudget }) : null; if (fold) { const body = await fold.text(); contained(body); - expect(fold.status).toBe(index === 2 ? 400 : 502); + expect(fold.status).toBe(index === 2 || index === 3 ? 400 : 502); expect(JSON.parse(body).error.type).toBe(payload.error.type); } });