Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/adapters/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -999,12 +999,12 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
break;
}
case "content_block_start": {
const block = data.content_block as { type: string; id?: string; name?: string; data?: string } | undefined;
const block = data.content_block as { type: string; id?: string; name?: unknown; data?: string } | undefined;
if (!block) break;
currentBlockType = block.type;
if (block.type === "tool_use") {
currentToolCallId = usableToolUseId(block.id);
currentToolCallName = toolNames.fromWire(block.name ?? "");
currentToolCallName = toolNames.fromWire(typeof block.name === "string" ? block.name : "");
currentToolCallJson = "";
budget.openCall(currentToolCallId);
yield { type: "tool_call_start", id: currentToolCallId, name: currentToolCallName };
Expand Down Expand Up @@ -1157,7 +1157,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
budget.chargeRetained(responseBytes, { kind: "retained_collectors" });
try {
const events: AdapterEvent[] = [];
const content = json.content as { type: string; text?: string; id?: string; name?: string; input?: unknown; thinking?: string; reasoning?: string; signature?: string; data?: string }[] | undefined;
const content = json.content as { type: string; text?: string; id?: string; name?: unknown; input?: unknown; thinking?: string; reasoning?: string; signature?: string; data?: string }[] | undefined;
if (content) {
for (const block of content) {
if (block.type === "text" && block.text) {
Expand All @@ -1173,7 +1173,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
events.push({ type: "redacted_thinking", data: block.data });
} else if (block.type === "tool_use") {
const id = usableToolUseId(block.id);
events.push({ type: "tool_call_start", id, name: toolNames.fromWire(block.name ?? "") });
events.push({ type: "tool_call_start", id, name: toolNames.fromWire(typeof block.name === "string" ? block.name : "") });
events.push({ type: "tool_call_delta", arguments: toolUseArguments(block.input, provider.anthropicEofTolerance === true) });
events.push({ type: "tool_call_end" });
}
Expand Down
31 changes: 31 additions & 0 deletions tests/anthropic-stream-hardening.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,24 @@ describe("anthropicMessagesUrl", () => {
});

describe("anthropic stream hardening", () => {
test("malformed escaped tool names degrade to an empty name", async () => {
const response = new Response([
"event: content_block_start\n",
'data: {"type":"content_block_start","content_block":{"type":"tool_use","id":"toolu_bad_name","name":{"bad":1}}}\n\n',
"event: content_block_stop\n",
'data: {"type":"content_block_stop"}\n\n',
"event: message_stop\n",
'data: {"type":"message_stop"}\n\n',
].join(""));
const escapedProvider = { ...provider, escapeBuiltinToolNames: true };
const events = await collect(createAnthropicAdapter(escapedProvider).parseStream(response));
expect(events.find(e => e.type === "tool_call_start")).toMatchObject({
type: "tool_call_start",
name: "",
});
expect(events.at(-1)?.type).toBe("done");
});

test("EOF after content without message_stop fails closed", async () => {
const response = new Response([
"event: content_block_start\n",
Expand Down Expand Up @@ -102,6 +120,19 @@ describe("anthropic stream hardening", () => {
});

describe("anthropic non-stream tool_use input", () => {
test("malformed escaped tool names degrade to an empty name", async () => {
const adapter = createAnthropicAdapter({ ...provider, escapeBuiltinToolNames: true });
const events = await adapter.parseResponse!(new Response(JSON.stringify({
content: [{ type: "tool_use", id: "toolu_bad_name", name: { bad: 1 }, input: {} }],
stop_reason: "tool_use",
})));
expect(events.find(e => e.type === "tool_call_start")).toMatchObject({
type: "tool_call_start",
name: "",
});
expect(events.at(-1)?.type).toBe("done");
});

test("parses string tool_use.input", async () => {
const adapter = createAnthropicAdapter(provider);
const events = await adapter.parseResponse!(new Response(JSON.stringify({
Expand Down
Loading