fix(providers): scope model target formats to providers (#10072)

Co-authored-by: xz-dev <xz-dev@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
This commit is contained in:
Xiangzhe
2026-08-17 18:01:09 +08:00
committed by GitHub
parent b082d0735b
commit faeca3bbac
2 changed files with 22 additions and 16 deletions

View File

@@ -173,14 +173,11 @@ export function getModelTargetFormat(aliasOrId: string, modelId: string): string
// Accept either the public alias ("cmd") or the raw provider id ("command-code"),
// mirroring getProviderModels (same pattern as #2798/#3870).
const alias = PROVIDER_ID_TO_ALIAS[aliasOrId] || aliasOrId;
const models = PROVIDER_MODELS[alias];
// Strip provider prefix if present: "openai/gpt-5.6-luna" → "gpt-5.6-luna"
const prefix = alias + "/";
const bareModelId =
typeof modelId === "string" && modelId.startsWith(prefix)
? modelId.slice(prefix.length)
: modelId;
const found = models?.find((m) => m.id === bareModelId);
const prefixes = [`${aliasOrId}/`, `${alias}/`];
const prefix = prefixes.find((value) => modelId.startsWith(value));
const bareModelId = prefix ? modelId.slice(prefix.length) : modelId;
const found = PROVIDER_MODELS[alias]?.find((m) => m.id === bareModelId);
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
@@ -188,16 +185,13 @@ export function getModelTargetFormat(aliasOrId: string, modelId: string): string
// 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 (alias === "openai" && /-pro$/i.test(modelId)) return "openai-responses";
if (alias === "openai" && /-pro$/i.test(bareModelId)) return "openai-responses";
// Model-level targetFormat is provider-scoped: a catalog entry declares how THIS
// provider's endpoint serves the model. When the provider has its own catalog but
// the model is not in it, do NOT import the global entry's tag — it encodes the
// DECLARING provider's endpoint semantics (e.g. ghe-copilot tags gpt-5.6-* as
// openai-responses, which must not hijack command-code's chat-shaped
// /alpha/generate → 502 "Invalid prompt: messages must not be empty"). Providers
// with no catalog at all keep the global fallback as their only metadata source.
if (models) return null;
return getGlobalModel(bareModelId)?.targetFormat ?? null;
// provider's endpoint serves the model — do NOT import another provider's tag.
// #9994 scoped this for providers WITH a catalog; #10072 extends it to catalogless
// providers (openai-compatible-chat-*), which previously inherited the declaring
// provider's endpoint semantics via the global fallback.
return null;
}
export function getModelStripTypes(aliasOrId: string, modelId: string): string[] {
const models = PROVIDER_MODELS[aliasOrId];

View File

@@ -58,6 +58,18 @@ test("delegates byte-identically for a normal model (no apiFormat / no custom ov
assert.deepEqual(r, expected("openai", "gpt-4o", undefined, undefined, undefined));
});
test("provider-local target format does not leak from another provider", () => {
const r = resolveChatCoreTargetFormat({
provider: "openai-compatible-chat-example",
resolvedModel: "gpt-5.6-sol",
apiFormat: undefined,
sourceFormat: FORMATS.OPENAI_RESPONSES,
customModelTargetFormat: undefined,
providerSpecificData: undefined,
});
assert.equal(r.targetFormat, FORMATS.OPENAI);
});
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