mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-20 22:22:57 +03:00
This commit is contained in:
committed by
GitHub
parent
e096a4c937
commit
ad067a193e
@@ -16,6 +16,7 @@
|
||||
|
||||
### 🐛 Fixed
|
||||
|
||||
- **fix(opencode): forward the OpenCode session id to the upstream regardless of how the user named the provider** — the `OpencodeExecutor` forwarded the `x-opencode-session/request/project/client` headers, but the OpenCode CLI only emits those when the configured `providerID` **starts with** `"opencode"`. A user who adds OmniRoute as a custom provider (e.g. `"omniroute"`) makes the CLI send `x-session-affinity` / `X-Session-Id` instead (both carry the same session id), which the executor never read — so the session-metadata forwarding was effectively dead code for the realistic provider-naming case. The opencode-family executor now falls back to `x-session-affinity` / `X-Session-Id` and maps it onto `x-opencode-session` when the client didn't send the header directly, so session continuity to the `opencode.ai` upstream works for any provider name (a direct `x-opencode-session` still wins). Scoped to this executor only — the generic `DefaultExecutor` intentionally does **not** do this, to avoid leaking the client session id to arbitrary third-party upstreams. ([#4022](https://github.com/diegosouzapw/OmniRoute/issues/4022) — thanks @pizzav-xyz)
|
||||
- **fix(guardrails): Vision Bridge no longer drops the image when the describe call fails (Nvidia NIM "Image unavailable")** — the Vision Bridge is enabled by default and engages for any model whose vision capability OmniRoute can't prove from the registry (`supportsVision !== true`, which includes uncatalogued models that resolve to `null`). When the per-image describe call failed (e.g. no vision model configured), it replaced the image with the literal text `[Image N]: (unavailable)` and dropped the original `image_url` — so a genuinely vision-capable upstream (Nvidia NIM) received text only and answered "Image unavailable. Cannot provide description without visual data." A describe failure is no longer destructive: `replaceImageParts` now receives `null` for failed images and **preserves the original image part** so the upstream can still see it (successful describes still replace the image with the text description; `meta.descriptions` observability is unchanged). ([#4012](https://github.com/diegosouzapw/OmniRoute/issues/4012) — thanks @daniij)
|
||||
- **fix(kiro): preserve `finish_reason: "tool_calls"` on the Kiro streaming path** — streaming tool-call requests through the Kiro (Responses API) provider had their terminal `finish_reason` reported as `"stop"` instead of `"tool_calls"`, so agent clients (Hermes) treated the tool-call turn as a finished turn, never ran the tool, and the next request failed with HTTP 400 on the incomplete tool state. `convertKiroToOpenAI`'s terminal `messageStopEvent`/`done` branch hardcoded `finish_reason: "stop"` regardless of whether the stream had emitted `toolUseEvent`s. The translator now records `state.sawToolUse` when a tool-use chunk is emitted and reports `finish_reason: "tool_calls"` on the terminal chunk (and in `state.finishReason`) whenever the stream produced tool calls. The non-streaming path was already correct. ([#3980](https://github.com/diegosouzapw/OmniRoute/issues/3980) — thanks @lordavadon2)
|
||||
|
||||
|
||||
@@ -77,6 +77,9 @@ export class OpencodeExecutor extends BaseExecutor {
|
||||
}
|
||||
|
||||
// Forward OpenCode request metadata headers from client
|
||||
const findClientHeader = (name: string) =>
|
||||
Object.entries(clientHeaders).find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1];
|
||||
|
||||
const opencodeHeaderKeys = [
|
||||
"x-opencode-session",
|
||||
"x-opencode-request",
|
||||
@@ -84,13 +87,27 @@ export class OpencodeExecutor extends BaseExecutor {
|
||||
"x-opencode-client",
|
||||
];
|
||||
for (const headerName of opencodeHeaderKeys) {
|
||||
const value = Object.entries(clientHeaders).find(
|
||||
([key]) => key.toLowerCase() === headerName.toLowerCase()
|
||||
)?.[1];
|
||||
const value = findClientHeader(headerName);
|
||||
if (value) {
|
||||
headers[headerName] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// #4022: OpenCode CLI only emits x-opencode-* headers when the provider id
|
||||
// starts with "opencode". For a custom-named provider (e.g. "omniroute") it
|
||||
// instead sends x-session-affinity / X-Session-Id, which both carry the same
|
||||
// OpenCode sessionID. Map that session id onto x-opencode-session so session
|
||||
// continuity to the opencode.ai upstream works regardless of how the user
|
||||
// named the provider. Scoped to this executor (opencode.ai/zen upstreams
|
||||
// only) — the generic DefaultExecutor intentionally does NOT do this, to
|
||||
// avoid leaking the client session id to arbitrary third-party upstreams.
|
||||
if (!headers["x-opencode-session"]) {
|
||||
const sessionAffinity =
|
||||
findClientHeader("x-session-affinity") || findClientHeader("x-session-id");
|
||||
if (sessionAffinity) {
|
||||
headers["x-opencode-session"] = sessionAffinity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void model;
|
||||
|
||||
@@ -410,6 +410,57 @@ describe("OpencodeExecutor", () => {
|
||||
assert.equal(headers["Authorization"], undefined);
|
||||
});
|
||||
});
|
||||
|
||||
// #4022: OpenCode CLI only emits x-opencode-* when the provider id starts with
|
||||
// "opencode". For a custom-named provider (e.g. "omniroute") it instead sends
|
||||
// x-session-affinity / X-Session-Id (both carry the same OpenCode sessionID).
|
||||
// The executor must map that session id onto x-opencode-session so session
|
||||
// continuity to the opencode.ai upstream works regardless of provider name.
|
||||
describe("opencode session-affinity fallback (#4022)", () => {
|
||||
it("maps x-session-affinity to x-opencode-session when no direct x-opencode-session", () => {
|
||||
const headers = zenExecutor.buildHeaders({ apiKey: "test-key" }, true, {
|
||||
"x-session-affinity": "sess-aff",
|
||||
});
|
||||
assert.equal(headers["x-opencode-session"], "sess-aff");
|
||||
});
|
||||
|
||||
it("maps X-Session-Id to x-opencode-session when no direct x-opencode-session", () => {
|
||||
const headers = zenExecutor.buildHeaders({ apiKey: "test-key" }, true, {
|
||||
"X-Session-Id": "sess-id",
|
||||
});
|
||||
assert.equal(headers["x-opencode-session"], "sess-id");
|
||||
});
|
||||
|
||||
it("prefers a direct x-opencode-session over x-session-affinity (regression guard)", () => {
|
||||
const headers = zenExecutor.buildHeaders({ apiKey: "test-key" }, true, {
|
||||
"x-opencode-session": "direct",
|
||||
"x-session-affinity": "affinity",
|
||||
"X-Session-Id": "session-id",
|
||||
});
|
||||
assert.equal(headers["x-opencode-session"], "direct");
|
||||
});
|
||||
|
||||
it("does not set x-opencode-session when neither direct nor affinity is present", () => {
|
||||
const headers = zenExecutor.buildHeaders({ apiKey: "test-key" }, true, {
|
||||
"some-other-header": "val",
|
||||
});
|
||||
assert.equal(headers["x-opencode-session"], undefined);
|
||||
});
|
||||
|
||||
it("matches session-affinity headers case-insensitively", () => {
|
||||
const headers = zenExecutor.buildHeaders({ apiKey: "test-key" }, true, {
|
||||
"X-Session-Affinity": "sess-ci",
|
||||
});
|
||||
assert.equal(headers["x-opencode-session"], "sess-ci");
|
||||
});
|
||||
|
||||
it("opencode-go executor also maps session-affinity to x-opencode-session", () => {
|
||||
const headers = goExecutor.buildHeaders({ apiKey: "test-key" }, true, {
|
||||
"x-session-affinity": "sess-go-aff",
|
||||
});
|
||||
assert.equal(headers["x-opencode-session"], "sess-go-aff");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("DefaultExecutor", () => {
|
||||
|
||||
Reference in New Issue
Block a user