fix(chat): resolve stored combo names before image-model validation (#8986) (#9027)

Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
This commit is contained in:
小妍儿 ✨
2026-08-08 07:52:20 +08:00
committed by GitHub
parent 305a9d5f37
commit 12e5c83692
2 changed files with 36 additions and 3 deletions

View File

@@ -48,7 +48,7 @@ import { checkAndRefreshToken } from "../services/tokenRefresh";
import { createHookContext, runHooks, initPreRequestRegistry } from "@/lib/middleware/registry";
import { rejectPeerRequest } from "@/shared/resilience/peerRouting";
import { deleteHandoff, getHandoff } from "@/lib/db/contextHandoffs";
import { updateCombo } from "@/lib/db/combos";
import { getComboByName, updateCombo } from "@/lib/db/combos";
import { isModelAllowedForKey } from "@/lib/db/apiKeys";
import { promoteSuccessfulComboModel } from "@/lib/combos/autoPromote";
import {
@@ -462,10 +462,14 @@ async function handleChatImplementation(
// image-registry match is only image-only when the same provider/model pair is
// absent from the chat catalog.
const imageModel = getImageModelEntry(modelStr);
// Exact stored combo names take precedence over colliding bare image aliases.
// Keep this narrower than getComboForModel() so mappings and synthetic aliases
// retain their existing resolution order.
const isExactStoredCombo = imageModel ? Boolean(await getComboByName(modelStr)) : false;
const isChatCatalogModel = imageModel
? getModelsByProviderId(imageModel.provider).some((model) => model.id === imageModel.model)
: false;
if (imageModel && !isChatCatalogModel) {
if (imageModel && !isExactStoredCombo && !isChatCatalogModel) {
log.warn("CHAT", `Rejecting image-generation model on chat endpoint: ${modelStr}`);
return errorResponse(
HTTP_STATUS.BAD_REQUEST,

View File

@@ -11,8 +11,11 @@ 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 {
const { buildRequest, combosDb, handleChat, resetStorage } = harness as {
buildRequest: (opts: { body: unknown }) => Request;
combosDb: {
createCombo: (data: Record<string, unknown>) => Promise<unknown>;
};
handleChat: (req: Request) => Promise<Response>;
resetStorage: () => void | Promise<void>;
};
@@ -72,6 +75,32 @@ test("POST /v1/chat/completions with a chat model still reaches routing (guard i
}
});
test("POST /v1/chat/completions routes a stored chat combo whose name is an image alias (#8986)", async () => {
await combosDb.createCombo({
name: "fast",
strategy: "priority",
models: ["openai/gpt-4o"],
});
const request = buildRequest({
body: {
model: "fast",
messages: [{ role: "user", content: "hi" }],
},
});
const res = await handleChat(request);
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,
"a stored chat combo must take precedence over a colliding image alias"
);
}
});
test("POST /v1/chat/completions allows a model registered for both chat and image generation", async () => {
const request = buildRequest({
body: {