mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
The Codex CLI WS->HTTP fallback rewrite (resolveResponsesApiModel) prefixes a bare model id with codex/ whenever codex/<id> resolves to codex. Codex accepts arbitrary model strings, so a combo name with no slash (e.g. n8n-text, paid-premium) was rewritten to codex/<combo> and sent to Codex instead of being resolved as a combo — regressing combos via /v1/responses in v3.8.9+. Skip the rewrite when the bare id is a combo (getComboByName). + unit test.
This commit is contained in:
committed by
GitHub
parent
b413774bdf
commit
cf7f684bd8
@@ -58,17 +58,31 @@ export async function resolveCodexWsModelInfo(
|
||||
*
|
||||
* @param requestedModel the model id from the Responses API request body
|
||||
* @param resolve a getModelInfo-style resolver
|
||||
* @param isCombo optional predicate — when the bare id is a combo name, skip the codex
|
||||
* rewrite so downstream combo routing resolves it (#3227/#3233).
|
||||
* @returns { model, changed } — model is the (possibly rewritten) id;
|
||||
* changed=true means a codex/ prefix was applied.
|
||||
*/
|
||||
export async function resolveResponsesApiModel(
|
||||
requestedModel: string,
|
||||
resolve: ModelResolver
|
||||
resolve: ModelResolver,
|
||||
isCombo?: (name: string) => Promise<boolean> | boolean
|
||||
): Promise<{ model: string; changed: boolean }> {
|
||||
if (!requestedModel || requestedModel.includes("/")) {
|
||||
return { model: requestedModel, changed: false };
|
||||
}
|
||||
|
||||
// #3227/#3233: a bare combo name (e.g. "n8n-text", "paid-premium") must NOT be
|
||||
// force-prefixed to codex/ — Codex accepts arbitrary model strings, so the rewrite
|
||||
// would shadow the combo and route to codex. Let downstream combo routing handle it.
|
||||
if (isCombo) {
|
||||
try {
|
||||
if (await isCombo(requestedModel)) return { model: requestedModel, changed: false };
|
||||
} catch {
|
||||
// combo lookup unavailable — fall through to normal codex-preference resolution
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const resolved = await resolveCodexWsModelInfo(requestedModel, resolve);
|
||||
if (resolved?.provider !== "codex") {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { handleChat } from "@/sse/handlers/chat";
|
||||
import { withEarlyStreamKeepalive } from "@omniroute/open-sse/utils/earlyStreamKeepalive";
|
||||
import { resolveResponsesApiModel } from "@/app/api/internal/codex-responses-ws/modelResolution";
|
||||
import { getModelInfo } from "@/sse/services/model";
|
||||
import { getComboByName } from "@/lib/db/combos";
|
||||
|
||||
// NOTE: We do NOT call initTranslators() here — the translator registry is
|
||||
// bootstrapped at module level inside open-sse/translator/index.ts when it
|
||||
@@ -37,7 +38,11 @@ async function withCodexPreferredModel(request: Request): Promise<Request> {
|
||||
if (!body || typeof body !== "object" || typeof body.model !== "string") {
|
||||
return request;
|
||||
}
|
||||
const { model, changed } = await resolveResponsesApiModel(body.model, getModelInfo);
|
||||
const { model, changed } = await resolveResponsesApiModel(
|
||||
body.model,
|
||||
getModelInfo,
|
||||
async (name) => !!(await getComboByName(name))
|
||||
);
|
||||
if (!changed) return request;
|
||||
|
||||
return new Request(request.url, {
|
||||
|
||||
45
tests/unit/responses-combo-resolution-3227.test.ts
Normal file
45
tests/unit/responses-combo-resolution-3227.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* #3227 / #3233 — combo names broke on /v1/responses in v3.8.9+.
|
||||
*
|
||||
* The Codex CLI WS→HTTP fallback added `resolveResponsesApiModel`, which rewrites a bare
|
||||
* model id to `codex/<id>` whenever `codex/<id>` resolves to the codex provider. Codex
|
||||
* accepts ANY model string, so a *combo* name with no slash (e.g. `n8n-text`,
|
||||
* `paid-premium`) was force-rewritten to `codex/<combo>` and sent to Codex —
|
||||
* "No credentials for provider: codex" / "model not supported" — instead of being
|
||||
* resolved as a combo. The fix skips the codex rewrite when the bare name is a combo.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { resolveResponsesApiModel } from "../../src/app/api/internal/codex-responses-ws/modelResolution.ts";
|
||||
|
||||
// Resolver where bare ids resolve to a non-codex default, but codex accepts anything
|
||||
// (mirrors real getModelInfo: the codex provider passes arbitrary model strings).
|
||||
const resolver = async (modelStr: string) => {
|
||||
if (modelStr.startsWith("codex/")) return { provider: "codex", model: modelStr.slice(6) };
|
||||
if (modelStr === "gpt-5.5") return { provider: "openrouter", model: "gpt-5.5" };
|
||||
return { provider: "openrouter", model: modelStr };
|
||||
};
|
||||
|
||||
test("a bare combo name is NOT rewritten to codex/ (it must resolve as a combo)", async () => {
|
||||
const isCombo = async (name: string) => name === "n8n-text" || name === "paid-premium";
|
||||
|
||||
for (const combo of ["n8n-text", "paid-premium"]) {
|
||||
const out = await resolveResponsesApiModel(combo, resolver, isCombo);
|
||||
assert.equal(out.changed, false, `${combo} must not be codex-prefixed`);
|
||||
assert.equal(out.model, combo);
|
||||
}
|
||||
});
|
||||
|
||||
test("a bare ChatGPT model id is still codex-preferred (Codex CLI WS→HTTP fallback preserved)", async () => {
|
||||
const isCombo = async () => false;
|
||||
const out = await resolveResponsesApiModel("gpt-5.5", resolver, isCombo);
|
||||
assert.equal(out.changed, true);
|
||||
assert.equal(out.model, "codex/gpt-5.5");
|
||||
});
|
||||
|
||||
test("provider-prefixed ids pass through unchanged", async () => {
|
||||
const out = await resolveResponsesApiModel("anthropic/claude-opus-4-8", resolver, async () => false);
|
||||
assert.equal(out.changed, false);
|
||||
assert.equal(out.model, "anthropic/claude-opus-4-8");
|
||||
});
|
||||
Reference in New Issue
Block a user