Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/fixes/12371-kimi-k3-effort-tiers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(models):** publish `effort_tiers` on Kimi K3's synced base-model entries (`kmca/k3`, `kmca/k3-256k`) so catalog-only clients (OpenCode, plain SDK pickers) can see and select the reasoning tiers (`low`/`high`/`max`) the synced metadata already carried — the `isSkippedEffortProvider` gate no longer suppresses tier visibility on those base entries, while synthetic `<id>-<tier>` variant generation stays prevented and Codex/GLM base models remain excluded unchanged ([#12299](https://github.com/diegosouzapw/OmniRoute/issues/12299))
30 changes: 29 additions & 1 deletion src/app/api/v1/models/syncedCapabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
* `<id>-<tier>` catalog entries (open-sse/utils/syncedEffortVariants.ts) — it
* never runs over the base entry's `capabilities`, so it cannot substitute
* for this check. Required (not optional) so no call site can silently skip it.
*
* #12299 carve-out: Kimi K3's synced base entries (`k3`, `k3-256k` — the kmca
* catalog's `low`/`high`/`max` vocabulary) are exempted from the exclusion so
* catalog-only clients (OpenCode, plain SDK pickers) can see and select their
* tiers. Model-scoped, never provider-wide: Codex, GLM, and non-K3 kimi models
* keep the full exclusion exactly as before this carve-out.
*/
// Use the same canonical alias as catalogModelPolicy.ts (l.1) — a relative path from
// src/app/api/v1/models/ to open-sse/ would need 5 `../` and silently breaks under
Expand All @@ -39,8 +45,30 @@ interface SyncedCapabilityFlags {
supportedThinkingEfforts?: string[];
}

// Model-id pattern for the Kimi K3 family (#12299): the kmca catalog syncs
// `k3`/`k3-256k` (and prefixed forms such as `kmca/k3`). Same shape the
// executor/translator layers use to recognize K3 elsewhere
// (reasoningContentInjector.ts::K3_AUTHENTIC_REASONING_PATTERN).
const KIMI_K3_MODEL_ID_PATTERN = /(?:^|\/)(?:kimi-)?k3(?:$|-)/i;

/**
* #12299: only Kimi K3's synced BASE entries are exempt from the
* `isSkippedEffortProvider` exclusion. Model-scoped, never provider-wide —
* the exemption requires a kimi-owned provider AND a K3 model id, so Codex,
* GLM, and non-K3 kimi models keep the exclusion contract from #7694.
*/
function isExemptKimiK3BaseModel(sm: SyncedCapabilityFlags, ownedBy: string): boolean {
return (
ownedBy.startsWith("kimi") && typeof sm.id === "string" && KIMI_K3_MODEL_ID_PATTERN.test(sm.id)
);
}

function effectiveEffortTiers(sm: SyncedCapabilityFlags, ownedBy: string): string[] | undefined {
if (isSkippedEffortProvider(ownedBy)) return undefined;
// Exclusion gate (#7694): codex/glm/kimi own a conflicting `-{effort}` suffix
// mechanism — the blind opencode-plugin mapping must never see effort_tiers
// for them, or it double-handles the suffix. #12299 narrows only the kimi K3
// base-model entries out of that gate; everything else stays excluded.
if (isSkippedEffortProvider(ownedBy) && !isExemptKimiK3BaseModel(sm, ownedBy)) return undefined;
const learned = sm.id ? getLearnedReasoningEffortForModel(sm.id) : null;
const synced =
Array.isArray(sm.supportedThinkingEfforts) && sm.supportedThinkingEfforts.length > 0
Expand Down
103 changes: 103 additions & 0 deletions tests/unit/kimi-k3-effort-tiers-12299.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import path from "node:path";
import {
buildSyncedCapabilities,
mergeSyncedCapabilities,
} from "../../src/app/api/v1/models/syncedCapabilities.ts";
import {
shouldExposeSyncedEffortVariants,
appendSyncedEffortVariants,
} from "../../open-sse/utils/syncedEffortVariants.ts";

// #12299: Kimi K3's supportedThinkingEfforts (["low", "high", "max"]) were
// suppressed on the BASE model by isSkippedEffortProvider in
// effectiveEffortTiers(), leaving catalog-only clients with no tiers to copy.
// Fix: publish effort_tiers on the base model while still preventing
// synthetic <id>-<tier> variant generation for kimi providers.

const KIMI_K3_TIERS = ["low", "high", "max"];

test("Kimi K3 base model publishes effort_tiers via buildSyncedCapabilities (#12299)", () => {
const caps = buildSyncedCapabilities(
{ id: "k3", supportsThinking: true, supportedThinkingEfforts: KIMI_K3_TIERS },
"kimi-coding-apikey"
);
assert.ok(caps, "capabilities must be defined for kimi K3");
assert.deepEqual(
caps.effort_tiers,
KIMI_K3_TIERS,
"kimi K3 base model must publish effort_tiers low/high/max"
);
});

test("Kimi K3-256k base model publishes effort_tiers via buildSyncedCapabilities (#12299)", () => {
const caps = buildSyncedCapabilities(
{ id: "k3-256k", supportsThinking: true, supportedThinkingEfforts: KIMI_K3_TIERS },
"kimi-coding-apikey"
);
assert.ok(caps, "capabilities must be defined for kimi K3-256k");
assert.deepEqual(
caps.effort_tiers,
KIMI_K3_TIERS,
"kimi K3-256k base model must publish effort_tiers low/high/max"
);
});

test("Kimi K3 merge path also publishes effort_tiers (#12299)", () => {
const merged = mergeSyncedCapabilities(
{ tool_calling: true },
{ id: "k3", supportsThinking: true, supportedThinkingEfforts: KIMI_K3_TIERS },
"kimi-coding-apikey"
);
assert.ok(merged, "merged capabilities must be defined");
assert.deepEqual(
merged.effort_tiers,
KIMI_K3_TIERS,
"merge path must publish kimi K3 effort_tiers"
);
assert.equal(merged.tool_calling, true, "existing tool_calling must be preserved");
});

test("shouldExposeSyncedEffortVariants still prevents synthetic kimi variants", () => {
// The base model should NOT generate synthetic <id>-<tier> entries
assert.equal(
shouldExposeSyncedEffortVariants({
id: "kimi/k3",
owned_by: "kimi-coding-apikey",
capabilities: { effort_tiers: KIMI_K3_TIERS },
}),
false,
"must not generate synthetic kimi/k3-low, kimi/k3-high, etc."
);
});

test("appendSyncedEffortVariants does not create kimi variant entries", () => {
const models = [
{
id: "kimi-coding-apikey/k3",
owned_by: "kimi-coding-apikey",
capabilities: { effort_tiers: KIMI_K3_TIERS },
},
];
const result = appendSyncedEffortVariants(models);
assert.equal(result.length, 1, "must not add synthetic variant entries for kimi");
assert.equal(result[0].id, "kimi-coding-apikey/k3", "original entry must be unchanged");
});

test("kimi K3 static registry tiers match synced metadata", () => {
// Verify the static registry in runtime.ts has the correct tiers
const runtimePath = path.join(
path.dirname(fileURLToPath(import.meta.url)),
"../../open-sse/config/providers/registry/kimi/coding/runtime.ts"
);
const content = readFileSync(runtimePath, "utf8");

// Verify the static thinking policies declare the same tiers
assert.ok(
content.includes('"low", "high", "max"'),
"KIMI_CODE_STATIC_THINKING_POLICIES.k3 must declare low/high/max"
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ test("merge path keeps vision AND applies the learned override", () => {
// Exclusion gate (#7694): codex/glm/kimi already own a conflicting
// `-{effort}` suffix mechanism — the blind opencode-plugin mapping must never
// see effort_tiers for them, learned or synced, or it double-handles the suffix.
// #12299 exempts only Kimi K3's BASE model entries (asserted in
// tests/unit/kimi-k3-effort-tiers-12299.test.ts) — non-K3 kimi models such as
// "excluded-model" below stay excluded alongside codex/glm.
for (const ownedBy of ["codex", "glm", "glm-cn", "glmt", "kimi", "kimi-coding-apikey"]) {
test(`build: excluded provider "${ownedBy}" never gets effort_tiers (synced)`, () => {
const caps = buildSyncedCapabilities(
Expand Down