Files
OmniRoute/open-sse/executors/kimiToolNames.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

43 lines
1.8 KiB
TypeScript

type JsonRecord = Record<string, unknown>;
function asRecord(value: unknown): JsonRecord | null {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : null;
}
// ponytail: Kimi K3 (Moonshot) enforces a stricter tool-message contract than most
// OpenAI-compatible APIs: every role:"tool" message must carry a `name` field matching
// the function that issued the tool_call_id. When requests arrive through combo routing
// or format translation, the `name` field is frequently stripped, causing a 400.
// This builds a tool_call_id -> function.name lookup from assistant tool_calls and
// backfills missing names. Shared by KimiExecutor and DefaultExecutor (for BYOK providers).
// Upgrade path: if Moonshot relaxes this requirement, this function becomes a no-op.
export function ensureToolMessageNames(record: JsonRecord): JsonRecord {
if (!Array.isArray(record.messages)) return record;
const callIdToName = new Map<string, string>();
for (const msg of record.messages) {
const m = asRecord(msg);
if (!m || m.role !== "assistant" || !Array.isArray(m.tool_calls)) continue;
for (const tc of m.tool_calls as { id?: string; function?: { name?: string } }[]) {
if (tc?.id && typeof tc.function?.name === "string") {
callIdToName.set(String(tc.id), tc.function.name);
}
}
}
if (callIdToName.size === 0) return record;
let modified = false;
const messages = record.messages.map((msg: unknown) => {
const m = asRecord(msg);
if (!m || m.role !== "tool" || typeof m.name === "string") return msg;
const callId = String(m.tool_call_id ?? "");
const resolvedName = callIdToName.get(callId);
if (!resolvedName) return msg;
modified = true;
return { ...m, name: resolvedName };
});
return modified ? { ...record, messages } : record;
}