Compare commits

...

6 Commits

Author SHA1 Message Date
oyi77
d396dcc1ef test(chatcore): reconcile #7533 expectation with declared opencode-go tiers
glm-5.2 on opencode-go ships only -high/-max variants (providerRegistry), so the
nearest-tier capability clamp (#11295/#11305) legitimately upgrades unsupported
'low'. No-mutation guarantee still holds for declared tiers (z.ai cases).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-08-25 04:07:38 -03:00
oyi77
dc39a7bcfd fix(quota-share): keep #5923 silent-stop bookkeeping on the no-targets early exit
The #11371 slot-release replaced (instead of augmenting) the recordComboFailure
call on the no-executable-targets path, freezing the pin auto-clear counter.
Restore both effects side by side + regression guard in combo-routing-engine.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-08-25 04:07:38 -03:00
Markus Hartung
ad2e8f600e board #11409 2026-08-25 01:55:51 -03:00
Markus Hartung
2dba0a859f board #11408 2026-08-25 01:46:00 -03:00
oyi77
fa7ccaf679 fix(providers): stop silently dropping reasoning effort on opencode families (#10788)
Dispatch: OpencodeExecutor.transformRequest stripped the effort-suffixed
alias (e.g. glm-5.2-high) down to the base id and injected a flat
reasoning_effort body field — but opencode-go's ChatCompletionRequest has
no such field for non-DeepSeek families, so the tier never reached the
upstream and every request ran at default effort. Only DeepSeek V4
accepts the flat field (its native contract, #4647), so it keeps the old
rewrite; every other family now forwards the aliased model id verbatim,
which is their only native effort mechanism.

Registry: declare supportedThinkingEfforts on the base rows that had
aliases but no tier vocabulary — opencode-go (glm-5.2, mimo-v2.5,
grok-4.5, hy3, kimi-k3, qwen3.7-max/plus), the shared zen/go row
(qwen3.6-plus) and opencode-zen (deepseek-v4-pro/flash, glm-5.2,
kimi-k3) — so catalog variant synthesis (#9485) and
sanitizeReasoningEffortForProvider clamp from one source of truth.
nvidia z-ai/glm-5.2 is declared reasoning-capable with an EMPTY tier
list: its upstream only exposes a binary enable_thinking switch
(mapNvidiaGlm52ReasoningParams), so no honest tiers exist to advertise.

Ollama Cloud declarations were already landed upstream (#11307); this
change covers the remaining providers named in #10788.
2026-08-24 22:06:59 +07:00
oyi77
ef2e5487c0 fix(quota-share): release the winner's reserved in-flight slot (#11371)
selectQuotaShareTarget reserves an in-flight slot for its winner but no
production caller ever invoked the returned release callback, so counters
grew monotonically for the process lifetime: P2C degenerated into
'fewest lifetime dispatches' and the max_concurrent gate fail-opened
past its cap permanently.

Thread the idempotent release out of the ordering path:
- applyStrategyOrdering now returns { orderedTargets, quotaShareRelease }
  (non-null only for the quota-share strategy)
- resolveComboTargetPipeline releases it when a post-selection hard filter
  produces an earlyResponse and otherwise hands it to the host
- handleComboChatInner invokes it in the dispatch finally and on the two
  early returns between resolution and the try block

Regression tests pin the missing case from the issue: ordering through
the real path leaves the counter back at zero, plus null-release for
non-quota-share strategies.
2026-08-24 21:45:42 +07:00
15 changed files with 372 additions and 71 deletions

View File

@@ -12,5 +12,15 @@
export const OPENCODE_ZEN_GO_SHARED_MODELS = Object.freeze([
{ id: "kimi-k2.7-code", name: "Kimi K2.7 Code" },
{ id: "qwen3.5-plus", name: "Qwen3.5 Plus", targetFormat: "claude", supportsVision: false },
{ id: "qwen3.6-plus", name: "Qwen3.6 Plus", targetFormat: "claude", supportsVision: false },
{
id: "qwen3.6-plus",
name: "Qwen3.6 Plus",
targetFormat: "claude",
supportsVision: false,
// #10788: effort-tier aliases exist as explicit registry rows; declare the
// vocabulary on the shared base row so variant synthesis and the sanitizer
// agree on it for both opencode-go and opencode-zen.
supportsReasoning: true,
supportedThinkingEfforts: ["high", "max"],
},
]);

View File

@@ -18,7 +18,17 @@ export const nvidiaProvider: RegistryEntry = {
passthroughModels: true,
models: [
// #6108: z-ai/glm-5.1 EOL'd 2026-07-02 (direct probe returns 410) — dropped.
{ id: "z-ai/glm-5.2", name: "GLM 5.2" },
// #10788: NVIDIA's hosted GLM-5.2 exposes a BINARY thinking switch
// (chat_template_kwargs.enable_thinking), not effort tiers — see
// mapNvidiaGlm52ReasoningParams. Declaring an empty tier list keeps the
// catalog from synthesizing unresolvable -low/-high/-max variant ids while
// still marking the model reasoning-capable.
{
id: "z-ai/glm-5.2",
name: "GLM 5.2",
supportsReasoning: true,
supportedThinkingEfforts: [],
},
// #3329/#6108: minimaxai/minimax-m3 stays excluded from the nvidia tier — it
// still 404s here for most callers; the single 200 probe in #6108 was not
// reproducible enough to override the #3329 guard. Re-add only once NVIDIA

View File

@@ -19,9 +19,15 @@ export const opencode_goProvider: RegistryEntry = {
// `kimi-k2.7-code` (the live API rejects the plain `kimi-k2.7` alias for
// `/chat/completions`, even though the docs config example uses it).
// GLM-5.2 — base model + effort-tier aliases (#6922).
// OpencodeExecutor rewrites the alias to the canonical id and injects
// reasoning_effort, mirroring the deepseek-v4-pro-* pattern.
{ id: "glm-5.2", name: "GLM-5.2", supportsReasoning: true },
// #10788: the tier vocabulary is declared on the base row so the catalog's
// variant synthesis (#9485) and the effort sanitizer share one source of
// truth with OpencodeExecutor's EFFORT_TIERS.
{
id: "glm-5.2",
name: "GLM-5.2",
supportsReasoning: true,
supportedThinkingEfforts: ["high", "max"],
},
{ id: "glm-5.2-high", name: "GLM-5.2 (high effort)", supportsReasoning: true },
{ id: "glm-5.2-max", name: "GLM-5.2 (max effort)", supportsReasoning: true },
@@ -34,11 +40,16 @@ export const opencode_goProvider: RegistryEntry = {
{ id: "kimi-k2.6", name: "Kimi K2.6" },
{ id: "kimi-k2.5", name: "Kimi K2.5" },
// #8353: Kimi K3 base + max-effort alias from the OpenCode Go registry.
{ id: "kimi-k3", name: "Kimi K3", supportsReasoning: true },
{ id: "kimi-k3", name: "Kimi K3", supportsReasoning: true, supportedThinkingEfforts: ["max"] },
{ id: "kimi-k3-max", name: "Kimi K3 (max effort)", supportsReasoning: true },
// MiMo-V2.5 — base model + effort-tier aliases (#6922).
{ id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", supportsReasoning: true },
{ id: "mimo-v2.5", name: "MiMo-V2.5", supportsReasoning: true },
{
id: "mimo-v2.5",
name: "MiMo-V2.5",
supportsReasoning: true,
supportedThinkingEfforts: ["high", "max"],
},
{ id: "mimo-v2.5-high", name: "MiMo-V2.5 (high effort)", supportsReasoning: true },
{ id: "mimo-v2.5-max", name: "MiMo-V2.5 (max effort)", supportsReasoning: true },
// #3110: MiniMax M3 via OpenCode Go tier
@@ -59,7 +70,14 @@ export const opencode_goProvider: RegistryEntry = {
// so combo routing skips them when the request contains image blocks,
// preventing image content from reaching a vision-incapable upstream.
// #8353: effort-tier aliases from the OpenCode Go registry.
{ id: "qwen3.7-max", name: "Qwen3.7 Max", targetFormat: "claude", supportsVision: false },
{
id: "qwen3.7-max",
name: "Qwen3.7 Max",
targetFormat: "claude",
supportsVision: false,
supportsReasoning: true,
supportedThinkingEfforts: ["high", "max"],
},
{
id: "qwen3.7-max-high",
name: "Qwen3.7 Max (high effort)",
@@ -79,6 +97,8 @@ export const opencode_goProvider: RegistryEntry = {
name: "Qwen3.7 Plus",
targetFormat: "claude",
supportsVision: false,
supportsReasoning: true,
supportedThinkingEfforts: ["high", "max"],
},
{
id: "qwen3.7-plus-high",
@@ -111,7 +131,13 @@ export const opencode_goProvider: RegistryEntry = {
supportsReasoning: true,
},
// #8353: hy3 is the Go-tier base id (distinct from hy3-preview / hy3-free).
{ id: "hy3", name: "Hunyuan3", contextLength: 256000, supportsReasoning: true },
{
id: "hy3",
name: "Hunyuan3",
contextLength: 256000,
supportsReasoning: true,
supportedThinkingEfforts: ["none", "low", "high"],
},
{
id: "hy3-none",
name: "Hunyuan3 (none effort)",
@@ -201,7 +227,12 @@ export const opencode_goProvider: RegistryEntry = {
targetFormat: "openai-responses",
},
// #8353: Grok 4.5 + effort tiers from the OpenCode Go registry.
{ id: "grok-4.5", name: "Grok 4.5", supportsReasoning: true },
{
id: "grok-4.5",
name: "Grok 4.5",
supportsReasoning: true,
supportedThinkingEfforts: ["low", "medium", "high"],
},
{ id: "grok-4.5-low", name: "Grok 4.5 (low effort)", supportsReasoning: true },
{ id: "grok-4.5-medium", name: "Grok 4.5 (medium effort)", supportsReasoning: true },
{ id: "grok-4.5-high", name: "Grok 4.5 (high effort)", supportsReasoning: true },

View File

@@ -79,18 +79,35 @@ export const opencode_zenProvider: RegistryEntry = {
},
// ── DeepSeek ────────────────────────────────────────────────
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro" },
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash" },
// #10788: same tier vocabulary as opencode-go's DeepSeek rows — the Zen
// upstream accepts the identical effort set on these models.
{
id: "deepseek-v4-pro",
name: "DeepSeek V4 Pro",
supportsReasoning: true,
supportedThinkingEfforts: ["none", "low", "high", "max"],
},
{
id: "deepseek-v4-flash",
name: "DeepSeek V4 Flash",
supportsReasoning: true,
supportedThinkingEfforts: ["none", "low", "high", "max"],
},
// ── GLM / Z.AI ─────────────────────────────────────────────
{ id: "glm-5.2", name: "GLM-5.2" },
{
id: "glm-5.2",
name: "GLM-5.2",
supportsReasoning: true,
supportedThinkingEfforts: ["high", "max"],
},
// ── MiniMax ────────────────────────────────────────────────
// #3110: MiniMax M3 — frontier coding model with 1M context
{ id: "minimax-m3", name: "MiniMax M3", contextLength: 1048576, supportsVision: true },
// ── Kimi / Moonshot ────────────────────────────────────────
{ id: "kimi-k3", name: "Kimi K3" },
{ id: "kimi-k3", name: "Kimi K3", supportsReasoning: true, supportedThinkingEfforts: ["max"] },
// kimi-k2.7-code declared identically on opencode-go — see OPENCODE_ZEN_GO_SHARED_MODELS.
// ── Qwen ───────────────────────────────────────────────────

View File

@@ -895,10 +895,21 @@ export class OpencodeExecutor extends BaseExecutor {
const mb = modifiedBody as Record<string, unknown>;
const parsed = parseEffortLevel(model);
if (parsed) {
mb.model = parsed.baseModel;
if (mb.reasoning_effort === undefined) {
mb.reasoning_effort = parsed.effort;
const deepseekFamily =
parsed.baseModel === "deepseek-v4-pro" || parsed.baseModel === "deepseek-v4-flash";
if (deepseekFamily) {
// DeepSeek via opencode-go proxies the native DeepSeek contract, which
// accepts a flat reasoning_effort field (#4647).
mb.model = parsed.baseModel;
if (mb.reasoning_effort === undefined) {
mb.reasoning_effort = parsed.effort;
}
}
// #10788: every other family's ONLY native effort mechanism is the
// -<tier> suffix in the model id itself (the ids `opencode models
// opencode-go --verbose` lists). The opencode-go ChatCompletionRequest
// carries no flat reasoning_effort field, so rewriting to the base id
// silently dropped the tier — forward the aliased id verbatim instead.
}
}
// #1543 / upstream PR #1099: thinking-mode upstreams routed through OpenCode

View File

@@ -926,6 +926,9 @@ async function handleComboChatInner({
if (activeNativeTurnPin) {
orderedTargets = applyNativeCodexTurnPin(orderedTargets, activeNativeTurnPin);
if (orderedTargets.length === 0) {
// #11371: quota-share ordering already reserved a winner slot; release it on
// this early exit (idempotent).
targetResolution.quotaShareRelease?.();
return errorResponse(
409,
"The pinned native Codex turn target is no longer available; the turn cannot be moved to another provider"
@@ -953,6 +956,8 @@ async function handleComboChatInner({
// no-target failures (silent-stop fix). Threshold of 3 prevents a one-off account
// wipe from destroying the prompt-cache pin benefit on the next request.
recordComboFailure(effectiveSessionId, combo.name);
// #11371: same early-exit release as the pinned-turn path above.
targetResolution.quotaShareRelease?.();
return errorResponseWithComboDiagnostics(
404,
"Combo has no executable targets",
@@ -2841,6 +2846,9 @@ async function handleComboChatInner({
return await dispatchWithCooldownRetry();
} finally {
quotaShareConcurrencyRelease?.();
// #11371: release the in-flight slot quota-share ordering reserved for its
// winner — the counter must not leak monotonically upward across requests.
targetResolution.quotaShareRelease?.();
// G2: Clean up candidate registry to prevent unbounded memory growth.
_unregisterExecutionCandidates(_registeredExecutionKeys);
}

View File

@@ -20,6 +20,21 @@ import {
} from "./targetSorters.ts";
import type { ComboLike, ComboLogger, ResolvedComboTarget } from "./types.ts";
/**
* Result of {@link applyStrategyOrdering}.
*
* `quotaShareRelease` carries the idempotent release for the in-flight slot that
* quota-share ordering reserves for its winner (#11371). It is non-null only when
* the `quota-share` strategy ran; every other strategy leaves it null. The caller
* MUST invoke it exactly once when the request settles — selection reserves the
* slot, so dropping the callback leaks the counter monotonically upward and
* degenerates P2C into "fewest lifetime dispatches".
*/
export interface ApplyStrategyOrderingResult {
orderedTargets: ResolvedComboTarget[];
quotaShareRelease: (() => void) | null;
}
export interface ApplyStrategyOrderingDeps {
combo: ComboLike;
config: Record<string, unknown>;
@@ -45,9 +60,10 @@ export async function applyStrategyOrdering(
strategy: string,
initialOrderedTargets: ResolvedComboTarget[],
deps: ApplyStrategyOrderingDeps
): Promise<ResolvedComboTarget[]> {
): Promise<ApplyStrategyOrderingResult> {
const { combo, config, body, log, apiKeyAllowedConnections, sessionKey } = deps;
let orderedTargets = initialOrderedTargets;
let quotaShareRelease: (() => void) | null = null;
if (strategy === "lkgp") {
try {
@@ -229,14 +245,18 @@ export async function applyStrategyOrdering(
const qsModel =
typeof body?.model === "string" ? body.model : (orderedTargets[0]?.modelStr ?? "");
const qsMaxConcurrent = await resolveMaxConcurrentByConnection(orderedTargets);
orderedTargets = selectQuotaShareTarget(orderedTargets, combo.name, qsModel, Date.now(), {
const qsSelection = selectQuotaShareTarget(orderedTargets, combo.name, qsModel, Date.now(), {
maxConcurrentByConnection: qsMaxConcurrent,
}).orderedTargets;
});
orderedTargets = qsSelection.orderedTargets;
// #11371: the reservation made inside selectQuotaShareTarget must outlive this
// call — hand the release to the host so it can fire it when the request settles.
quotaShareRelease = qsSelection.decrementInflight;
log.info(
"COMBO",
`Quota-share ordering: ${orderedTargets[0]?.modelStr}${orderedTargets[0]?.connectionId ? ` (${orderedTargets[0].connectionId})` : ""} selected (DRR+P2C)`
);
}
return orderedTargets;
return { orderedTargets, quotaShareRelease };
}

View File

@@ -122,6 +122,13 @@ export interface ResolvedComboTargetPipeline {
/** Session-stickiness result — the attempt loop reads `.messageHash` on success/failure. */
sticky: ApplyStickinessResult;
preScreenMap: Map<string, PreScreenResult>;
/**
* Idempotent release for the in-flight slot quota-share ordering reserved for
* its winner (#11371). Null unless the `quota-share` strategy ran. The host MUST
* invoke it when the request settles; this pipeline already releases it on any
* earlyResponse it produces after selection.
*/
quotaShareRelease: (() => void) | null;
}
export type ResolveComboTargetPipelineResult =
@@ -394,7 +401,11 @@ async function orderByStrategy(
initialOrderedTargets: ResolvedComboTarget[]
): Promise<
| { earlyResponse: Response }
| { orderedTargets: ResolvedComboTarget[]; autoUsedExplicitRouter: boolean }
| {
orderedTargets: ResolvedComboTarget[];
autoUsedExplicitRouter: boolean;
quotaShareRelease: (() => void) | null;
}
> {
const { strategy, body, combo, settings, config, log } = deps;
if (strategy === "auto") {
@@ -413,17 +424,22 @@ async function orderByStrategy(
return {
orderedTargets: autoResult.orderedTargets,
autoUsedExplicitRouter: autoResult.autoUsedExplicitRouter,
quotaShareRelease: null,
};
}
const orderedTargets = await applyStrategyOrdering(strategy, initialOrderedTargets, {
combo,
config,
body,
log,
apiKeyAllowedConnections: deps.apiKeyAllowedConnections,
sessionKey: deps.relayOptions?.sessionId,
});
return { orderedTargets, autoUsedExplicitRouter: false };
const { orderedTargets, quotaShareRelease } = await applyStrategyOrdering(
strategy,
initialOrderedTargets,
{
combo,
config,
body,
log,
apiKeyAllowedConnections: deps.apiKeyAllowedConnections,
sessionKey: deps.relayOptions?.sessionId,
}
);
return { orderedTargets, autoUsedExplicitRouter: false, quotaShareRelease };
}
/**
@@ -714,10 +730,15 @@ export async function resolveComboTargetPipeline(
const ordering = await orderByStrategy(deps, orderedTargets);
if ("earlyResponse" in ordering) return ordering;
const { autoUsedExplicitRouter } = ordering;
const { autoUsedExplicitRouter, quotaShareRelease } = ordering;
const continuity = await applyContinuityFilters(deps, ordering.orderedTargets);
if ("earlyResponse" in continuity) return continuity;
if ("earlyResponse" in continuity) {
// #11371: selection already reserved the winner's in-flight slot; a hard
// filter exhausting the pool must not leak it.
quotaShareRelease?.();
return continuity;
}
orderedTargets = applyTaskAwareOrdering(deps, continuity.orderedTargets, autoUsedExplicitRouter);
orderedTargets = await applyPromptCacheStage(
deps,
@@ -741,5 +762,6 @@ export async function resolveComboTargetPipeline(
getWeightedStepKeyForTarget,
sticky: continuity.sticky,
preScreenMap,
quotaShareRelease,
};
}

View File

@@ -179,7 +179,12 @@ test("Codex Responses routing keeps reasoning effort while dropping GPT-only ver
credentials: null,
});
assert.equal(outbound.reasoning_effort, "low");
// #11409 reconciliation: opencode-go advertises ONLY glm-5.2-high/-max variants
// (providerRegistry go/index.ts), so the #11295/#11305 nearest-tier capability
// clamp legitimately upgrades the unsupported "low" request to the smallest
// declared tier ("high"). The #7533 no-mutation guarantee still holds for any
// tier the resolved target actually declares (covered by the z.ai/Claude cases).
assert.equal(outbound.reasoning_effort, "high");
assert.equal(outbound.verbosity, undefined);
});

View File

@@ -50,7 +50,7 @@ test("unknown strategy -> input order unchanged (same reference contents)", asyn
const input = [target("openai", "gpt-4o"), target("anthropic", "claude-3")];
const out = await applyStrategyOrdering("no-such-strategy", input, deps());
assert.deepEqual(
out.map((t: { executionKey: string }) => t.executionKey),
out.orderedTargets.map((t: { executionKey: string }) => t.executionKey),
["openai>gpt-4o", "anthropic>claude-3"]
);
});
@@ -59,7 +59,7 @@ test("fill-first -> preserves priority order", async () => {
const input = [target("a", "m1"), target("b", "m2"), target("c", "m3")];
const out = await applyStrategyOrdering("fill-first", input, deps());
assert.deepEqual(
out.map((t: { executionKey: string }) => t.executionKey),
out.orderedTargets.map((t: { executionKey: string }) => t.executionKey),
["a>m1", "b>m2", "c>m3"]
);
});
@@ -67,8 +67,8 @@ test("fill-first -> preserves priority order", async () => {
test("random -> same multiset of targets (a permutation)", async () => {
const input = [target("a", "m1"), target("b", "m2"), target("c", "m3")];
const out = await applyStrategyOrdering("random", input, deps());
assert.equal(out.length, 3);
assert.deepEqual(keys(out), keys(input));
assert.equal(out.orderedTargets.length, 3);
assert.deepEqual(keys(out.orderedTargets), keys(input));
});
test("cost-optimized manifest routing logs through the canonical strategy path", async () => {
@@ -90,7 +90,7 @@ test("cost-optimized manifest routing logs through the canonical strategy path",
log,
} as never);
assert.equal(out.length, 2);
assert.equal(out.orderedTargets.length, 2);
assert.equal(
debugCalls.some((args) => args[1] === "manifest routing applied"),
true,

View File

@@ -16,6 +16,9 @@ const {
handleComboChat,
} = await import("../../open-sse/services/combo.ts");
const { resolveComboTargets } = await import("../../open-sse/services/combo/comboStructure.ts");
const { getComboFailureCount, __resetComboFailureTrackerForTests } = await import(
"../../open-sse/services/combo/failureTracker.ts"
);
const { applyPromptCacheAffinity } =
await import("../../open-sse/services/combo/promptCacheAffinity.ts");
const { resolveReasoningBufferedMaxTokens } =
@@ -1327,6 +1330,43 @@ test("handleComboChat returns 404 model_not_found when a combo has no executable
assert.match(payload.error.message, /Combo has no executable targets/);
});
test("#11408 guard: no-executable-targets early exit still records the combo failure (silent-stop counter, #5923)", async () => {
__resetComboFailureTrackerForTests();
const result = await handleComboChat({
body: {},
combo: {
name: "guard-empty-11408",
strategy: "priority",
models: [],
context_cache_protection: true,
},
handleSingleModel: async () => {
throw new Error("handleSingleModel should not run for empty combos");
},
isModelAvailable: async () => true,
log: createLog(),
settings: {
comboDefaults: {
maxRetries: 0,
retryDelayMs: 1,
},
},
relayOptions: { sessionId: "sess-guard-11408" } as any,
allCombos: null,
});
const payload = (await result.json()) as any;
assert.equal(result.status, 404);
// The quota-share slot release must not replace the #5923 silent-stop
// bookkeeping: the consecutive-failure counter must still advance so the
// session pin auto-clears after COMBO_FAILURE_THRESHOLD no-target failures.
assert.equal(
getComboFailureCount("sess-guard-11408", "guard-empty-11408"),
1,
"recordComboFailure must run on the no-executable-targets early exit"
);
});
test("handleComboChat round-robin returns 404 when no models are configured", async () => {
const result = await handleComboChat({
body: {},

View File

@@ -96,35 +96,43 @@ test("#6922 parseEffortLevel: base model without tier → null", () => {
assert.strictEqual(parseEffortLevel("glm-5.2"), null);
});
// ─── transformRequest: end-to-end model-id rewrite + reasoning_effort inject ──
// ─── transformRequest: #6922 wiring, updated by #10788 ─────────────────────
//
// parseEffortLevel is a pure function, but the actual bug (#6922) surfaces
// through OpencodeExecutor.transformRequest — the caller that rewrites the
// outbound model id and injects reasoning_effort. These tests exercise that
// public entry point directly so a broken wiring (e.g. parseEffortLevel
// correct but never called, or its result dropped) would fail here even if
// the parseEffortLevel-only tests above stayed green.
// parseEffortLevel is a pure function, but the original bug (#6922) surfaces
// through OpencodeExecutor.transformRequest. Since #10788, glm-5.2 / mimo-v2.5
// (non-DeepSeek families) forward the effort-suffixed alias VERBATIM — the
// suffix is their only native effort mechanism and opencode-go has no flat
// reasoning_effort field to receive a rewritten tier. Only DeepSeek V4 keeps
// the base-rewrite + field-injection contract.
const CREDENTIALS = { apiKey: "k" } as Record<string, unknown>;
test("#6922 transformRequest: glm-5.2-high → model rewritten to glm-5.2, reasoning_effort injected", () => {
test("#6922/#10788 transformRequest: glm-5.2-high forwards the alias verbatim", () => {
const executor = new OpencodeExecutor("opencode-go");
const body = { model: "glm-5.2-high", messages: [{ role: "user", content: "hi" }] };
const out = executor.transformRequest("glm-5.2-high", body, true, CREDENTIALS);
assert.equal(out.model, "glm-5.2", "model id must be rewritten to the base id");
assert.equal(out.reasoning_effort, "high", "reasoning_effort must be injected from the alias");
assert.equal(out.model, "glm-5.2-high", "alias id must reach the wire untouched");
assert.equal(
out.reasoning_effort,
undefined,
"no flat reasoning_effort may be injected for non-DeepSeek families"
);
});
test("#6922 transformRequest: mimo-v2.5-max → model rewritten to mimo-v2.5, reasoning_effort injected", () => {
test("#6922/#10788 transformRequest: mimo-v2.5-max forwards the alias verbatim", () => {
const executor = new OpencodeExecutor("opencode-go");
const body = { model: "mimo-v2.5-max", messages: [{ role: "user", content: "hi" }] };
const out = executor.transformRequest("mimo-v2.5-max", body, true, CREDENTIALS);
assert.equal(out.model, "mimo-v2.5", "model id must be rewritten to the base id");
assert.equal(out.reasoning_effort, "max", "reasoning_effort must be injected from the alias");
assert.equal(out.model, "mimo-v2.5-max", "alias id must reach the wire untouched");
assert.equal(
out.reasoning_effort,
undefined,
"no flat reasoning_effort may be injected for non-DeepSeek families"
);
});
test("#6922 transformRequest: does not clobber an already-set reasoning_effort", () => {
@@ -137,7 +145,7 @@ test("#6922 transformRequest: does not clobber an already-set reasoning_effort",
const out = executor.transformRequest("glm-5.2-high", body, true, CREDENTIALS);
assert.equal(out.model, "glm-5.2", "model id is still rewritten to the base id");
assert.equal(out.model, "glm-5.2-high", "non-DeepSeek alias id is left untouched");
assert.equal(
out.reasoning_effort,
"caller-supplied",

View File

@@ -27,7 +27,15 @@ const { parseEffortLevel, OpencodeExecutor } =
const { REGISTRY } = (await import("../../open-sse/config/providerRegistry.ts")) as {
REGISTRY: Record<
string,
{ models?: Array<{ id: string; name?: string; targetFormat?: string }> }
{
models?: Array<{
id: string;
name?: string;
targetFormat?: string;
supportsReasoning?: boolean;
supportedThinkingEfforts?: string[];
}>;
}
>;
};
@@ -189,29 +197,46 @@ test("#8353 parseEffortLevel: MiniMax M3 has no effort-tier aliases", () => {
const CREDENTIALS = { apiKey: "k" } as Record<string, unknown>;
// #10788: DeepSeek V4 keeps the base-rewrite + reasoning_effort injection (the
// opencode-go DeepSeek contract accepts the flat field); every other family
// must receive the effort-suffixed alias VERBATIM, because the suffix is their
// only native effort mechanism and opencode-go has no flat reasoning_effort.
const TRANSFORM_SAMPLES = [
{ alias: "deepseek-v4-flash-low", base: "deepseek-v4-flash", effort: "low" },
{ alias: "grok-4.5-medium", base: "grok-4.5", effort: "medium" },
{ alias: "hy3-none", base: "hy3", effort: "none" },
{ alias: "kimi-k3-max", base: "kimi-k3", effort: "max" },
{ alias: "qwen3.7-plus-max", base: "qwen3.7-plus", effort: "max" },
{ alias: "qwen3.7-max-high", base: "qwen3.7-max", effort: "high" },
{
alias: "deepseek-v4-flash-low",
wireModel: "deepseek-v4-flash",
effort: "low" as const,
note: "DeepSeek rewrites to base + reasoning_effort (#4647)",
},
{ alias: "grok-4.5-medium", wireModel: "grok-4.5-medium", effort: null },
{ alias: "hy3-none", wireModel: "hy3-none", effort: null },
{ alias: "kimi-k3-max", wireModel: "kimi-k3-max", effort: null },
{ alias: "qwen3.7-plus-max", wireModel: "qwen3.7-plus-max", effort: null },
{ alias: "qwen3.7-max-high", wireModel: "qwen3.7-max-high", effort: null },
{
alias: "muse-spark-1.2-contributor-xhigh",
base: "muse-spark-1.2-contributor",
effort: "xhigh",
wireModel: "muse-spark-1.2-contributor-xhigh",
effort: null,
},
] as const;
for (const { alias, base, effort } of TRANSFORM_SAMPLES) {
test(`#8353 transformRequest: ${alias} → model=${base}, reasoning_effort=${effort}`, () => {
for (const { alias, wireModel, effort, note } of TRANSFORM_SAMPLES) {
test(`#8353/#10788 transformRequest: ${alias} → model=${wireModel}`, () => {
const executor = new OpencodeExecutor("opencode-go");
const body = { model: alias, messages: [{ role: "user", content: "hi" }] };
const out = executor.transformRequest(alias, body, true, CREDENTIALS);
assert.equal(out.model, base, "model id must be rewritten to the base id");
assert.equal(out.reasoning_effort, effort, "reasoning_effort must be injected from the alias");
assert.equal(out.model, wireModel);
if (effort === null) {
assert.equal(
out.reasoning_effort,
undefined,
"non-DeepSeek families must not receive a flat reasoning_effort field"
);
} else {
assert.equal(out.reasoning_effort, effort, note);
}
});
}
@@ -228,3 +253,42 @@ test("#8353 transformRequest: does not clobber an already-set reasoning_effort",
assert.equal(out.model, "deepseek-v4-flash");
assert.equal(out.reasoning_effort, "caller-supplied");
});
// ─── #10788: base-model tier declarations match the executor vocabulary ────
test("#10788 registry base rows declare the same tiers EFFORT_TIERS parses", () => {
const expectedTiers: Record<string, string[]> = {
"glm-5.2": ["high", "max"],
"mimo-v2.5": ["high", "max"],
"grok-4.5": ["low", "medium", "high"],
hy3: ["none", "low", "high"],
"kimi-k3": ["max"],
"qwen3.6-plus": ["high", "max"],
"qwen3.7-max": ["high", "max"],
"qwen3.7-plus": ["high", "max"],
};
for (const providerId of ["opencode-go", "opencode-zen"]) {
const entry = REGISTRY[providerId];
assert.ok(entry?.models, `${providerId} must expose models`);
for (const [base, tiers] of Object.entries(expectedTiers)) {
if (providerId === "opencode-zen" && !entry.models.some((m) => m.id === base)) continue;
const row = entry.models.find((m) => m.id === base);
assert.ok(row, `${providerId} must declare base model ${base}`);
assert.ok(row.supportsReasoning, `${providerId}/${base} must be reasoning-capable`);
assert.deepEqual(
[...(row.supportedThinkingEfforts ?? [])].sort(),
[...tiers].sort(),
`${providerId}/${base} tier vocabulary must match EFFORT_TIERS`
);
}
}
});
test("#10788 nvidia z-ai/glm-5.2 declares reasoning with an empty tier list (binary switch)", () => {
const entry = REGISTRY["nvidia"];
assert.ok(entry?.models, "nvidia must expose models");
const row = entry.models.find((m) => m.id === "z-ai/glm-5.2");
assert.ok(row, "nvidia z-ai/glm-5.2 must exist");
assert.equal(row.supportsReasoning, true);
assert.deepEqual(row.supportedThinkingEfforts, []);
});

View File

@@ -134,7 +134,7 @@ test("cache-optimized strategy routes a stable prompt key to the same account",
};
const first = await applyStrategyOrdering("cache-optimized", targets, deps);
const second = await applyStrategyOrdering("cache-optimized", [...targets].reverse(), deps);
assert.equal(first[0].connectionId, second[0].connectionId);
assert.equal(first.orderedTargets[0].connectionId, second.orderedTargets[0].connectionId);
});
test("foreign OAuth session softly redirects cache affinity while the same session stays local", () => {
@@ -145,11 +145,18 @@ test("foreign OAuth session softly redirects cache affinity while the same sessi
];
const fixture = Array.from({ length: 10_000 }, (_, index) => {
const body = { prompt_cache_key: `occupied-cache-key-${index}` };
const baseline = applyPromptCacheAffinity(oauthTargets, body, true, "global", "session-a").targets;
const baseline = applyPromptCacheAffinity(
oauthTargets,
body,
true,
"global",
"session-a"
).targets;
const occupied = baseline[0];
const alternative = baseline[1];
const release = reserveOAuthSession(occupied.connectionId!, "session-a");
const foreignFirst = applyPromptCacheAffinity(oauthTargets, body, true, "global", "session-b").targets[0];
const foreignFirst = applyPromptCacheAffinity(oauthTargets, body, true, "global", "session-b")
.targets[0];
release();
return foreignFirst.connectionId === alternative.connectionId
? { body, occupied, alternative }
@@ -160,12 +167,14 @@ test("foreign OAuth session softly redirects cache affinity while the same sessi
const release = reserveOAuthSession(occupied.connectionId!, "session-a");
assert.equal(
applyPromptCacheAffinity(oauthTargets, body, true, "global", "session-a").targets[0].connectionId,
applyPromptCacheAffinity(oauthTargets, body, true, "global", "session-a").targets[0]
.connectionId,
occupied.connectionId,
"the owning session keeps its cache-local account"
);
assert.equal(
applyPromptCacheAffinity(oauthTargets, body, true, "global", "session-b").targets[0].connectionId,
applyPromptCacheAffinity(oauthTargets, body, true, "global", "session-b").targets[0]
.connectionId,
alternative.connectionId,
"a foreign session prefers the free OAuth account"
);

View File

@@ -21,6 +21,7 @@ import {
_clearDrrStateForTest,
_getDrrDeficitForTest,
} from "../../open-sse/services/combo/quotaShareStrategy.ts";
import { applyStrategyOrdering } from "../../open-sse/services/combo/applyStrategyOrdering.ts";
import {
incrementInflight,
decrementInflight,
@@ -490,3 +491,48 @@ describe("activation: qtSd/ combos use strategy 'quota-share'", () => {
);
});
});
// ─── #11371: release must travel out of the ordering path ───────────────────
describe("applyStrategyOrdering threads the in-flight release (#11371)", () => {
const noopLog = { info() {}, warn() {}, error() {}, debug() {} };
test("quota-share ordering returns a release that restores the counter to 0", async () => {
const targets = [makeTarget("ek-rel-a", "conn-rel-a"), makeTarget("ek-rel-b", "conn-rel-b")];
const { orderedTargets, quotaShareRelease } = await applyStrategyOrdering(
"quota-share",
targets,
{
combo: { id: "c-rel", name: "qtSd/rel" },
config: {},
body: { model: "anthropic/claude-sonnet-4-5" },
log: noopLog,
apiKeyAllowedConnections: null,
} as never
);
assert.ok(orderedTargets.length > 0);
assert.ok(quotaShareRelease, "quota-share ordering must hand back a release callback");
const winnerConn = orderedTargets[0].connectionId ?? "";
assert.ok(winnerConn, "winner must carry a connectionId for the reservation");
// Selection reserved exactly one slot on the winner's connection.
assert.equal(getInflight(winnerConn, NOW), 1, "selection must reserve one in-flight slot");
// The real-caller contract: release once when the request settles.
quotaShareRelease!();
assert.equal(getInflight(winnerConn, NOW), 0, "release must return the counter to 0");
// Idempotent: a second call must not push the counter negative.
quotaShareRelease!();
assert.equal(getInflight(winnerConn, NOW), 0, "double release floors at 0");
});
test("non-quota-share strategies leave quotaShareRelease null", async () => {
const result = await applyStrategyOrdering("fill-first", [makeTarget("ek-null", "conn-null")], {
combo: { id: "c-ff", name: "plain" },
config: {},
body: {},
log: noopLog,
apiKeyAllowedConnections: null,
} as never);
assert.equal(result.quotaShareRelease, null);
});
});