mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-08 08:12:20 +03:00
Compare commits
1 Commits
fix/9168-s
...
fix/codex-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc5d407ef3 |
1
changelog.d/fixes/9161-codex-responses-chat-targets.md
Normal file
1
changelog.d/fixes/9161-codex-responses-chat-targets.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(translator):** Honor configured Chat targets for Responses-shaped clients while preserving native Responses providers and outbound token fields ([#9161](https://github.com/diegosouzapw/OmniRoute/pull/9161)) — thanks @Zartharas
|
||||
@@ -6,8 +6,8 @@ export function sanitizeChatRequestBody(
|
||||
sourceFormat: string,
|
||||
targetFormat: string
|
||||
): Record<string, unknown> {
|
||||
const prefersResponsesTokenField =
|
||||
sourceFormat === FORMATS.OPENAI_RESPONSES || targetFormat === FORMATS.OPENAI_RESPONSES;
|
||||
void sourceFormat;
|
||||
const prefersResponsesTokenField = targetFormat === FORMATS.OPENAI_RESPONSES;
|
||||
|
||||
if (prefersResponsesTokenField) {
|
||||
if (body.max_output_tokens === undefined) {
|
||||
|
||||
@@ -2,56 +2,31 @@
|
||||
* chatCore wire target-format resolver (Quality Gate v2 / Fase 9 — chatCore god-file
|
||||
* decomposition, #3501).
|
||||
*
|
||||
* Pure resolution of the provider alias + the upstream target format used to translate the request:
|
||||
* apiFormat==="responses" forces OpenAI Responses; otherwise the model's registry target format, then
|
||||
* the per-model custom override (#2905), then AgentRouter's matching inbound protocol when the
|
||||
* connection has no explicit override, then the provider default. Returns both `alias` (reused by
|
||||
* the handler when stripping the `alias/` prefix off the upstream model id) and `targetFormat`.
|
||||
* Side-effect-free; sits alongside the other request-setup resolvers
|
||||
* (resolveChatCoreRequestSetup / resolveChatCoreRequestFormat).
|
||||
* Pure resolution of the provider alias + the upstream target format used to translate the request.
|
||||
* The inbound API shape does not determine the outbound protocol: the model registry target format,
|
||||
* per-model custom override (#2905), and provider configuration take precedence. This lets a
|
||||
* Responses-shaped client request be translated to a Chat Completions-compatible upstream while
|
||||
* preserving native Responses providers configured with an OpenAI Responses target.
|
||||
* Returns both `alias` (reused by the handler when stripping the `alias/` prefix off the upstream
|
||||
* model id) and `targetFormat`.
|
||||
*/
|
||||
|
||||
import { PROVIDER_ID_TO_ALIAS, getModelTargetFormat } from "../../config/providerModels.ts";
|
||||
import { getTargetFormat } from "../../services/provider.ts";
|
||||
import { FORMATS } from "../../translator/formats.ts";
|
||||
|
||||
export function resolveChatCoreTargetFormat(opts: {
|
||||
provider: string;
|
||||
resolvedModel: string;
|
||||
apiFormat: string | undefined;
|
||||
sourceFormat?: string;
|
||||
customModelTargetFormat: string | undefined;
|
||||
providerSpecificData: unknown;
|
||||
}) {
|
||||
const {
|
||||
provider,
|
||||
resolvedModel,
|
||||
apiFormat,
|
||||
sourceFormat,
|
||||
customModelTargetFormat,
|
||||
providerSpecificData,
|
||||
} = opts;
|
||||
const { provider, resolvedModel, customModelTargetFormat, providerSpecificData } = opts;
|
||||
const alias = PROVIDER_ID_TO_ALIAS[provider] || provider;
|
||||
const modelTargetFormat = getModelTargetFormat(alias, resolvedModel);
|
||||
const explicitConnectionTargetFormat = (
|
||||
providerSpecificData as { targetFormat?: unknown } | null | undefined
|
||||
)?.targetFormat;
|
||||
const inferredAgentRouterTargetFormat =
|
||||
provider === "agentrouter" &&
|
||||
!(typeof explicitConnectionTargetFormat === "string" && explicitConnectionTargetFormat) &&
|
||||
(sourceFormat === FORMATS.OPENAI_RESPONSES ||
|
||||
sourceFormat === FORMATS.OPENAI ||
|
||||
sourceFormat === FORMATS.CLAUDE)
|
||||
? sourceFormat
|
||||
: undefined;
|
||||
const targetFormat =
|
||||
apiFormat === "responses"
|
||||
? FORMATS.OPENAI_RESPONSES
|
||||
: modelTargetFormat ||
|
||||
customModelTargetFormat ||
|
||||
inferredAgentRouterTargetFormat ||
|
||||
getTargetFormat(provider, providerSpecificData);
|
||||
modelTargetFormat || customModelTargetFormat || getTargetFormat(provider, providerSpecificData);
|
||||
return { alias, targetFormat };
|
||||
}
|
||||
|
||||
export type ChatCoreTargetFormat = ReturnType<typeof resolveChatCoreTargetFormat>;
|
||||
export type ChatCoreTargetFormat = ReturnType<typeof resolveChatCoreTargetFormat>;
|
||||
@@ -18,8 +18,12 @@ import {
|
||||
import { FORMATS } from "../../open-sse/translator/formats.ts";
|
||||
import { saveIdempotency } from "../../src/lib/idempotencyLayer.ts";
|
||||
|
||||
test("sanitizeChatRequestBody: Chat Completions target maps max_output_tokens → max_tokens", () => {
|
||||
const out = sanitizeChatRequestBody({ max_output_tokens: 256 }, FORMATS.OPENAI, FORMATS.OPENAI);
|
||||
test("sanitizeChatRequestBody: Responses source targeting Chat maps max_output_tokens → max_tokens", () => {
|
||||
const out = sanitizeChatRequestBody(
|
||||
{ max_output_tokens: 256 },
|
||||
FORMATS.OPENAI_RESPONSES,
|
||||
FORMATS.OPENAI
|
||||
);
|
||||
assert.equal(out.max_tokens, 256);
|
||||
assert.equal(out.max_output_tokens, undefined);
|
||||
});
|
||||
@@ -35,7 +39,11 @@ test("sanitizeChatRequestBody: Responses target maps max_completion_tokens → m
|
||||
});
|
||||
|
||||
test("sanitizeChatRequestBody: Responses target maps max_tokens → max_output_tokens", () => {
|
||||
const out = sanitizeChatRequestBody({ max_tokens: 128 }, FORMATS.OPENAI_RESPONSES, FORMATS.OPENAI);
|
||||
const out = sanitizeChatRequestBody(
|
||||
{ max_tokens: 128 },
|
||||
FORMATS.OPENAI,
|
||||
FORMATS.OPENAI_RESPONSES
|
||||
);
|
||||
assert.equal(out.max_output_tokens, 128);
|
||||
assert.equal(out.max_tokens, undefined);
|
||||
});
|
||||
|
||||
@@ -1,72 +1,85 @@
|
||||
// tests/unit/chatcore-target-format.test.ts
|
||||
// Characterization of resolveChatCoreTargetFormat — the wire target-format resolution extracted
|
||||
// from handleChatCore (chatCore god-file decomposition, #3501). Resolves the provider alias and the
|
||||
// upstream target format: apiFormat==="responses" forces OpenAI Responses; otherwise the model's
|
||||
// registry target format, then the custom-model override, then the provider default. Returns both
|
||||
// `alias` (reused downstream when stripping the alias/ prefix off the upstream model) and
|
||||
// `targetFormat`.
|
||||
// from handleChatCore (chatCore god-file decomposition, #3501). The inbound client API shape is
|
||||
// independent from the outbound provider protocol: model registry metadata, custom-model overrides,
|
||||
// and provider configuration determine the upstream target format.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { resolveChatCoreTargetFormat } from "../../open-sse/handlers/chatCore/targetFormat.ts";
|
||||
import {
|
||||
PROVIDER_ID_TO_ALIAS,
|
||||
getModelTargetFormat,
|
||||
} from "../../open-sse/config/providerModels.ts";
|
||||
import { PROVIDER_ID_TO_ALIAS, getModelTargetFormat } from "../../open-sse/config/providerModels.ts";
|
||||
import { getTargetFormat } from "../../open-sse/services/provider.ts";
|
||||
import { FORMATS } from "../../open-sse/translator/formats.ts";
|
||||
|
||||
function expected(
|
||||
provider: string,
|
||||
resolvedModel: string,
|
||||
apiFormat: string | undefined,
|
||||
customModelTargetFormat: string | undefined,
|
||||
providerSpecificData: unknown
|
||||
) {
|
||||
const alias = PROVIDER_ID_TO_ALIAS[provider] || provider;
|
||||
const modelTargetFormat = getModelTargetFormat(alias, resolvedModel);
|
||||
const targetFormat =
|
||||
apiFormat === "responses"
|
||||
? FORMATS.OPENAI_RESPONSES
|
||||
: modelTargetFormat ||
|
||||
customModelTargetFormat ||
|
||||
getTargetFormat(provider, providerSpecificData);
|
||||
modelTargetFormat || customModelTargetFormat || getTargetFormat(provider, providerSpecificData);
|
||||
return { alias, targetFormat };
|
||||
}
|
||||
|
||||
test("apiFormat='responses' short-circuits to OPENAI_RESPONSES (alias still resolved)", () => {
|
||||
test("Responses client honors a Chat-compatible upstream target", () => {
|
||||
const provider = "openai-compatible-chat-regression";
|
||||
const r = resolveChatCoreTargetFormat({
|
||||
provider,
|
||||
resolvedModel: "custom-chat-model",
|
||||
apiFormat: "responses",
|
||||
customModelTargetFormat: undefined,
|
||||
providerSpecificData: { apiType: "chat" },
|
||||
});
|
||||
assert.equal(r.targetFormat, FORMATS.OPENAI);
|
||||
assert.equal(r.alias, provider);
|
||||
});
|
||||
|
||||
test("Responses client preserves a provider configured for native Responses", () => {
|
||||
const provider = "openai-compatible-responses-regression";
|
||||
const r = resolveChatCoreTargetFormat({
|
||||
provider,
|
||||
resolvedModel: "custom-responses-model",
|
||||
apiFormat: "responses",
|
||||
customModelTargetFormat: undefined,
|
||||
providerSpecificData: { apiType: "responses" },
|
||||
});
|
||||
assert.equal(r.targetFormat, FORMATS.OPENAI_RESPONSES);
|
||||
});
|
||||
|
||||
test("model registry native Responses target overrides a Chat provider default", () => {
|
||||
const model = "gpt-5.6-sol";
|
||||
assert.equal(getModelTargetFormat("openai", model), FORMATS.OPENAI_RESPONSES);
|
||||
|
||||
const r = resolveChatCoreTargetFormat({
|
||||
provider: "openai",
|
||||
resolvedModel: "gpt-4o",
|
||||
resolvedModel: model,
|
||||
apiFormat: "responses",
|
||||
sourceFormat: FORMATS.OPENAI,
|
||||
customModelTargetFormat: undefined,
|
||||
providerSpecificData: undefined,
|
||||
});
|
||||
assert.equal(r.targetFormat, FORMATS.OPENAI_RESPONSES);
|
||||
assert.equal(r.alias, PROVIDER_ID_TO_ALIAS["openai"] || "openai");
|
||||
});
|
||||
|
||||
test("delegates byte-identically for a normal model (no apiFormat / no custom override)", () => {
|
||||
test("delegates byte-identically for a normal model without a custom override", () => {
|
||||
const r = resolveChatCoreTargetFormat({
|
||||
provider: "openai",
|
||||
resolvedModel: "gpt-4o",
|
||||
apiFormat: undefined,
|
||||
sourceFormat: FORMATS.OPENAI,
|
||||
customModelTargetFormat: undefined,
|
||||
providerSpecificData: undefined,
|
||||
});
|
||||
assert.deepEqual(r, expected("openai", "gpt-4o", undefined, undefined, undefined));
|
||||
assert.deepEqual(r, expected("openai", "gpt-4o", undefined, undefined));
|
||||
});
|
||||
|
||||
test("customModelTargetFormat is used when the model has no registry target format", () => {
|
||||
const customModel = "totally-unknown-custom-model-xyz";
|
||||
// precondition: the registry has no target format for this unknown model
|
||||
assert.ok(!getModelTargetFormat(PROVIDER_ID_TO_ALIAS["openai"] || "openai", customModel));
|
||||
const r = resolveChatCoreTargetFormat({
|
||||
provider: "openai",
|
||||
resolvedModel: customModel,
|
||||
apiFormat: undefined,
|
||||
sourceFormat: FORMATS.OPENAI,
|
||||
apiFormat: "responses",
|
||||
customModelTargetFormat: "claude",
|
||||
providerSpecificData: undefined,
|
||||
});
|
||||
@@ -76,36 +89,25 @@ test("customModelTargetFormat is used when the model has no registry target form
|
||||
test("falls back to getTargetFormat(provider) when neither model nor custom format apply", () => {
|
||||
const customModel = "totally-unknown-custom-model-xyz";
|
||||
const r = resolveChatCoreTargetFormat({
|
||||
provider: "openai",
|
||||
provider: "openai-compatible-chat-regression",
|
||||
resolvedModel: customModel,
|
||||
apiFormat: undefined,
|
||||
sourceFormat: FORMATS.OPENAI,
|
||||
apiFormat: "responses",
|
||||
customModelTargetFormat: undefined,
|
||||
providerSpecificData: undefined,
|
||||
providerSpecificData: { apiType: "chat" },
|
||||
});
|
||||
assert.equal(r.targetFormat, getTargetFormat("openai", undefined));
|
||||
assert.equal(
|
||||
r.targetFormat,
|
||||
getTargetFormat("openai-compatible-chat-regression", { apiType: "chat" })
|
||||
);
|
||||
});
|
||||
|
||||
test("AgentRouter explicit connection protocol overrides the inferred inbound protocol", () => {
|
||||
const r = resolveChatCoreTargetFormat({
|
||||
provider: "agentrouter",
|
||||
resolvedModel: "gpt-5.6-sol",
|
||||
apiFormat: undefined,
|
||||
sourceFormat: FORMATS.OPENAI_RESPONSES,
|
||||
customModelTargetFormat: undefined,
|
||||
providerSpecificData: { targetFormat: FORMATS.CLAUDE },
|
||||
});
|
||||
assert.equal(r.targetFormat, FORMATS.CLAUDE);
|
||||
});
|
||||
|
||||
test("unmapped provider → alias falls back to the provider id", () => {
|
||||
test("unmapped provider alias falls back to the provider id", () => {
|
||||
const r = resolveChatCoreTargetFormat({
|
||||
provider: "some-unmapped-provider",
|
||||
resolvedModel: "x",
|
||||
apiFormat: "responses",
|
||||
sourceFormat: FORMATS.OPENAI,
|
||||
customModelTargetFormat: undefined,
|
||||
providerSpecificData: undefined,
|
||||
});
|
||||
assert.equal(r.alias, "some-unmapped-provider");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user