fix(api): don't force combo names to codex/ on /v1/responses (#3227, #3233) (#3244)

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:
Diego Rodrigues de Sa e Souza
2026-06-05 14:16:10 -03:00
committed by GitHub
parent b413774bdf
commit cf7f684bd8
3 changed files with 66 additions and 2 deletions

View File

@@ -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") {

View File

@@ -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, {