mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 15:52:52 +03:00
fix(providers): reject image-only models on /v1/chat/completions with a clear error (#6457) Integrated into release/v3.8.47. (thanks @chirag127)
This commit is contained in:
@@ -258,7 +258,7 @@
|
||||
"src/shared/services/cliRuntime.ts": 1100,
|
||||
"src/shared/validation/schemas.ts": 2523,
|
||||
"_rebaseline_2026_06_28_5275_correlation_id_extract": "Extraction of the safe CorrelationId subset of #5275 (hartmark) — request correlation id stored in call_logs (migration 109) and returned via the X-Correlation-Id response header, WITHOUT the combo/resilience or build/lazy-loading changes (those stay in #5275). Own growth: callLogs.ts 975->985 (correlation_id column on CallLogSummaryRow + read/map), usageHistory.ts 983->988 (correlationId metadata normalize), chat.ts 1575->1632 (withCorrelationId response wiring + combo-failure log carrying correlationId), chatHelpers.ts new 811 (withCorrelationId helper + reqId threading; was 791<cap pre-feature). Cohesive request/logging chokepoint wiring; structural shrink of chat.ts tracked in #3501.",
|
||||
"src/sse/handlers/chat.ts": 1763,
|
||||
"src/sse/handlers/chat.ts": 1778,
|
||||
"src/sse/handlers/chatHelpers.ts": 876,
|
||||
"src/sse/services/auth.ts": 2448,
|
||||
"open-sse/executors/default.ts": 877,
|
||||
@@ -386,5 +386,6 @@
|
||||
"_rebaseline_2026_07_07_6526_chirag_modal_1080p": "PR #6526 (@chirag127, #6265): AddApiKeyModal.tsx ->961 (1080p sizing). Owner-approved. Frozen.",
|
||||
"_rebaseline_2026_07_07_6515_chirag": "PR #6515 (@chirag127) own growth: src/sse/handlers/chat.ts ->1763. Owner-approved rebaseline. Frozen.",
|
||||
"_rebaseline_2026_07_07_6534_chirag": "PR #6534 (@chirag127) own growth: open-sse/services/compression/strategySelector.ts ->1025. Owner-approved rebaseline. Frozen.",
|
||||
"_rebaseline_2026_07_07_6546_chirag": "PR #6546 (@chirag127) own growth: src/sse/handlers/chatHelpers.ts ->876. Owner-approved rebaseline. Frozen."
|
||||
"_rebaseline_2026_07_07_6546_chirag": "PR #6546 (@chirag127) own growth: src/sse/handlers/chatHelpers.ts ->876. Owner-approved rebaseline. Frozen.",
|
||||
"_rebaseline_2026_07_07_6525_chirag_image_guard": "PR #6525 (@chirag127, #6457) own growth: chat.ts ->1778 (reject image-only models on /v1/chat/completions; stacks on #6515). Owner-approved. Frozen."
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { getModelInfo, getComboForModel } from "../services/model";
|
||||
import { resolveBareModelToConnectionDefault } from "@omniroute/open-sse/services/model.ts";
|
||||
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
|
||||
import { getImageModelEntry } from "@omniroute/open-sse/config/imageRegistry.ts";
|
||||
import { acceptHeaderForcesStream } from "@omniroute/open-sse/utils/aiSdkCompat.ts";
|
||||
import { isSelfInflictedUpstreamTimeout } from "@omniroute/open-sse/handlers/chatCore/cooldownClassification.ts";
|
||||
import { applyNoThinkingAlias } from "@omniroute/open-sse/utils/noThinkingAlias.ts";
|
||||
@@ -399,6 +400,20 @@ export async function handleChat(
|
||||
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing model");
|
||||
}
|
||||
|
||||
// Reject image-generation models routed to /v1/chat/completions (#6457).
|
||||
// Image-only models live in IMAGE_PROVIDERS (open-sse/config/imageRegistry.ts)
|
||||
// and are served by /v1/images/generations. Forwarding them to a chat upstream
|
||||
// yielded confusing raw provider 400s (e.g. HuggingFace: "not a chat model").
|
||||
// getImageModelEntry returns non-null only for models registered in the image
|
||||
// registry — chat-only models (openai/gpt-4o, etc.) resolve to null and pass.
|
||||
if (getImageModelEntry(modelStr)) {
|
||||
log.warn("CHAT", `Rejecting image-generation model on chat endpoint: ${modelStr}`);
|
||||
return errorResponse(
|
||||
HTTP_STATUS.BAD_REQUEST,
|
||||
`Model '${modelStr}' is an image-generation model and cannot be used on /v1/chat/completions. Use POST /v1/images/generations instead.`
|
||||
);
|
||||
}
|
||||
|
||||
// T04: client-provided external session header has priority over generated fingerprint.
|
||||
const externalSessionId = extractExternalSessionId(request.headers);
|
||||
const sessionId = externalSessionId || generateStableSessionId(body);
|
||||
|
||||
73
tests/unit/chat-rejects-image-only-model.test.ts
Normal file
73
tests/unit/chat-rejects-image-only-model.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
// #6457: /v1/chat/completions must reject image-only models with a clear 400 pointing
|
||||
// callers at /v1/images/generations, instead of forwarding to a chat upstream that
|
||||
// returns a confusing raw provider 400 (HuggingFace: "not a chat model").
|
||||
//
|
||||
// Discriminator: getImageModelEntry(modelStr) — non-null only for models registered
|
||||
// in open-sse/config/imageRegistry.ts. Chat-only models (openai/gpt-4o etc.) return
|
||||
// null and pass the guard unchanged.
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { createChatPipelineHarness } from "../integration/_chatPipelineHarness.ts";
|
||||
|
||||
const harness = await createChatPipelineHarness("chat-rejects-image-only-model");
|
||||
const { buildRequest, handleChat, resetStorage } = harness as {
|
||||
buildRequest: (opts: { body: unknown }) => Request;
|
||||
handleChat: (req: Request) => Promise<Response>;
|
||||
resetStorage: () => void | Promise<void>;
|
||||
};
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await resetStorage();
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
harness.cleanup?.();
|
||||
});
|
||||
|
||||
test("POST /v1/chat/completions with a HuggingFace image model returns 400 + generations hint (#6457)", async () => {
|
||||
const request = buildRequest({
|
||||
body: {
|
||||
model: "huggingface/stabilityai/stable-diffusion-xl-base-1.0",
|
||||
messages: [{ role: "user", content: "draw a cat" }],
|
||||
},
|
||||
});
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
let fetchCalls = 0;
|
||||
globalThis.fetch = async (...args: Parameters<typeof originalFetch>) => {
|
||||
fetchCalls++;
|
||||
return originalFetch(...args);
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await handleChat(request);
|
||||
assert.equal(res.status, 400, "must reject with 400 before dispatch");
|
||||
const body = (await res.json()) as { error?: { message?: string } };
|
||||
const msg = body?.error?.message || JSON.stringify(body);
|
||||
assert.match(msg, /image-generation model/i);
|
||||
assert.match(msg, /\/v1\/images\/generations/);
|
||||
assert.equal(fetchCalls, 0, "must not dispatch upstream for an image-only model");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("POST /v1/chat/completions with a chat model still reaches routing (guard is invisible)", async () => {
|
||||
const request = buildRequest({
|
||||
body: {
|
||||
model: "openai/gpt-4o",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
},
|
||||
});
|
||||
|
||||
const res = await handleChat(request);
|
||||
// The guard must not fire on a chat model — the response is whatever downstream
|
||||
// routing produces (typically a credentials/connection error in the harness).
|
||||
// The critical assertion is: it is NOT the image-guard 400.
|
||||
if (res.status === 400) {
|
||||
const body = (await res.json()) as { error?: { message?: string } };
|
||||
const msg = body?.error?.message || JSON.stringify(body);
|
||||
assert.doesNotMatch(msg, /image-generation model/i, "chat model must not trip the image guard");
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user