From 9e605f90366d8ddcae7758bda89944db70b78621 Mon Sep 17 00:00:00 2001 From: Sebastian Lorenz Date: Mon, 3 Aug 2026 16:21:35 +0000 Subject: [PATCH 1/2] Fix malformed JSON in Anthropic code-execution deltas --- .../AnthropicMalformedCodeDeltaAudit.test.ts | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 packages/ai/anthropic/test/AnthropicMalformedCodeDeltaAudit.test.ts diff --git a/packages/ai/anthropic/test/AnthropicMalformedCodeDeltaAudit.test.ts b/packages/ai/anthropic/test/AnthropicMalformedCodeDeltaAudit.test.ts new file mode 100644 index 00000000000..b5b28d36fad --- /dev/null +++ b/packages/ai/anthropic/test/AnthropicMalformedCodeDeltaAudit.test.ts @@ -0,0 +1,97 @@ +import { AnthropicClient, AnthropicLanguageModel } from "@effect/ai-anthropic" +import { assert, describe, it } from "@effect/vitest" +import { Effect, Layer, Redacted, Stream } from "effect" +import { LanguageModel } from "effect/unstable/ai" +import { HttpClient, type HttpClientError, type HttpClientRequest, HttpClientResponse } from "effect/unstable/http" + +describe("AnthropicLanguageModel malformed code delta", () => { + it.effect("emits valid JSON for streamed code execution parameters", () => + Effect.gen(function*() { + const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(sseResponse(request, [ + { + type: "message_start", + message: { + id: "msg_1", + type: "message", + role: "assistant", + model: "claude-sonnet-4-20250514", + content: [], + stop_reason: null, + stop_sequence: null, + usage: { + cache_creation: null, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + inference_geo: null, + input_tokens: 1, + output_tokens: 0, + service_tier: null + } + } + }, + { + type: "content_block_start", + index: 0, + content_block: { + type: "server_tool_use", + id: "srvtoolu_1", + name: "bash_code_execution", + input: {} + } + }, + { + type: "content_block_delta", + index: 0, + delta: { + type: "input_json_delta", + partial_json: "{\"command\":\"pwd\"}" + } + } + ])) + ) + )) + ) + + const parts = yield* LanguageModel.streamText({ + prompt: "run pwd", + disableToolCallResolution: true + }).pipe( + Stream.runCollect, + Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")), + Effect.provide(layer) + ) + const delta = globalThis.Array.from(parts).find((part) => part.type === "tool-params-delta") + assert.isDefined(delta) + if (delta?.type === "tool-params-delta") { + assert.deepStrictEqual(JSON.parse(delta.delta), { + type: "bash_code_execution", + command: "pwd" + }) + } + })) +}) + +const makeHttpClient = ( + handler: ( + request: HttpClientRequest.HttpClientRequest + ) => Effect.Effect +) => + HttpClient.makeWith( + Effect.fnUntraced(function*(requestEffect) { + return yield* handler(yield* requestEffect) + }), + Effect.succeed as HttpClient.HttpClient.Preprocess + ) + +const sseResponse = (request: HttpClientRequest.HttpClientRequest, events: ReadonlyArray) => + HttpClientResponse.fromWeb( + request, + new Response( + events.map((event) => `event: message_stream\ndata: ${JSON.stringify(event)}\n\n`).join(""), + { status: 200, headers: { "content-type": "text/event-stream" } } + ) + ) From 5650b8ae8901b874c6a3817a65b1d2e87560d34b Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Tue, 4 Aug 2026 09:27:01 +1200 Subject: [PATCH 2/2] Fix streamed Anthropic code execution parameters --- .../fix-anthropic-code-execution-deltas.md | 5 + .../anthropic/src/AnthropicLanguageModel.ts | 4 +- .../test/AnthropicLanguageModel.test.ts | 116 ++++++++++++++++++ .../AnthropicMalformedCodeDeltaAudit.test.ts | 97 --------------- 4 files changed, 123 insertions(+), 99 deletions(-) create mode 100644 .changeset/fix-anthropic-code-execution-deltas.md delete mode 100644 packages/ai/anthropic/test/AnthropicMalformedCodeDeltaAudit.test.ts diff --git a/.changeset/fix-anthropic-code-execution-deltas.md b/.changeset/fix-anthropic-code-execution-deltas.md new file mode 100644 index 00000000000..a50a451a256 --- /dev/null +++ b/.changeset/fix-anthropic-code-execution-deltas.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-anthropic": patch +--- + +Fix malformed JSON in streamed Anthropic code-execution tool parameters. diff --git a/packages/ai/anthropic/src/AnthropicLanguageModel.ts b/packages/ai/anthropic/src/AnthropicLanguageModel.ts index 922e8d9ccf7..6cb54a3d556 100644 --- a/packages/ai/anthropic/src/AnthropicLanguageModel.ts +++ b/packages/ai/anthropic/src/AnthropicLanguageModel.ts @@ -2575,7 +2575,7 @@ const makeStreamResponse = Effect.fnUntraced( (contentBlock.providerName === "bash_code_execution" || contentBlock.providerName === "text_editor_code_execution") ) { - delta = `{"type":${contentBlock.providerName},${delta.substring(1)}}` + delta = `{"type":${JSON.stringify(contentBlock.providerName)},${delta.substring(1)}` } parts.push({ @@ -2650,7 +2650,7 @@ const makeStreamResponse = Effect.fnUntraced( } const params = contentBlock.providerExecuted === true - ? finalParams + ? Tool.unsafeSecureJsonParse(finalParams) : yield* transformToolCallParams( options.tools, contentBlock.name, diff --git a/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts b/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts index f5d2848b07d..f2cc7d7b694 100644 --- a/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts +++ b/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts @@ -112,6 +112,122 @@ describe("AnthropicLanguageModel", () => { assert.deepStrictEqual(toolCall.params, toolParams) })) + const codeExecutionCases = [ + { + providerName: "bash_code_execution", + toolParams: { command: "pwd" }, + expectedParams: { type: "bash_code_execution", command: "pwd" } + }, + { + providerName: "text_editor_code_execution", + toolParams: { command: "view", path: "/tmp/example.txt" }, + expectedParams: { + type: "text_editor_code_execution", + command: "view", + path: "/tmp/example.txt" + } + } + ] as const + + for (const { expectedParams, providerName, toolParams } of codeExecutionCases) { + it.effect(`emits valid JSON for streamed ${providerName} parameters`, () => + Effect.gen(function*() { + const toolkit = Toolkit.make(AnthropicTool.CodeExecution_20250522()) + const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(sseResponse(request, [ + { + type: "message_start", + message: { + id: "msg_test_1", + type: "message", + role: "assistant", + model: "claude-sonnet-4-20250514", + content: [], + stop_reason: null, + stop_sequence: null, + usage: { + cache_creation: null, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + inference_geo: null, + input_tokens: 1, + output_tokens: 0, + service_tier: null + } + } + }, + { + type: "content_block_start", + index: 0, + content_block: { + type: "server_tool_use", + id: "srvtoolu_test_1", + name: providerName, + input: {} + } + }, + { + type: "content_block_delta", + index: 0, + delta: { + type: "input_json_delta", + partial_json: JSON.stringify(toolParams) + } + }, + { + type: "content_block_stop", + index: 0 + }, + { + type: "message_delta", + delta: { + stop_reason: "tool_use", + stop_sequence: null + }, + usage: { + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + input_tokens: null, + output_tokens: 1 + } + }, + { + type: "message_stop" + } + ])) + ) + )) + ) + + const partsChunk = yield* LanguageModel.streamText({ + prompt: "run pwd", + toolkit, + disableToolCallResolution: true + }).pipe( + Stream.runCollect, + Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")), + Effect.provide(layer) + ) + + const parts = globalThis.Array.from(partsChunk) + + const delta = parts.find((part) => part.type === "tool-params-delta") + assert.isDefined(delta) + if (delta?.type === "tool-params-delta") { + assert.deepStrictEqual(JSON.parse(delta.delta), expectedParams) + } + + const toolCall = parts.find((part) => part.type === "tool-call") + assert.isDefined(toolCall) + if (toolCall?.type === "tool-call") { + assert.deepStrictEqual(toolCall.params, expectedParams) + } + })) + } + // `Model` is an open enum in Anthropic's spec (`anyOf: [{ type: string }, ...consts]`), and it is // $ref'd by response schemas. Responses must therefore decode for model ids that are newer than the // generated literals, and the id must survive decoding unchanged. diff --git a/packages/ai/anthropic/test/AnthropicMalformedCodeDeltaAudit.test.ts b/packages/ai/anthropic/test/AnthropicMalformedCodeDeltaAudit.test.ts deleted file mode 100644 index b5b28d36fad..00000000000 --- a/packages/ai/anthropic/test/AnthropicMalformedCodeDeltaAudit.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { AnthropicClient, AnthropicLanguageModel } from "@effect/ai-anthropic" -import { assert, describe, it } from "@effect/vitest" -import { Effect, Layer, Redacted, Stream } from "effect" -import { LanguageModel } from "effect/unstable/ai" -import { HttpClient, type HttpClientError, type HttpClientRequest, HttpClientResponse } from "effect/unstable/http" - -describe("AnthropicLanguageModel malformed code delta", () => { - it.effect("emits valid JSON for streamed code execution parameters", () => - Effect.gen(function*() { - const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( - Layer.provide(Layer.succeed( - HttpClient.HttpClient, - makeHttpClient((request) => - Effect.succeed(sseResponse(request, [ - { - type: "message_start", - message: { - id: "msg_1", - type: "message", - role: "assistant", - model: "claude-sonnet-4-20250514", - content: [], - stop_reason: null, - stop_sequence: null, - usage: { - cache_creation: null, - cache_creation_input_tokens: null, - cache_read_input_tokens: null, - inference_geo: null, - input_tokens: 1, - output_tokens: 0, - service_tier: null - } - } - }, - { - type: "content_block_start", - index: 0, - content_block: { - type: "server_tool_use", - id: "srvtoolu_1", - name: "bash_code_execution", - input: {} - } - }, - { - type: "content_block_delta", - index: 0, - delta: { - type: "input_json_delta", - partial_json: "{\"command\":\"pwd\"}" - } - } - ])) - ) - )) - ) - - const parts = yield* LanguageModel.streamText({ - prompt: "run pwd", - disableToolCallResolution: true - }).pipe( - Stream.runCollect, - Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")), - Effect.provide(layer) - ) - const delta = globalThis.Array.from(parts).find((part) => part.type === "tool-params-delta") - assert.isDefined(delta) - if (delta?.type === "tool-params-delta") { - assert.deepStrictEqual(JSON.parse(delta.delta), { - type: "bash_code_execution", - command: "pwd" - }) - } - })) -}) - -const makeHttpClient = ( - handler: ( - request: HttpClientRequest.HttpClientRequest - ) => Effect.Effect -) => - HttpClient.makeWith( - Effect.fnUntraced(function*(requestEffect) { - return yield* handler(yield* requestEffect) - }), - Effect.succeed as HttpClient.HttpClient.Preprocess - ) - -const sseResponse = (request: HttpClientRequest.HttpClientRequest, events: ReadonlyArray) => - HttpClientResponse.fromWeb( - request, - new Response( - events.map((event) => `event: message_stream\ndata: ${JSON.stringify(event)}\n\n`).join(""), - { status: 200, headers: { "content-type": "text/event-stream" } } - ) - )