Files
OmniRoute/tests/unit/provider-models-target-format-scoping.test.ts
Chewji a367bf62f5 fix(providers): scope model-level targetFormat to declaring provider catalog (#9994)
Model-level targetFormat is provider-scoped endpoint semantics: a catalog entry
declares how the DECLARING provider serves the model. getModelTargetFormat()
fell back to getGlobalModel() when the provider's own catalog lacked the model
id, importing another provider's tag into every provider serving that id.

catalog. command-code serves gpt-5.6-luna over its chat-shaped /alpha/generate
endpoint but inherited that tag, so chatCore translated the request to Responses
format (messages -> input). CommandCodeExecutor.buildCommandCodeBody reads
chat-format input.messages -> undefined -> [] -> upstream 502 "Invalid prompt:
messages must not be empty" (call log 1786341194167-774a5b).

Fix: resolve the provider alias (mirroring getProviderModels), only apply the
provider's OWN catalog entry's targetFormat, and skip the global fallback when
the provider has a catalog. Catalog-less providers keep the global fallback
unchanged; ghe-copilot's Responses routing (#8835) is preserved.

Regression test: tests/unit/provider-models-target-format-scoping.test.ts
(red before the fix, green after).
2026-08-11 10:04:24 -03:00

39 lines
2.0 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import { getModelTargetFormat } from "../../open-sse/config/providerModels.ts";
import { resolveChatCoreTargetFormat } from "../../open-sse/handlers/chatCore/targetFormat.ts";
// Regression: #8835 tagged gpt-5.6-* with targetFormat "openai-responses" in the
// ghe-copilot catalog. getModelTargetFormat falls back to getGlobalModel() when
// the provider's own catalog lacks the model id, importing the DECLARING
// provider's endpoint semantics into every other provider serving the same id.
// command-code's chat-shaped /alpha/generate executor then received a
// Responses-format body (input, not messages) and shipped `messages: []`
// upstream — upstream rejected with "Invalid prompt: messages must not be empty"
// (502). Model-level targetFormat is provider-scoped: it must not leak.
test("model-level targetFormat does not leak across provider catalogs", () => {
// command-code serves gpt-5.6-luna over its chat-shaped /alpha/generate endpoint
assert.equal(getModelTargetFormat("cmd", "gpt-5.6-luna"), null);
// raw provider id form behaves identically (alias resolution)
assert.equal(getModelTargetFormat("command-code", "gpt-5.6-luna"), null);
// the declaring provider (ghe-copilot) keeps its Responses routing
assert.equal(getModelTargetFormat("gh", "gpt-5.6-luna"), "openai-responses");
assert.equal(getModelTargetFormat("ghe-copilot", "gpt-5.6-luna"), "openai-responses");
// unrelated models are unaffected
assert.equal(getModelTargetFormat("cmd", "kimi-k2"), null);
});
test("chat completions to command-code gpt-5.6-luna resolve to chat format", () => {
const resolved = resolveChatCoreTargetFormat({
provider: "command-code",
resolvedModel: "gpt-5.6-luna",
apiFormat: undefined,
sourceFormat: "openai",
customModelTargetFormat: undefined,
providerSpecificData: null,
});
assert.equal(resolved.alias, "cmd");
assert.equal(resolved.targetFormat, "openai");
});