mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-21 22:52:19 +03:00
This commit is contained in:
committed by
GitHub
parent
92afce6d09
commit
b4dbf7b235
@@ -30,6 +30,7 @@ _In development — bullets added per PR; finalized at release._
|
||||
- **fix(pricing): align Claude Code (`cc`) pricing with current Anthropic per-MTok rates** — the `cc` provider block in the default pricing table had stale numbers across every Claude 4.x family entry — most visibly, `claude-opus-4-5-20251101` was billed at the deprecated Opus 4.1 rate (`input $15` / `output $75`), and `claude-haiku-4-5-20251001` was at half the current Haiku 4.5 rate. The `cached` (cache hit) and `cache_creation` (5-minute cache write) multipliers were also off across Opus 4.6/4.7/4.8, Sonnet 4.5/4.6, Haiku 4.5, and Fable 5. All eight entries now match the rates Anthropic publishes (input, 5m cache write at 1.25x input, cache hit at 0.1x input, output; reasoning billed at the output rate), so cost accounting on the dashboard and per-request usage events stop under- or over-reporting Claude Code spend. (thanks @chulanpro5)
|
||||
- **fix(executors): sanitize Anthropic-shape content parts before GitHub Copilot `/chat/completions`** — Claude models on GitHub Copilot driven from clients like Cursor IDE (e.g. `gh/claude-sonnet-4.6`) failed with `Provider returned error: type has to be either 'image_url' or 'text' (reset after 30s)` because the client passed through Anthropic-shape content parts (`tool_use`, `tool_result`, `thinking`) untouched, and the Copilot chat-completions endpoint only accepts `text`/`image_url`. `GithubExecutor.transformRequest` now serializes any unsupported part type as `text` (preserving the model's context), drops empty parts, and collapses to `null` when an assistant message's only content was tool_calls — `tool_calls` ride alongside untouched. Codex-family models still route through `/responses` unchanged. (thanks @cngznNN)
|
||||
- **fix(sse):** refactor stall detection to reduce false positives on slow but progressing streams. (thanks @zakirkun)
|
||||
- **fix(executors): synthesize `x-opencode-request` for custom-named OpenCode providers** — the OpenCode CLI only emits the `x-opencode-*` header set when the provider id starts with `opencode`; a custom-named provider (e.g. `omniroute`) instead sends `x-session-affinity` / `x-session-id` (mapped to `x-opencode-session` since #4022) but no request-correlation id, so `x-opencode-request` was silently dropped. `OpencodeExecutor` now synthesizes a fresh `x-opencode-request` on that session-affinity fallback path so custom-named providers are not disadvantaged on the opencode.ai upstream. `x-opencode-client` / `x-opencode-project` are intentionally **not** fabricated (no valid client source — an invented value risks upstream rejection) and remain forward-only; `DefaultExecutor` is untouched. ([#4465](https://github.com/diegosouzapw/OmniRoute/issues/4465) — thanks @pizzav-xyz)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import {
|
||||
BaseExecutor,
|
||||
setUserAgentHeader,
|
||||
@@ -106,6 +107,20 @@ export class OpencodeExecutor extends BaseExecutor {
|
||||
findClientHeader("x-session-affinity") || findClientHeader("x-session-id");
|
||||
if (sessionAffinity) {
|
||||
headers["x-opencode-session"] = sessionAffinity;
|
||||
|
||||
// #4465: a custom-named provider only reaches this fallback because the
|
||||
// OpenCode CLI did NOT emit the x-opencode-* set (it only does so when the
|
||||
// provider id starts with "opencode"). It therefore also dropped
|
||||
// x-opencode-request, a per-request correlation id. Synthesize one so these
|
||||
// users are not disadvantaged versus opencode-prefixed providers on the
|
||||
// opencode.ai upstream. x-opencode-client / x-opencode-project are NOT
|
||||
// fabricated: their valid values are opencode-internal and inventing them
|
||||
// could be rejected upstream — they remain forward-only above. Scoped to this
|
||||
// executor (opencode.ai/zen) and only to the fallback path, so the direct
|
||||
// OpenCode CLI flow (which controls its own request id) is untouched.
|
||||
if (!headers["x-opencode-request"]) {
|
||||
headers["x-opencode-request"] = randomUUID();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,6 +461,71 @@ describe("OpencodeExecutor", () => {
|
||||
assert.equal(headers["x-opencode-session"], "sess-go-aff");
|
||||
});
|
||||
});
|
||||
|
||||
// #4465: custom-named providers reach the session-affinity fallback above, but the
|
||||
// OpenCode CLI never emits x-opencode-request for them (it only emits x-opencode-*
|
||||
// when the provider id starts with "opencode"). Synthesize a request correlation id
|
||||
// so these users are not disadvantaged on the opencode.ai upstream. x-opencode-client
|
||||
// / x-opencode-project are NOT fabricated: their valid values are opencode-internal
|
||||
// and inventing them risks upstream rejection — they stay forward-only.
|
||||
describe("opencode request-id synthesis for custom-named providers (#4465)", () => {
|
||||
it("synthesizes x-opencode-request when only session-affinity is present", () => {
|
||||
const headers = zenExecutor.buildHeaders({ apiKey: "test-key" }, true, {
|
||||
"x-session-affinity": "sess-aff",
|
||||
});
|
||||
assert.equal(headers["x-opencode-session"], "sess-aff");
|
||||
assert.ok(
|
||||
typeof headers["x-opencode-request"] === "string" &&
|
||||
headers["x-opencode-request"].length > 0,
|
||||
"expected a synthesized x-opencode-request id"
|
||||
);
|
||||
});
|
||||
|
||||
it("synthesizes a unique x-opencode-request per call", () => {
|
||||
const a = zenExecutor.buildHeaders({ apiKey: "k" }, true, {
|
||||
"x-session-affinity": "sess-aff",
|
||||
});
|
||||
const b = zenExecutor.buildHeaders({ apiKey: "k" }, true, {
|
||||
"x-session-affinity": "sess-aff",
|
||||
});
|
||||
assert.notEqual(a["x-opencode-request"], b["x-opencode-request"]);
|
||||
});
|
||||
|
||||
it("prefers a client-sent x-opencode-request over the synthesized one", () => {
|
||||
const headers = zenExecutor.buildHeaders({ apiKey: "test-key" }, true, {
|
||||
"x-session-affinity": "sess-aff",
|
||||
"x-opencode-request": "req-real",
|
||||
});
|
||||
assert.equal(headers["x-opencode-request"], "req-real");
|
||||
});
|
||||
|
||||
it("does not fabricate x-opencode-client / x-opencode-project (no client source)", () => {
|
||||
const headers = zenExecutor.buildHeaders({ apiKey: "test-key" }, true, {
|
||||
"x-session-affinity": "sess-aff",
|
||||
});
|
||||
assert.equal(headers["x-opencode-client"], undefined);
|
||||
assert.equal(headers["x-opencode-project"], undefined);
|
||||
});
|
||||
|
||||
it("does not synthesize x-opencode-request on the direct opencode-session path", () => {
|
||||
// opencode CLI (provider id starts with "opencode") sends its own x-opencode-*
|
||||
// set; we must not override/inject when it controls the request id itself.
|
||||
const headers = zenExecutor.buildHeaders({ apiKey: "test-key" }, true, {
|
||||
"x-opencode-session": "direct",
|
||||
});
|
||||
assert.equal(headers["x-opencode-request"], undefined);
|
||||
});
|
||||
|
||||
it("opencode-go executor also synthesizes x-opencode-request on the fallback path", () => {
|
||||
const headers = goExecutor.buildHeaders({ apiKey: "test-key" }, true, {
|
||||
"x-session-affinity": "sess-go-aff",
|
||||
});
|
||||
assert.ok(
|
||||
typeof headers["x-opencode-request"] === "string" &&
|
||||
headers["x-opencode-request"].length > 0
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("DefaultExecutor", () => {
|
||||
|
||||
Reference in New Issue
Block a user