mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
fix(codex): convert chat json schema to responses text format (#5933)
Integrated into release/v3.8.44 — converts Chat Completions json_schema response_format → Responses API text.format on the Codex path, and preserves existing text.format through verbosity normalization. Base redirected main→release; the openai-responses.ts split that landed this cycle was reconciled by re-applying the delta onto openai-responses/toResponses.ts. Validated locally: 48 translator-openai-responses-req + 8 codex-verbosity tests green. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
This commit is contained in:
@@ -9,9 +9,8 @@
|
||||
*
|
||||
* This helper runs on the translated path (before the allowlist) and folds whichever
|
||||
* shape arrived into a single, validated `text:{verbosity}`. `text` is then added to
|
||||
* the allowlist so the hint survives. Non-verbosity `text` keys (e.g. a stray
|
||||
* `text.format`) are intentionally dropped — they were already stripped by the
|
||||
* pre-existing allowlist, so this preserves the status quo while adding verbosity.
|
||||
* the allowlist so the hint survives. Other Responses `text` keys, such as
|
||||
* `text.format` for structured output, are preserved.
|
||||
*
|
||||
* Ref: OpenAI GPT-5 docs (text.verbosity), Azure Foundry reasoning guide.
|
||||
*/
|
||||
@@ -33,8 +32,8 @@ function normalizeLevel(value: unknown): string | undefined {
|
||||
/**
|
||||
* Mutates `body` in place: resolves verbosity from `text.verbosity` (Responses) or
|
||||
* the top-level `verbosity` (Chat Completions, which takes precedence when both are
|
||||
* present), drops the Chat-only top-level field, and collapses `text` to
|
||||
* `{verbosity}` when valid or removes it otherwise.
|
||||
* present), drops the Chat-only top-level field, and preserves any other existing
|
||||
* Responses `text` configuration.
|
||||
*/
|
||||
export function normalizeCodexVerbosity(body: Record<string, unknown>): void {
|
||||
const textRecord = asRecord(body.text);
|
||||
@@ -45,8 +44,15 @@ export function normalizeCodexVerbosity(body: Record<string, unknown>): void {
|
||||
|
||||
delete body.verbosity;
|
||||
|
||||
const nextText = textRecord ? { ...textRecord } : {};
|
||||
if (verbosity) {
|
||||
body.text = { verbosity };
|
||||
nextText.verbosity = verbosity;
|
||||
} else {
|
||||
delete nextText.verbosity;
|
||||
}
|
||||
|
||||
if (Object.keys(nextText).length > 0) {
|
||||
body.text = nextText;
|
||||
} else {
|
||||
delete body.text;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,28 @@ import {
|
||||
normalizeResponsesReasoningEffort,
|
||||
} from "./helpers.ts";
|
||||
|
||||
// Chat Completions `response_format: { type: "json_schema" }` → Responses API `text.format`.
|
||||
// Merges into any existing `result.text` (e.g. verbosity) so structured-output schemas from
|
||||
// Chat clients survive the translation to the Responses/Codex upstream (#5933).
|
||||
function mapChatResponseFormatToResponsesText(body: JsonRecord, result: JsonRecord): void {
|
||||
const responseFormat = toRecord(body.response_format);
|
||||
if (responseFormat.type !== "json_schema") return;
|
||||
|
||||
const jsonSchema = toRecord(responseFormat.json_schema);
|
||||
if (jsonSchema.schema === undefined) return;
|
||||
|
||||
const existingText = toRecord(result.text);
|
||||
const format: JsonRecord = {
|
||||
type: "json_schema",
|
||||
name: toString(jsonSchema.name, "codex_output_schema"),
|
||||
schema: jsonSchema.schema,
|
||||
};
|
||||
if (jsonSchema.description !== undefined) format.description = jsonSchema.description;
|
||||
if (jsonSchema.strict !== undefined) format.strict = jsonSchema.strict;
|
||||
|
||||
result.text = { ...existingText, format };
|
||||
}
|
||||
|
||||
export function openaiToOpenAIResponsesRequest(
|
||||
model: unknown,
|
||||
body: unknown,
|
||||
@@ -301,6 +323,7 @@ export function openaiToOpenAIResponsesRequest(
|
||||
result.max_output_tokens = root.max_tokens;
|
||||
}
|
||||
if (root.top_p !== undefined) result.top_p = root.top_p;
|
||||
mapChatResponseFormatToResponsesText(root, result);
|
||||
// GPT-5 verbosity: Chat Completions `verbosity` → Responses `text.verbosity`.
|
||||
const chatVerbosity = normalizeVerbosity(root.verbosity);
|
||||
if (chatVerbosity) {
|
||||
|
||||
@@ -37,10 +37,19 @@ test("invalid verbosity is dropped and text removed", () => {
|
||||
assert.equal(body.verbosity, undefined);
|
||||
});
|
||||
|
||||
test("no verbosity + stray non-verbosity text → text removed (status quo)", () => {
|
||||
const body: Record<string, unknown> = { text: { format: { type: "json" } }, input: [] };
|
||||
test("no verbosity + Responses text.format is preserved", () => {
|
||||
const format = { type: "json_schema", name: "schema", schema: { type: "object" } };
|
||||
const body: Record<string, unknown> = { text: { format }, input: [] };
|
||||
normalizeCodexVerbosity(body);
|
||||
assert.equal(body.text, undefined);
|
||||
assert.deepEqual(body.text, { format });
|
||||
});
|
||||
|
||||
test("verbosity is merged with existing Responses text.format", () => {
|
||||
const format = { type: "json_schema", name: "schema", schema: { type: "object" } };
|
||||
const body: Record<string, unknown> = { verbosity: "low", text: { format }, input: [] };
|
||||
normalizeCodexVerbosity(body);
|
||||
assert.deepEqual(body.text, { format, verbosity: "low" });
|
||||
assert.equal(body.verbosity, undefined);
|
||||
});
|
||||
|
||||
test("verbosity is normalized case-insensitively", () => {
|
||||
|
||||
@@ -393,6 +393,81 @@ test("Chat -> Responses converts messages, tool calls, tool outputs, tools and p
|
||||
assert.equal((result as any).top_p, 0.9);
|
||||
});
|
||||
|
||||
test("Chat -> Responses converts json_schema response_format to text.format", () => {
|
||||
const schema = {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: { answer: { type: "string" } },
|
||||
required: ["answer"],
|
||||
};
|
||||
|
||||
const result = openaiToOpenAIResponsesRequest(
|
||||
"gpt-5.2-codex",
|
||||
{
|
||||
messages: [{ role: "user", content: "Return the answer as JSON" }],
|
||||
response_format: {
|
||||
type: "json_schema",
|
||||
json_schema: {
|
||||
name: "answer_schema",
|
||||
description: "A structured answer",
|
||||
strict: true,
|
||||
schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
null
|
||||
) as any;
|
||||
|
||||
assert.deepEqual(result.text, {
|
||||
format: {
|
||||
type: "json_schema",
|
||||
name: "answer_schema",
|
||||
description: "A structured answer",
|
||||
strict: true,
|
||||
schema,
|
||||
},
|
||||
});
|
||||
assert.equal(result.response_format, undefined);
|
||||
});
|
||||
|
||||
test("Chat -> Responses uses response_format over nonstandard chat text.format", () => {
|
||||
const result = openaiToOpenAIResponsesRequest(
|
||||
"gpt-5.2-codex",
|
||||
{
|
||||
messages: [{ role: "user", content: "Return JSON" }],
|
||||
text: {
|
||||
format: { type: "json_schema", name: "nonstandard", schema: { type: "object" } },
|
||||
verbosity: "low",
|
||||
},
|
||||
response_format: {
|
||||
type: "json_schema",
|
||||
json_schema: { name: "chat", schema: { type: "object", properties: {} } },
|
||||
},
|
||||
},
|
||||
false,
|
||||
null
|
||||
) as any;
|
||||
|
||||
assert.deepEqual(result.text, {
|
||||
format: { type: "json_schema", name: "chat", schema: { type: "object", properties: {} } },
|
||||
});
|
||||
});
|
||||
|
||||
test("Chat -> Responses ignores nonstandard chat text.format without response_format", () => {
|
||||
const result = openaiToOpenAIResponsesRequest(
|
||||
"gpt-5.2-codex",
|
||||
{
|
||||
messages: [{ role: "user", content: "Return JSON" }],
|
||||
text: { format: { type: "json_schema", name: "nonstandard", schema: { type: "object" } } },
|
||||
},
|
||||
false,
|
||||
null
|
||||
) as any;
|
||||
|
||||
assert.equal(result.text, undefined);
|
||||
});
|
||||
|
||||
test("Responses round-trip preserves store and previous_response_id when opt-in is enabled", () => {
|
||||
const credentials = {
|
||||
providerSpecificData: {
|
||||
|
||||
Reference in New Issue
Block a user