mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
* fix(providers): route OpenAI responses-only models to /v1/responses (#5842) * docs(changelog): restore #5842 bullet after merge auto-resolve ate it * docs(changelog): keep #5842 bullet additive over release tip
This commit is contained in:
committed by
GitHub
parent
fa2f9e21a8
commit
cbc161bfc3
@@ -14,6 +14,8 @@
|
||||
|
||||
- **fix(usage):** preserve (bounded) tool definitions in request logs even when the request body is truncated, so the request-details view can still show available tools. (thanks @noir017)
|
||||
|
||||
- **fix(providers):** route OpenAI responses-only models to `/v1/responses` instead of 404ing on `/v1/chat/completions`. The curated `gpt-5.5-pro` / `gpt-5.4-pro` entries never worked (OpenAI only serves `*-pro` reasoning models via the Responses API), and "Test all models" surfaced the same 404s. The registry entries now carry `targetFormat: "openai-responses"` (reusing the existing per-model translation plumbing shared with `gh`/`codex`), `DefaultExecutor.buildUrl` swaps the `openai` endpoint to `/responses` in lockstep (honoring custom base URLs), and a `-pro` suffix heuristic covers dynamically-synced ids such as `o1-pro` / `gpt-5.2-pro` (same spirit as the gh executor's `/codex/i` routing, 9router#102). Legacy completions-only ids (e.g. `gpt-3.5-turbo-instruct`) are out of scope — they are not in the catalog and OmniRoute has no legacy `/v1/completions` upstream. Regression guard: `tests/unit/openai-responses-only-models-5842.test.ts` (8). Thanks [@maikokan](https://github.com/maikokan). ([#5842](https://github.com/diegosouzapw/OmniRoute/issues/5842))
|
||||
|
||||
- **fix(image):** keep bare codex image aliases (e.g. `gpt-5.5`) resolving to the codex image pipeline even when a combo shares the same name. A chat combo named `gpt-5.5` used to shadow the bare image alias in `resolveImageRouteModel`, hijacking `/v1/images/*` requests to a chat target (regression path adjacent to [#5887](https://github.com/diegosouzapw/OmniRoute/issues/5887)); codex bare models are now reserved before bare-combo resolution, while non-codex aliases (e.g. `gpt-image-2`) remain user-shadowable (#3214/#3215 behavior preserved). Regression guard: `tests/unit/image-routes-combo-edits-3214-3215.test.ts` (9). ([#5902](https://github.com/diegosouzapw/OmniRoute/pull/5902) by [@KooshaPari](https://github.com/KooshaPari))
|
||||
|
||||
- **fix(ci):** re-green the `release/v3.8.43` fast-gates queue — every PR→release was inheriting base-reds ([#5798](https://github.com/diegosouzapw/OmniRoute/issues/5798)). Five distinct blockers cleared: (1) stale `modelContextOverrides` entry in the `check:db-rules` intentionally-internal allowlist ([#5827](https://github.com/diegosouzapw/OmniRoute/pull/5827) allowlisted it while the [#5609](https://github.com/diegosouzapw/OmniRoute/issues/5609) fix re-exported it from `localDb.ts`; the re-export stays, the obsolete entry goes, classification guard re-pinned to 33); (2) `LIVE_WS_ALLOWED_HOSTS` / `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL` documented in `docs/reference/ENVIRONMENT.md` (env/docs contract, from [#5877](https://github.com/diegosouzapw/OmniRoute/pull/5877)); (3) the Router Backends ADR's references to the not-yet-merged registry ([#5868](https://github.com/diegosouzapw/OmniRoute/pull/5868)) marked as landing-with-PR so `check:fabricated-docs --strict` passes; (4) `antigravity-429-quota-tdd` + `middleware-header-strip-5849` added to stryker `tap.testFiles` (`check:mutation-test-coverage`); (5) file-size / complexity / cognitive-complexity ratchets rebaselined with justification notes — all drift measured identical on the pristine tip and this PR (net-zero). Regression guard: `tests/unit/check-db-rules-classification.test.ts`. ([#5798](https://github.com/diegosouzapw/OmniRoute/issues/5798))
|
||||
|
||||
@@ -49,9 +49,16 @@ export function findModelName(aliasOrId: string, modelId: string): string {
|
||||
|
||||
export function getModelTargetFormat(aliasOrId: string, modelId: string): string | null {
|
||||
const models = PROVIDER_MODELS[aliasOrId];
|
||||
if (!models) return null;
|
||||
const found = models.find((m) => m.id === modelId);
|
||||
return found?.targetFormat || null;
|
||||
const found = models?.find((m) => m.id === modelId);
|
||||
if (found?.targetFormat) return found.targetFormat;
|
||||
// #5842: OpenAI "*-pro" reasoning models (o1-pro, gpt-5.x-pro) are only served by
|
||||
// the native /v1/responses endpoint — /v1/chat/completions 404s ("only supported
|
||||
// in v1/responses"). Curated catalog entries are tagged explicitly; this heuristic
|
||||
// covers dynamically-synced ids that post-date the catalog (same spirit as the gh
|
||||
// executor's /codex/i routing, 9router#102). Scoped to the openai alias so other
|
||||
// providers shipping *-pro ids keep their own endpoint semantics.
|
||||
if (aliasOrId === "openai" && /-pro$/i.test(modelId)) return "openai-responses";
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getModelStripTypes(aliasOrId: string, modelId: string): string[] {
|
||||
|
||||
@@ -12,9 +12,21 @@ export const openaiProvider: RegistryEntry = {
|
||||
defaultContextLength: 128000,
|
||||
models: [
|
||||
{ id: "gpt-5.5", name: "GPT-5.5", contextLength: 1050000 },
|
||||
{ id: "gpt-5.5-pro", name: "GPT-5.5 Pro", contextLength: 1050000 },
|
||||
// #5842: *-pro reasoning models are responses-only upstream — /v1/chat/completions
|
||||
// 404s ("only supported in v1/responses"). targetFormat routes them natively.
|
||||
{
|
||||
id: "gpt-5.5-pro",
|
||||
name: "GPT-5.5 Pro",
|
||||
contextLength: 1050000,
|
||||
targetFormat: "openai-responses",
|
||||
},
|
||||
{ id: "gpt-5.4", name: "GPT-5.4", contextLength: 1050000 },
|
||||
{ id: "gpt-5.4-pro", name: "GPT-5.4 Pro", contextLength: 1050000 },
|
||||
{
|
||||
id: "gpt-5.4-pro",
|
||||
name: "GPT-5.4 Pro",
|
||||
contextLength: 1050000,
|
||||
targetFormat: "openai-responses",
|
||||
},
|
||||
{ id: "gpt-5.4-mini", name: "GPT-5.4 Mini", contextLength: 400000 },
|
||||
{ id: "gpt-5.4-nano", name: "GPT-5.4 Nano", contextLength: 400000 },
|
||||
{ id: "gpt-4.1", name: "GPT-4.1", contextLength: 1047576 },
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "../services/claudeCodeCompatible.ts";
|
||||
import { getGigachatAccessToken } from "../services/gigachatAuth.ts";
|
||||
import { getRegistryEntry } from "../config/providerRegistry.ts";
|
||||
import { getModelTargetFormat } from "../config/providerModels.ts";
|
||||
import {
|
||||
mergeClientAnthropicBeta,
|
||||
normalizeAnthropicHeaderVariants,
|
||||
@@ -182,6 +183,24 @@ export class DefaultExecutor extends BaseExecutor {
|
||||
return `${normalized}${customPath || "/messages"}`;
|
||||
}
|
||||
switch (this.provider) {
|
||||
case "openai": {
|
||||
// #5842: responses-only models (o1-pro / gpt-5.x-pro) 404 on
|
||||
// /v1/chat/completions ("only supported in v1/responses"). Route them to
|
||||
// the native /responses endpoint — the per-model targetFormat (registry tag
|
||||
// + the -pro heuristic in getModelTargetFormat) is the single source of
|
||||
// truth, keeping the URL in lockstep with the chatCore body translation.
|
||||
// Mirrors the gh executor's targetFormat-driven routing (9router#102).
|
||||
const customBaseUrl =
|
||||
typeof credentials?.providerSpecificData?.baseUrl === "string" &&
|
||||
credentials.providerSpecificData.baseUrl.trim()
|
||||
? (credentials.providerSpecificData.baseUrl as string)
|
||||
: null;
|
||||
const chatUrl = customBaseUrl ? normalizeOpenAIChatUrl(customBaseUrl) : this.config.baseUrl;
|
||||
if (getModelTargetFormat("openai", model) === "openai-responses") {
|
||||
return chatUrl.replace(/\/chat\/completions\/?$/, "/responses");
|
||||
}
|
||||
return chatUrl;
|
||||
}
|
||||
case "bailian-coding-plan": {
|
||||
const baseUrl = this.resolveBaseUrl(credentials);
|
||||
return normalizeBailianMessagesUrl(baseUrl);
|
||||
|
||||
103
tests/unit/openai-responses-only-models-5842.test.ts
Normal file
103
tests/unit/openai-responses-only-models-5842.test.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { DefaultExecutor } from "../../open-sse/executors/default.ts";
|
||||
import { getModelTargetFormat } from "../../open-sse/config/providerModels.ts";
|
||||
import { openaiProvider } from "../../open-sse/config/providers/registry/openai/index.ts";
|
||||
import { resolveChatCoreTargetFormat } from "../../open-sse/handlers/chatCore/targetFormat.ts";
|
||||
|
||||
// #5842 — OpenAI responses-only models (o1-pro / gpt-5.x-pro) 404 on
|
||||
// /v1/chat/completions ("This model is only supported in v1/responses").
|
||||
// The native `openai` provider must route them to /v1/responses, mirroring the
|
||||
// gh executor's per-model targetFormat + heuristic routing (9router#102).
|
||||
|
||||
// --- Registry: curated responses-only entries are tagged ---
|
||||
|
||||
test("openai registry tags gpt-5.5-pro and gpt-5.4-pro as openai-responses", () => {
|
||||
for (const id of ["gpt-5.5-pro", "gpt-5.4-pro"]) {
|
||||
const entry = openaiProvider.models.find((m) => m.id === id);
|
||||
assert.ok(entry, `${id} must stay in the curated openai catalog`);
|
||||
assert.equal(
|
||||
entry.targetFormat,
|
||||
"openai-responses",
|
||||
`${id} is responses-only upstream and must carry targetFormat: "openai-responses"`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("openai chat models stay untagged (default /chat/completions path)", () => {
|
||||
for (const id of ["gpt-5.5", "gpt-4o", "gpt-4.1", "o3"]) {
|
||||
assert.equal(
|
||||
getModelTargetFormat("openai", id),
|
||||
null,
|
||||
`${id} is a chat model and must not be re-routed`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// --- Heuristic: dynamically-synced *-pro ids (not in the curated catalog) ---
|
||||
|
||||
test("dynamically-synced OpenAI *-pro ids resolve to openai-responses", () => {
|
||||
for (const id of ["o1-pro", "gpt-5.2-pro"]) {
|
||||
assert.equal(
|
||||
getModelTargetFormat("openai", id),
|
||||
"openai-responses",
|
||||
`${id} should hit the -pro responses-only heuristic`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("the -pro heuristic is scoped to the openai alias only", () => {
|
||||
// blackbox ships gpt-5.4-pro as a plain chat entry — other providers must not
|
||||
// inherit OpenAI's endpoint semantics.
|
||||
assert.equal(getModelTargetFormat("blackbox", "gpt-5.4-pro"), null);
|
||||
});
|
||||
|
||||
// --- chatCore wire format resolution ---
|
||||
|
||||
test("resolveChatCoreTargetFormat picks openai-responses for openai pro models", () => {
|
||||
const { targetFormat } = resolveChatCoreTargetFormat({
|
||||
provider: "openai",
|
||||
resolvedModel: "gpt-5.5-pro",
|
||||
apiFormat: undefined,
|
||||
customModelTargetFormat: undefined,
|
||||
providerSpecificData: null,
|
||||
});
|
||||
assert.equal(targetFormat, "openai-responses");
|
||||
});
|
||||
|
||||
// --- Executor URL routing ---
|
||||
|
||||
test("DefaultExecutor routes openai responses-only models to /v1/responses", () => {
|
||||
const executor = new DefaultExecutor("openai");
|
||||
assert.equal(
|
||||
executor.buildUrl("gpt-5.5-pro", false),
|
||||
"https://api.openai.com/v1/responses"
|
||||
);
|
||||
assert.equal(executor.buildUrl("o1-pro", true), "https://api.openai.com/v1/responses");
|
||||
});
|
||||
|
||||
test("DefaultExecutor keeps openai chat models on /v1/chat/completions", () => {
|
||||
const executor = new DefaultExecutor("openai");
|
||||
assert.equal(
|
||||
executor.buildUrl("gpt-4o", false),
|
||||
"https://api.openai.com/v1/chat/completions"
|
||||
);
|
||||
assert.equal(
|
||||
executor.buildUrl("gpt-5.5", true),
|
||||
"https://api.openai.com/v1/chat/completions"
|
||||
);
|
||||
});
|
||||
|
||||
test("DefaultExecutor honors a custom openai base URL for both endpoints", () => {
|
||||
const executor = new DefaultExecutor("openai");
|
||||
const credentials = { providerSpecificData: { baseUrl: "https://gw.example.com/v1" } };
|
||||
assert.equal(
|
||||
executor.buildUrl("gpt-5.5-pro", false, 0, credentials),
|
||||
"https://gw.example.com/v1/responses"
|
||||
);
|
||||
assert.equal(
|
||||
executor.buildUrl("gpt-4o", false, 0, credentials),
|
||||
"https://gw.example.com/v1/chat/completions"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user