Files
OmniRoute/tests/unit/codex-effort-model-echo-3697.test.ts
Diego Rodrigues de Sa e Souza 1045e57aa1 feat(codex): echo requested effort-suffixed model id in Responses payloads (#3697) (#6820)
* feat(codex): echo requested effort-suffixed model id in Responses payloads (#3697)

Codex CLI compatibility shim: the Responses API response.created/
response.in_progress/response.completed payloads now carry a `model`
field (previously absent), and for Codex-CLI-originated requests it
echoes the client-requested effort-suffixed model id (e.g.
gpt-5.5-xhigh) instead of the bare upstream id (gpt-5.5), so the Codex
CLI status line/model button shows the active reasoning effort.

- openai-responses.ts translator threads the upstream model into the
  Responses event objects (additive, omitted when unknown).
- New isCodexOriginatedHeaders() (codexIdentity.ts) reuses PR #3481's
  originator/User-Agent detection, header-based so it still fires when
  a combo routes codex/gpt-5.5-xhigh to a non-codex upstream.
- chatCore's existing opt-in #1311 echoModel pipeline now also fires
  automatically for Codex clients on the Responses API, regardless of
  the echoRequestedModelName setting.
- responseModelEcho.ts now also rewrites the nested response.model
  field the Responses API uses (previously only top-level model).
- /v1/models keeps returning models: [] for Codex (unchanged, #3481).

Regression guard: tests/unit/codex-effort-model-echo-3697.test.ts.

Closes #3697

* chore(merge): re-sync with release/v3.8.47 (restore CHANGELOG, keep own bullet)

* chore(merge): re-sync with release/v3.8.47; move changelog bullet to changelog.d fragment (merge-storm proof)
2026-07-11 07:39:09 -03:00

131 lines
5.3 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import { isCodexOriginatedHeaders } from "../../open-sse/config/codexIdentity.ts";
import {
echoModelInObject,
echoModelInSseLine,
} from "../../open-sse/services/responseModelEcho.ts";
const { openaiToOpenAIResponsesResponse } = await import(
"../../open-sse/translator/response/openai-responses.ts"
);
const { initState } = await import("../../open-sse/translator/index.ts");
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
// #3697: Codex CLI compatibility shim — echo the client-requested (effort-suffixed) model
// id (e.g. `gpt-5.5-xhigh`) in Responses API payloads instead of the bare upstream id
// (`gpt-5.5`), so the Codex CLI status line/model button shows the active effort.
function collectResponsesEvents(chunks: Array<Record<string, unknown> | null>) {
const state = initState(FORMATS.OPENAI_RESPONSES) as Record<string, unknown>;
const events: Array<{ event: string; data: Record<string, unknown> }> = [];
for (const chunk of chunks) {
const result = openaiToOpenAIResponsesResponse(chunk as never, state as never);
if (result) events.push(...(result as never));
}
return events;
}
test("isCodexOriginatedHeaders detects Codex CLI via originator header (Headers instance)", () => {
const headers = new Headers({ originator: "codex_cli_rs" });
assert.equal(isCodexOriginatedHeaders(headers), true);
});
test("isCodexOriginatedHeaders detects Codex CLI via User-Agent (plain object, case-insensitive)", () => {
assert.equal(isCodexOriginatedHeaders({ "User-Agent": "codex_cli_rs/0.136.0" }), true);
});
test("isCodexOriginatedHeaders returns false for non-Codex clients", () => {
assert.equal(isCodexOriginatedHeaders(new Headers({ "user-agent": "curl/8.0" })), false);
assert.equal(isCodexOriginatedHeaders({}), false);
assert.equal(isCodexOriginatedHeaders(null), false);
});
test("OpenAI -> Responses translator carries the upstream model into response.created/completed", () => {
const events = collectResponsesEvents([
{
id: "chatcmpl-1",
model: "gpt-5.5",
choices: [{ index: 0, delta: { content: "hi" }, finish_reason: null }],
},
{
id: "chatcmpl-1",
model: "gpt-5.5",
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
},
null, // flush -> response.completed
]);
const created = events.find((e) => e.event === "response.created");
const completed = events.find((e) => e.event === "response.completed");
assert.equal((created!.data.response as Record<string, unknown>).model, "gpt-5.5");
assert.equal((completed!.data.response as Record<string, unknown>).model, "gpt-5.5");
});
test("OpenAI -> Responses translator omits model when the upstream never sent one (no regression)", () => {
const events = collectResponsesEvents([
{
id: "chatcmpl-1",
choices: [{ index: 0, delta: { content: "hi" }, finish_reason: null }],
},
{
id: "chatcmpl-1",
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
},
null,
]);
const created = events.find((e) => e.event === "response.created");
const completed = events.find((e) => e.event === "response.completed");
assert.equal("model" in (created!.data.response as Record<string, unknown>), false);
assert.equal("model" in (completed!.data.response as Record<string, unknown>), false);
});
test("full shim pipeline: bare upstream model in Responses payloads gets rewritten to the requested effort-suffixed id", () => {
const events = collectResponsesEvents([
{
id: "chatcmpl-1",
model: "gpt-5.5",
choices: [{ index: 0, delta: { content: "hi" }, finish_reason: null }],
},
{
id: "chatcmpl-1",
model: "gpt-5.5",
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
},
null,
]);
const requestedModel = "gpt-5.5-xhigh";
const created = events.find((e) => e.event === "response.created")!.data;
const completed = events.find((e) => e.event === "response.completed")!.data;
// Non-stream / object form (chatCore's non-streaming return path).
echoModelInObject(created, requestedModel);
echoModelInObject(completed, requestedModel);
assert.equal((created.response as Record<string, unknown>).model, requestedModel);
assert.equal((completed.response as Record<string, unknown>).model, requestedModel);
// Streaming SSE-line form (chatCore's createModelEchoTransform pipe stage).
const sseLine = `data: ${JSON.stringify({
type: "response.completed",
response: { id: "resp_1", object: "response", model: "gpt-5.5", status: "completed" },
})}`;
const rewritten = echoModelInSseLine(sseLine, requestedModel);
assert.ok(rewritten.includes(`"model":"${requestedModel}"`), rewritten);
assert.ok(!rewritten.includes('"model":"gpt-5.5"'), rewritten);
});
test("echoModelInObject/echoModelInSseLine leave non-Responses shapes governed by the existing top-level rule (no regression)", () => {
const chatCompletionChunk = { id: "x", model: "gpt-5.5", choices: [] };
echoModelInObject(chatCompletionChunk, "claude-sonnet-cx");
assert.equal(chatCompletionChunk.model, "claude-sonnet-cx");
const line = echoModelInSseLine(
'data: {"id":"1","model":"gpt-5.5","choices":[]}',
"claude-sonnet-cx"
);
assert.equal(line, 'data: {"id":"1","model":"claude-sonnet-cx","choices":[]}');
});