Files
OmniRoute/tests/unit/probe-claude-gemini-tool-casing.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

114 lines
4.1 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
const { claudeToGeminiRequest } =
await import("../../open-sse/translator/request/claude-to-gemini.ts");
const { geminiToClaudeResponse } =
await import("../../open-sse/translator/response/gemini-to-claude.ts");
// Claude Code (Anthropic Messages format) → OmniRoute → Gemini.
//
// Gemini always lowercases tool names in its functionCall responses, so the
// round trip only works if the request translator publishes a lowercase alias
// ("read" → "Read") in `_toolNameMap`. Claude Code registers its tools in
// TitleCase and rejects anything else with "No such tool available: read".
//
// The identity-entry filter in claude-to-gemini.ts dropped `Read → Read`
// (sanitized === original), so no alias reached the response translator and
// `normalizeToolName()` — whose REVERSE_MAP is keyed by TitleCase — left the
// lowercase name untouched. Same class as #9568, which only fixed the
// openai-to-gemini request path.
function claudeBodyWithTools(names: string[]) {
return {
messages: [{ role: "user", content: "read package.json" }],
tools: names.map((name) => ({
name,
description: `${name} tool`,
input_schema: { type: "object", properties: {} },
})),
};
}
function firstToolUseName(events) {
for (const event of events || []) {
if (event?.type === "content_block_start" && event.content_block?.type === "tool_use") {
return event.content_block.name;
}
}
return null;
}
function geminiFunctionCallChunk(name: string) {
return {
candidates: [
{
content: { parts: [{ functionCall: { name, args: { file_path: "package.json" } } }] },
finishReason: "STOP",
},
],
};
}
test("claude-to-gemini: TitleCase tool name publishes a lowercase alias in _toolNameMap", () => {
const translated = claudeToGeminiRequest("gemini-2.5-pro", claudeBodyWithTools(["Read"]), false);
const map = translated._toolNameMap;
assert.ok(map instanceof Map, "_toolNameMap must be present so the response can be remapped");
assert.equal(
map.get("read"),
"Read",
"Gemini lowercases functionCall names — the map needs a 'read' key to restore 'Read'"
);
});
test("Claude Code round trip: Gemini's lowercase 'read' is restored to 'Read'", () => {
const translated = claudeToGeminiRequest("gemini-2.5-pro", claudeBodyWithTools(["Read"]), true);
// chatCore extracts `_toolNameMap` from the translated request body and hands
// it to the response translator as `state.toolNameMap`.
const state = { toolNameMap: translated._toolNameMap, contentBlockIndex: 0 };
const events = geminiToClaudeResponse(geminiFunctionCallChunk("read"), state);
assert.equal(
firstToolUseName(events),
"Read",
"Claude Code registers 'Read'; emitting 'read' fails with 'No such tool available: read'"
);
});
test("Claude Code round trip: multiple TitleCase tools survive the lowercase round trip", () => {
const translated = claudeToGeminiRequest(
"gemini-2.5-pro",
claudeBodyWithTools(["Read", "Bash", "WebFetch"]),
true
);
for (const [lowered, expected] of [
["read", "Read"],
["bash", "Bash"],
["webfetch", "WebFetch"],
]) {
const state = { toolNameMap: translated._toolNameMap, contentBlockIndex: 0 };
const events = geminiToClaudeResponse(geminiFunctionCallChunk(lowered), state);
assert.equal(firstToolUseName(events), expected, `${lowered} should restore to ${expected}`);
}
});
test("claude-to-gemini: names actually sanitized for Gemini keep their original mapping", () => {
// A tool name Gemini cannot accept verbatim must still map back to the original.
const translated = claudeToGeminiRequest(
"gemini-2.5-pro",
claudeBodyWithTools(["mcp__server__do-thing"]),
false
);
const map = translated._toolNameMap;
assert.ok(map instanceof Map, "sanitized names must still publish a map");
const restored = [...map.values()];
assert.ok(
restored.includes("mcp__server__do-thing"),
"the original (unsanitized) name must remain reachable for the response translator"
);
});