mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 21:32:10 +03:00
fix(translator): clamp Responses API call_id to 64 chars (#4317)
* fix(translator): clamp Responses API call_id to 64 chars (port from 9router#396) The OpenAI Responses API rejects call_id values longer than 64 characters with a 400. Long upstream tool-call ids (some clients emit ids well over the limit) were forwarded verbatim. Clamp the id deterministically on both the function_call item and its matching function_call_output, so the pair stays matched through the orphaned-output filter and the request is accepted. Reported-by: ngapngap (https://github.com/decolua/9router/issues/393) Co-authored-by: Anurag Saxena <17893081+anuragg-saxenaa@users.noreply.github.com> Co-authored-by: ngapngap <27039619+ngapngap@users.noreply.github.com> * chore(quality): bump translator-openai-responses-req file-size baseline 1011->1047 The clamp-call_id regression test (+36 lines) grew the test file past its frozen baseline; bump it in the same change (Rule #9). --------- Co-authored-by: Anurag Saxena <17893081+anuragg-saxenaa@users.noreply.github.com> Co-authored-by: ngapngap <27039619+ngapngap@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
032387401a
commit
0abc00cf2a
@@ -25,6 +25,7 @@ _In development — bullets added per PR; finalized at release._
|
||||
- **fix(api): cache-HIT `X-OmniRoute-Response-Cost` now reports the incremental cost (≈0), not the original** — on a semantic-cache HIT the gateway serves the stored response **without** an upstream call, but `X-OmniRoute-Response-Cost` was reporting the original call's full cost (recomputed from the cached `usage`). A consumer summing `response-cost` for billing was therefore charging for responses that cost ≈$0 to serve (and stale entries could inflate it). Cache hits now bill `X-OmniRoute-Response-Cost: 0.0000000000` (the real incremental cost), and the avoided cost is surfaced in a new **`X-OmniRoute-Cost-Saved`** header for cache analytics — mirroring the existing `tokens_saved` concept. The MISS path is unchanged. (PRD-2026-06-19-cache-hit-cost-reporting)
|
||||
- **fix(models): imported vision-capable models keep their vision capability** — after importing a provider key, vision-capable models (e.g. OpenRouter models whose `architecture` declares image input, and other synced providers) were listed as text-only in `/v1/models` and the dashboard — even though image requests actually worked. Synced model records never captured the vision flag, and the catalog's OpenRouter live-enrichment (which derives vision from `architecture.input_modalities`) is skipped once a provider has synced models. Discovery now captures `supportsVision` at sync time (from `architecture.input_modalities`, the string `architecture.modality`, or a top-level `input_modalities`), mirroring the existing `supportsThinking` capture, and the catalog surfaces `capabilities.vision` for synced models. ([#4264](https://github.com/diegosouzapw/OmniRoute/issues/4264) — thanks @FerLuisxd)
|
||||
- **fix(providers): Cloudflare Workers AI model discovery shows model names, not UUIDs** — importing a Cloudflare Workers AI key listed models with internal UUID identifiers (e.g. `429b9e8b-d99e-…`) instead of their usable slugs (`@cf/meta/llama-3.1-8b-instruct`). Cloudflare's `/ai/models/search` returns `{ id: "<uuid>", name: "@cf/…" }`, and discovery was passing the raw objects through — so the UUID `id` became the callable model id. The `cloudflare-ai` discovery now maps each result's `name` → id, surfacing the real `@cf/…` model ids. ([#4259](https://github.com/diegosouzapw/OmniRoute/issues/4259) — thanks @FerLuisxd)
|
||||
- **fix(translator): clamp Responses API `call_id` to 64 characters** — the OpenAI Responses API rejects `call_id` values longer than 64 characters with a 400. Long upstream tool-call ids (some clients emit ids well over the limit) are now clamped deterministically on both the `function_call` item and its matching `function_call_output`, so the pair stays matched through the orphaned-output filter and the request is accepted. (thanks @anuragg-saxenaa, @ngapngap)
|
||||
- **fix(oauth): GitHub Copilot token refresh now sends the public client_id** — the `github` provider config never carried a `clientId`, so GitHub OAuth `refresh_token` exchanges either omitted `client_id` or sent the literal string `undefined` (and a bogus `client_secret=undefined`), which GitHub rejects — leaving a Copilot connection stuck once its short-lived token expired and the long-lived refresh path was needed. The provider now resolves its public device-flow `client_id` from the embedded public credential and omits `client_secret` entirely (GitHub's Copilot app is a public client with no secret). (thanks @baslr)
|
||||
|
||||
---
|
||||
|
||||
@@ -209,7 +209,7 @@
|
||||
"tests/unit/token-refresh-service.test.ts": 1322,
|
||||
"tests/unit/translator-friendly-test-bench.test.tsx": 848,
|
||||
"tests/unit/translator-helper-branches.test.ts": 850,
|
||||
"tests/unit/translator-openai-responses-req.test.ts": 1011,
|
||||
"tests/unit/translator-openai-responses-req.test.ts": 1047,
|
||||
"tests/unit/translator-openai-to-gemini.test.ts": 1579,
|
||||
"tests/unit/translator-openai-to-kiro.test.ts": 918,
|
||||
"tests/unit/translator-resp-gemini-to-openai.test.ts": 1234,
|
||||
|
||||
@@ -35,6 +35,14 @@ function toRecord(value: unknown): JsonRecord {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
|
||||
}
|
||||
|
||||
// The Responses API rejects call_id values longer than 64 characters (9router#396).
|
||||
// Clamp deterministically so a function_call and its matching function_call_output keep
|
||||
// the same id and stay paired through the orphaned-output filter below.
|
||||
const MAX_CALL_ID_LEN = 64;
|
||||
function clampCallId(id: string): string {
|
||||
return id.length > MAX_CALL_ID_LEN ? id.slice(0, MAX_CALL_ID_LEN) : id;
|
||||
}
|
||||
|
||||
function toArray(value: unknown): unknown[] {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
@@ -579,7 +587,7 @@ export function openaiToOpenAIResponsesRequest(
|
||||
}
|
||||
input.push({
|
||||
type: "function_call",
|
||||
call_id: toString(toolCall.id).trim() || generateToolCallId(),
|
||||
call_id: clampCallId(toString(toolCall.id).trim() || generateToolCallId()),
|
||||
name: fnName,
|
||||
arguments: toString(fn.arguments, "{}"),
|
||||
});
|
||||
@@ -593,7 +601,7 @@ export function openaiToOpenAIResponsesRequest(
|
||||
if (fnName) {
|
||||
input.push({
|
||||
type: "function_call",
|
||||
call_id: `call_${fnName}`,
|
||||
call_id: clampCallId(`call_${fnName}`),
|
||||
name: fnName,
|
||||
arguments: toString(fc.arguments, "{}"),
|
||||
});
|
||||
@@ -605,7 +613,7 @@ export function openaiToOpenAIResponsesRequest(
|
||||
if (role === "tool") {
|
||||
input.push({
|
||||
type: "function_call_output",
|
||||
call_id: toString(msg.tool_call_id),
|
||||
call_id: clampCallId(toString(msg.tool_call_id)),
|
||||
output:
|
||||
typeof msg.content === "string"
|
||||
? msg.content
|
||||
@@ -624,7 +632,7 @@ export function openaiToOpenAIResponsesRequest(
|
||||
if (role === "function") {
|
||||
input.push({
|
||||
type: "function_call_output",
|
||||
call_id: `call_${toString(msg.name)}`,
|
||||
call_id: clampCallId(`call_${toString(msg.name)}`),
|
||||
output: typeof msg.content === "string" ? msg.content : String(msg.content ?? ""),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -252,6 +252,42 @@ test("Responses -> Chat strips client_metadata (Mistral 422 fix)", () => {
|
||||
assert.equal((result.messages as unknown[]).length, 1, "user message must be preserved");
|
||||
});
|
||||
|
||||
test("Chat -> Responses clamps call_id to 64 chars and keeps the pair matched (port from 9router#396)", () => {
|
||||
// The Responses API rejects call_id values longer than 64 characters. A long
|
||||
// upstream tool-call id must be clamped on BOTH the function_call and its matching
|
||||
// function_call_output, identically, so the orphan filter still pairs them.
|
||||
const longId = "call_" + "a".repeat(80); // 85 chars, > 64
|
||||
const result = openaiToOpenAIResponsesRequest(
|
||||
"gpt-4o",
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{ id: longId, type: "function", function: { name: "read_file", arguments: "{}" } },
|
||||
],
|
||||
},
|
||||
{ role: "tool", tool_call_id: longId, content: "ok" },
|
||||
],
|
||||
},
|
||||
false,
|
||||
null
|
||||
) as any;
|
||||
|
||||
const input = result.input as Array<Record<string, any>>;
|
||||
const fnCall = input.find((i) => i.type === "function_call");
|
||||
const fnOut = input.find((i) => i.type === "function_call_output");
|
||||
assert.ok(fnCall, "function_call item must exist");
|
||||
assert.equal(fnCall.call_id.length, 64, "function_call call_id must be clamped to 64 chars");
|
||||
assert.ok(fnOut, "function_call_output must survive the orphan filter after clamping");
|
||||
assert.equal(
|
||||
fnOut.call_id,
|
||||
fnCall.call_id,
|
||||
"output call_id must match the clamped function_call id"
|
||||
);
|
||||
});
|
||||
|
||||
test("Chat -> Responses converts messages, tool calls, tool outputs, tools and pass-through params", () => {
|
||||
const result = openaiToOpenAIResponsesRequest(
|
||||
"gpt-4o",
|
||||
|
||||
Reference in New Issue
Block a user