Files
OmniRoute/tests/unit/probe-9408-tool-use-protocol.test.ts
Praveen K Palaniswamy 65e81158ab fix(ollama): route models by advertised capability (#11088)
Landed with the design call resolved per the owner's pick — **option 1**: the synced store is now endpoint-agnostic (persistDiscoveredModels and managedModelImport no longer drop non-chat models at write time), and chat selectability moved to read time (auto-pool expansion in autoStrategy applies filterChatSelectableModels; the models-route projection already had its chatOnly filter). Your discovery test now passes end-to-end (3/3): /api/show capabilities persist per connection and image/embedding requests route through the advertising host.

Reconciliation notes: conflicted areas merged onto the current tip (adobe discovery import, requestedModel preflight signature, resolvedProvider fast-path coexists with the synced-route override — explicit resolution wins); carried base-red drains (#10055 memoization, #11071 test variants) dropped as already-landed; the managed-model-import exclusion test was propagated to the new contract (image/video models persist; the read filter still hides them from chat pickers — pinned by a new assertion). Full battery: 205/206 focused (the one red is a confirmed periodic-timer timing flake on the loaded devbox — 20/20 isolated), autoCombo vitest 30/30, combo suites 46/46, gates + typecheck clean.

Thank you @yourspraveen — the capability probe + routing design was right; it just needed the store contract opened up. Fixes #11087.
2026-08-23 11:45:01 -03:00

254 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { createClaudeWebResponse } from "../../open-sse/executors/claude-web/stream.ts";
function byteStream(text: string): ReadableStream<Uint8Array> {
const bytes = new TextEncoder().encode(text);
return new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(bytes);
controller.close();
},
});
}
function frames(events: Array<Record<string, unknown>>, newline = "\n"): string {
return events.map((event) => `data: ${JSON.stringify(event)}${newline}${newline}`).join("");
}
/**
* Reproduce #9408: Claude Web emits tool_use content blocks and the stream
* parser has no handler for them, causing input_json_delta to be rejected as
* a protocol violation → HTTP 502.
*
* Upstream event sequence:
* message_start
* → content_block_start(type:"tool_use", id:"toolu_xxx", name:"get_weather")
* → ×3 content_block_delta(type:"input_json_delta", partial_json:"...")
* → content_block_stop
* → message_delta(stop_reason:"tool_use")
* → message_stop
*/
describe("Claude Web tool_use protocol (#9408)", () => {
it("converts tool_use blocks to tool_calls in buffered mode", async () => {
const events = [
{ type: "message_start", message: { model: "claude-sonnet-5" } },
{
type: "content_block_start",
index: 0,
content_block: {
type: "tool_use",
id: "toolu_9408_001",
name: "get_weather",
input: {},
},
},
{
type: "content_block_delta",
index: 0,
delta: { type: "input_json_delta", partial_json: '{"loca' },
},
{
type: "content_block_delta",
index: 0,
delta: { type: "input_json_delta", partial_json: 'tion": "Sa' },
},
{
type: "content_block_delta",
index: 0,
delta: { type: "input_json_delta", partial_json: 'n Francisco"}' },
},
{ type: "content_block_stop", index: 0 },
{ type: "message_delta", delta: { stop_reason: "tool_use" } },
{ type: "message_stop" },
];
const completions: Array<{ assistantText: string; stopReason: string }> = [];
let failures = 0;
const response = await createClaudeWebResponse(byteStream(frames(events)), {
model: "claude-sonnet-5",
stream: false,
responseMetadata: {},
onComplete: (result) => completions.push(result),
onFailure: () => {
failures += 1;
},
});
// Should NOT be 502 — the bug was that tool_use blocks caused protocol failure
assert.equal(response.status, 200, "Expected 200, not 502 — tool_use should not crash");
const body = (await response.json()) as {
choices: Array<{
message: {
content: string | null;
tool_calls?: Array<{
id: string;
type: string;
function: { name: string; arguments: string };
}>;
};
finish_reason: string;
}>;
};
assert.equal(body.choices[0].finish_reason, "tool_calls");
assert.ok(body.choices[0].message.tool_calls, "Expected tool_calls in message");
assert.equal(body.choices[0].message.tool_calls!.length, 1);
assert.equal(body.choices[0].message.tool_calls![0].id, "toolu_9408_001");
assert.equal(body.choices[0].message.tool_calls![0].type, "function");
assert.equal(body.choices[0].message.tool_calls![0].function.name, "get_weather");
// Content should be null when there's only a tool call
assert.equal(body.choices[0].message.content, null);
// Preserve upstream tool call ID — the input should parse correctly
const parsed = JSON.parse(body.choices[0].message.tool_calls![0].function.arguments);
assert.deepEqual(parsed, { location: "San Francisco" });
assert.deepEqual(completions, [{ assistantText: "", stopReason: "tool_use" }]);
assert.equal(failures, 0);
});
it("converts tool_use blocks to tool_calls in streaming mode", async () => {
const events = [
{ type: "message_start", message: { model: "claude-sonnet-5" } },
{
type: "content_block_start",
index: 0,
content_block: {
type: "tool_use",
id: "toolu_9408_002",
name: "search_code",
input: {},
},
},
{
type: "content_block_delta",
index: 0,
delta: { type: "input_json_delta", partial_json: '{"query":"initial"' },
},
{
type: "content_block_delta",
index: 0,
delta: { type: "input_json_delta", partial_json: ',"limit":10}' },
},
{ type: "content_block_stop", index: 0 },
{ type: "message_delta", delta: { stop_reason: "tool_use" } },
{ type: "message_stop" },
];
const completions: Array<{ assistantText: string; stopReason: string }> = [];
let failures = 0;
const response = await createClaudeWebResponse(byteStream(frames(events)), {
model: "claude-sonnet-5",
stream: true,
responseMetadata: {},
onComplete: (result) => completions.push(result),
onFailure: () => {
failures += 1;
},
});
assert.equal(response.status, 200, "Expected 200, not 502");
const output = await response.text();
// Verify it contains tool_calls in some chunk
assert.match(output, /tool_calls/);
// Verify finish_reason: tool_calls
assert.match(output, /"finish_reason":"tool_calls"/);
// Verify tool call id preserved
assert.match(output, /"id":"toolu_9408_002"/);
// Verify tool call name
assert.match(output, /"name":"search_code"/);
// Verify arguments contain the accumulated input
assert.match(output, /"arguments":".*query.*initial.*limit.*10/);
assert.deepEqual(completions, [{ assistantText: "", stopReason: "tool_use" }]);
assert.equal(failures, 0);
});
it("handles tool_use alongside text content", async () => {
const events = [
{ type: "message_start", message: { model: "claude-sonnet-5" } },
{ type: "content_block_start", index: 0, content_block: { type: "text" } },
{
type: "content_block_delta",
index: 0,
delta: { type: "text_delta", text: "I'll look that up." },
},
{ type: "content_block_stop", index: 0 },
{
type: "content_block_start",
index: 1,
content_block: {
type: "tool_use",
id: "toolu_9408_003",
name: "get_info",
input: { topic: "weather" },
},
},
{ type: "content_block_stop", index: 1 },
{ type: "message_delta", delta: { stop_reason: "tool_use" } },
{ type: "message_stop" },
];
const response = await createClaudeWebResponse(byteStream(frames(events)), {
model: "claude-sonnet-5",
stream: false,
responseMetadata: {},
onComplete() {},
onFailure() {},
});
assert.equal(response.status, 200);
const body = (await response.json()) as {
choices: Array<{
message: {
content: string | null;
tool_calls?: Array<{
id: string;
type: string;
function: { name: string; arguments: string };
}>;
};
finish_reason: string;
}>;
};
// Should have text content AND tool calls
assert.equal(body.choices[0].message.content, "I'll look that up.");
assert.equal(body.choices[0].message.tool_calls!.length, 1);
assert.equal(body.choices[0].message.tool_calls![0].id, "toolu_9408_003");
});
it("rejects input_json_delta when no tool_use block is open", async () => {
const events = [
{ type: "message_start" },
{ type: "content_block_start", index: 0, content_block: { type: "text" } },
{
type: "content_block_delta",
index: 0,
delta: { type: "input_json_delta", partial_json: "{}" },
},
{ type: "content_block_stop", index: 0 },
{ type: "message_delta", delta: { stop_reason: "end_turn" } },
{ type: "message_stop" },
];
const completions: Array<unknown> = [];
let failures = 0;
const response = await createClaudeWebResponse(byteStream(frames(events)), {
model: "claude-sonnet-5",
stream: false,
responseMetadata: {},
onComplete: (result) => completions.push(result),
onFailure: () => {
failures += 1;
},
});
assert.equal(response.status, 502, "input_json_delta without open tool_use should fail");
assert.deepEqual(completions, []);
assert.equal(failures, 1);
});
});