fix(providers): resolve unsupportedParams via model aliases so K3 stops 400ing on temperature (#13037)

Merged. Resolving `unsupportedParams` through the model aliases is the fix that generalises — K3 stops 400ing on `temperature`, and any other alias of a model with the same restriction is covered by construction instead of by a second patch later.

Validated as a combined board first (this PR merged with the 21 siblings of the same wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-counts, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 176 passing / 0 failing focused node:test cases across the 25 test files the wave touches and the dashboard test under Vitest (2/0). Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run.

Thank you.
This commit is contained in:
Patryk Kopyciński
2026-09-16 18:37:57 +02:00
committed by GitHub
parent 4591476141
commit 9cc077ef55
3 changed files with 75 additions and 2 deletions

View File

@@ -252,9 +252,9 @@ function ensureUnsupportedParamsPopulated(): void {
*/
export function getUnsupportedParams(provider: string, modelId: string): readonly string[] {
ensureUnsupportedParamsPopulated();
// 1. Check current provider's registry (exact match)
// 1. Check current provider's registry (exact match, then declared aliases)
const entry = getRegistryEntry(provider);
const modelEntry = entry?.models?.find((m) => m.id === modelId);
const modelEntry = entry?.models?.find((m) => m.id === modelId || m.aliases?.includes(modelId));
if (modelEntry?.unsupportedParams) return modelEntry.unsupportedParams;
// 2. O(1) lookup in precomputed map (handles cross-provider routing)

View File

@@ -5,6 +5,12 @@ import { REASONING_UNSUPPORTED, type RegistryEntry, type RegistryModel } from ".
export const KIMI_K3_MODEL: RegistryModel = {
id: "kimi-k3",
name: "Kimi K3",
// Moonshot's LIVE catalogue serves this model as `k3` and `k3-256k` (the
// 256K-context cut), NOT as `kimi-k3`. Both spellings must resolve to this
// entry or `getUnsupportedParams` returns [] and temperature reaches an
// upstream that hard-400s ("invalid temperature: only 1 is allowed for this
// model") — which silently drops the target out of every combo it sits in.
aliases: ["k3", "k3-256k"],
contextLength: 1048576,
maxOutputTokens: 1048576,
supportsVision: true,

View File

@@ -0,0 +1,67 @@
// tests/unit/moonshot-k3-unsupported-params-aliases.test.ts
//
// Live incident (2026-09-07): every `moonshot/k3-256k` hop in the `judge` and
// `best-reasoning-paid` combos hard-400'd with
// "invalid temperature: only 1 is allowed for this model"
// because the registry catalogues the model as `kimi-k3` while Moonshot's live
// catalogue serves it as `k3` / `k3-256k`. `getUnsupportedParams` matched on the
// canonical id only, returned [], and temperature was forwarded upstream.
//
// The kimi-coding provider (`kmc`/`kmca`) serves its OWN `k3`/`k3-256k` over the
// Anthropic-format path and DOES accept temperature — verified live: temp=0
// returns 200 there. So the resolution must stay provider-scoped; a global
// alias index would wrongly strip temperature for kimi-coding.
import { test } from "node:test";
import assert from "node:assert/strict";
import { getUnsupportedParams } from "../../open-sse/config/providerRegistry.ts";
const stripsTemperature = (provider: string, modelId: string): boolean =>
getUnsupportedParams(provider, modelId).includes("temperature");
test("moonshot live K3 ids resolve to the kimi-k3 reasoning restrictions", () => {
for (const modelId of ["k3", "k3-256k", "kimi-k3"]) {
assert.equal(
stripsTemperature("moonshot", modelId),
true,
`moonshot/${modelId} must strip temperature`
);
}
});
test("the kimi provider shares the moonshot catalogue and resolves the same aliases", () => {
assert.equal(stripsTemperature("kimi", "k3-256k"), true);
assert.equal(stripsTemperature("kimi", "k3"), true);
});
test("full REASONING_UNSUPPORTED set resolves via alias, not just temperature", () => {
const viaAlias = getUnsupportedParams("moonshot", "k3-256k");
const viaCanonical = getUnsupportedParams("moonshot", "kimi-k3");
assert.deepEqual([...viaAlias], [...viaCanonical]);
for (const param of ["temperature", "top_p", "frequency_penalty", "n"]) {
assert.ok(viaAlias.includes(param), `expected ${param} in alias-resolved params`);
}
});
test("kimi-coding keeps temperature — its k3 ids are a DIFFERENT upstream that accepts it", () => {
for (const provider of ["kimi-coding", "kmc", "kimi-coding-apikey", "kmca"]) {
for (const modelId of ["k3", "k3-256k"]) {
assert.equal(
stripsTemperature(provider, modelId),
false,
`${provider}/${modelId} must NOT strip temperature`
);
}
}
});
test("non-K3 moonshot models are unaffected by the alias resolution", () => {
// kimi-for-coding has no unsupportedParams and accepts temperature live.
assert.equal(stripsTemperature("moonshot", "kimi-for-coding"), false);
// Sibling reasoning models keep their existing restrictions.
assert.equal(stripsTemperature("moonshot", "kimi-k2.7-code"), true);
assert.equal(stripsTemperature("moonshot", "kimi-k2.6"), true);
});
test("an unknown model id still resolves to no restrictions", () => {
assert.deepEqual([...getUnsupportedParams("moonshot", "definitely-not-a-model")], []);
});