@oh-my-pi/pi-catalog 18.2.0 → 18.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/types/compat/behavior.d.ts +11 -0
  3. package/dist/types/compat/cascade.d.ts +10 -0
  4. package/dist/types/compat/collapse.d.ts +8 -0
  5. package/dist/types/compat/resolve.d.ts +10 -0
  6. package/dist/types/compat/types.d.ts +21 -0
  7. package/dist/types/discovery/antigravity.d.ts +10 -1
  8. package/dist/types/model-thinking.d.ts +7 -0
  9. package/dist/types/provider-models/openai-compat.d.ts +2 -0
  10. package/dist/types/types.d.ts +36 -1
  11. package/package.json +4 -4
  12. package/src/build.ts +59 -0
  13. package/src/compat/axes.ts +15 -0
  14. package/src/compat/behavior.ts +22 -2
  15. package/src/compat/cascade.ts +27 -1
  16. package/src/compat/collapse.ts +100 -8
  17. package/src/compat/context-window.ts +11 -1
  18. package/src/compat/resolve.ts +56 -17
  19. package/src/compat/rules/README.md +3 -1
  20. package/src/compat/rules/classes/deepseek.kdl +9 -1
  21. package/src/compat/rules/classes/kimi.kdl +6 -0
  22. package/src/compat/rules/providers/alibaba-token-plan.kdl +16 -8
  23. package/src/compat/rules/providers/amazon-bedrock.kdl +30 -0
  24. package/src/compat/rules/providers/azure.kdl +6 -0
  25. package/src/compat/rules/providers/cerebras.kdl +10 -0
  26. package/src/compat/rules/providers/commandcode.kdl +20 -4
  27. package/src/compat/rules/providers/cursor.kdl +32 -0
  28. package/src/compat/rules/providers/deepseek.kdl +5 -5
  29. package/src/compat/rules/providers/devin.kdl +46 -0
  30. package/src/compat/rules/providers/google-vertex.kdl +15 -0
  31. package/src/compat/rules/providers/kimi-code.kdl +48 -0
  32. package/src/compat/rules/providers/meta.kdl +3 -0
  33. package/src/compat/rules/providers/muse-code.kdl +3 -0
  34. package/src/compat/rules/providers/openrouter.kdl +6 -0
  35. package/src/compat/rules/runtime/behavior.kdl +17 -0
  36. package/src/compat/rules/taxonomy/deepseek.kdl +5 -0
  37. package/src/compat/rules.json +1 -1
  38. package/src/compat/types.ts +23 -0
  39. package/src/discovery/antigravity.ts +80 -43
  40. package/src/discovery/devin.ts +22 -4
  41. package/src/identity/bundled.ts +4 -3
  42. package/src/model-cache.ts +154 -120
  43. package/src/model-thinking.ts +10 -7
  44. package/src/models.json +1 -1
  45. package/src/provider-models/bundled-references.ts +4 -3
  46. package/src/provider-models/cache-provider-id.ts +14 -8
  47. package/src/provider-models/ollama.ts +11 -31
  48. package/src/provider-models/openai-compat.ts +297 -37
  49. package/src/types.ts +38 -0
@@ -734,6 +734,80 @@ function reconcileDefaultMember<TSpec extends VariantSpecLike>(
734
734
  return spec;
735
735
  }
736
736
 
737
+ /**
738
+ * Whether a Cursor wire id names an extended tier that upstream serves only in
739
+ * max mode. The compiled taxonomy identifies `xhigh`/`extra-high`/`max`
740
+ * efforts and their optional service lanes. This is an inference, not an
741
+ * upstream marker: it is the only per-tier signal available for bundled rows
742
+ * and for routes live discovery never advertised.
743
+ */
744
+ export function isCursorMaxModeWireId(wireModelId: string): boolean {
745
+ const effort = collapseVariantId("cursor", wireModelId).effort;
746
+ return effort === Effort.XHigh || effort === Effort.Max;
747
+ }
748
+
749
+ /**
750
+ * Recover Cursor's max-mode marker for a bundled collapsed row. The bundled
751
+ * snapshot may contain only the logical row, but its effort routing still
752
+ * records the wire ids, so {@link isCursorMaxModeWireId} preserves the
753
+ * transport invariant even when live discovery contributes no raw members.
754
+ */
755
+ function reconcileCursorMaxModeFromRouting<TSpec extends VariantSpecLike>(spec: TSpec): TSpec {
756
+ if (spec.provider !== "cursor" || spec.cursorMaxMode === true) return spec;
757
+ const routing = spec.thinking?.effortRouting;
758
+ if (routing === undefined) return spec;
759
+ const hasMaxModeRoute = Object.values(routing).some(
760
+ (target): target is string => typeof target === "string" && isCursorMaxModeWireId(target),
761
+ );
762
+ return hasMaxModeRoute ? { ...spec, cursorMaxMode: true } : spec;
763
+ }
764
+
765
+ /**
766
+ * Index the discovered `max_mode` marker of every live Cursor member by its own
767
+ * wire id. The collapsed row's `cursorMaxMode` is an OR across members, so it
768
+ * says nothing per tier; the transport needs the marker addressable by the wire
769
+ * id it actually sends, because upstream marks tiers the slug cannot identify
770
+ * (the whole Opus `-fast` lane is max-mode, `-low-fast` included). Returns
771
+ * `undefined` when no member carries a marker, so unmarked rosters add no field.
772
+ */
773
+ function cursorMaxModeRoutesOf<TSpec extends VariantSpecLike>(
774
+ provider: string,
775
+ memberSpecs: readonly TSpec[],
776
+ ): Record<string, boolean> | undefined {
777
+ if (provider !== "cursor") return undefined;
778
+ let routes: Record<string, boolean> | undefined;
779
+ for (const member of memberSpecs) {
780
+ if (member.cursorMaxMode === undefined) continue;
781
+ routes ??= {};
782
+ routes[member.id] = member.cursorMaxMode;
783
+ }
784
+ return routes;
785
+ }
786
+
787
+ /**
788
+ * Lift Cursor's max-mode markers from live member rows onto an already-collapsed
789
+ * snapshot. Bundled catalog and cache rows froze the flag from `memberSpecs[0]`
790
+ * — the `-none`/`-low` tier — so the committed `gpt-5.6-*` / `cursor-grok-*`
791
+ * rows carry `cursorMaxMode: false`. The existing-collapsed pass-through keeps
792
+ * the snapshot verbatim, so a live `GetUsableModels` roster that marks the
793
+ * `-xhigh`/`-max` tiers would be discarded and max-tier requests would keep
794
+ * sending `max_mode: false` on a max-mode-only wire id. The row-level flag
795
+ * mirrors the fresh-collapse aggregation: only the positive case is lifted, so
796
+ * a roster that marks nothing leaves the snapshot alone. The per-wire-id
797
+ * markers are merged on top of the snapshot's own so live rows win per route.
798
+ * Returns `spec` by reference when unchanged.
799
+ */
800
+ function reconcileCursorMaxMode<TSpec extends VariantSpecLike>(spec: TSpec, memberSpecs: readonly TSpec[]): TSpec {
801
+ const routes = cursorMaxModeRoutesOf(spec.provider, memberSpecs);
802
+ const lifts = spec.cursorMaxMode !== true && memberSpecs.some(member => member.cursorMaxMode === true);
803
+ if (routes === undefined && !lifts) return spec;
804
+ return {
805
+ ...spec,
806
+ ...(lifts ? { cursorMaxMode: true } : {}),
807
+ ...(routes === undefined ? {} : { cursorMaxModeRoutes: { ...spec.cursorMaxModeRoutes, ...routes } }),
808
+ };
809
+ }
810
+
737
811
  /**
738
812
  * Collapse every family in `table` found in `specs`. Non-member specs pass
739
813
  * through verbatim (by reference), order preserved; the collapsed spec
@@ -776,7 +850,9 @@ function collapseWithTable<TSpec extends VariantSpecLike>(
776
850
  // Recycled extraAliases rows are healed in a later pass.
777
851
  const refreshed =
778
852
  existing !== undefined && existingCollapsed
779
- ? reconcileDefaultMember(refreshCollapsedThinking(reconciled ?? existing, family, retired), family)
853
+ ? reconcileCursorMaxModeFromRouting(
854
+ reconcileDefaultMember(refreshCollapsedThinking(reconciled ?? existing, family, retired), family),
855
+ )
780
856
  : reconciled;
781
857
  if (refreshed !== undefined && refreshed !== existing) {
782
858
  familyIdBySpecId.set(family.id, family.id);
@@ -788,20 +864,26 @@ function collapseWithTable<TSpec extends VariantSpecLike>(
788
864
  for (const id of rawPresent) familyIdBySpecId.set(id, family.id);
789
865
  if (existing) familyIdBySpecId.set(family.id, family.id);
790
866
 
867
+ const memberSpecs: TSpec[] = [];
868
+ for (const id of rawPresent) {
869
+ const member = byId.get(id);
870
+ if (member !== undefined) memberSpecs.push(member);
871
+ }
872
+
791
873
  if (existingCollapsed && reconciled !== undefined) {
792
874
  // Mixed input: the collapsed entry wins; stale raw members are deduped
793
875
  // away. Retired targets are re-pointed first, then the default wire id
794
876
  // prefers the family's declared member when live and otherwise falls
795
- // back to the first member the account actually advertised.
796
- replacement.set(family.id, reconcileDefaultMember(reconciled, family, new Set(rawPresent)));
877
+ // back to the first member the account actually advertised. The live
878
+ // members still own `cursorMaxMode`: the snapshot froze it from the
879
+ // lowest tier.
880
+ replacement.set(
881
+ family.id,
882
+ reconcileCursorMaxMode(reconcileDefaultMember(reconciled, family, new Set(rawPresent)), memberSpecs),
883
+ );
797
884
  continue;
798
885
  }
799
886
 
800
- const memberSpecs: TSpec[] = [];
801
- for (const id of rawPresent) {
802
- const member = byId.get(id);
803
- if (member !== undefined) memberSpecs.push(member);
804
- }
805
887
  const firstMember = memberSpecs[0];
806
888
  if (firstMember === undefined) continue;
807
889
  const presentSet = new Set(rawPresent);
@@ -836,6 +918,14 @@ function collapseWithTable<TSpec extends VariantSpecLike>(
836
918
  if (memberSpecs.some(spec => spec.input.includes("text"))) input.push("text");
837
919
  if (memberSpecs.some(spec => spec.input.includes("image"))) input.push("image");
838
920
 
921
+ // `cursorMaxMode` gates the `max_mode` request flag. The collapsed row
922
+ // otherwise inherits `memberSpecs[0]`, so a family whose max-mode member
923
+ // is not the first one would advertise `false` and send `max_mode: false`
924
+ // on a max-mode wire id. Only the positive case is aggregated — an
925
+ // unmarked family keeps whatever the first member carried. The OR loses
926
+ // which tier needed it, so the members' own markers are kept per wire id.
927
+ const cursorMaxMode = memberSpecs.some(spec => spec.cursorMaxMode === true) ? true : undefined;
928
+ const cursorMaxModeRoutes = cursorMaxModeRoutesOf(firstMember.provider, memberSpecs);
839
929
  const collapsed: TSpec = {
840
930
  ...firstMember,
841
931
  id: family.id,
@@ -844,6 +934,8 @@ function collapseWithTable<TSpec extends VariantSpecLike>(
844
934
  input,
845
935
  contextWindow: maxOrNull(memberSpecs.map(spec => spec.contextWindow)),
846
936
  maxTokens: maxOrNull(memberSpecs.map(spec => spec.maxTokens)),
937
+ ...(cursorMaxMode === undefined ? {} : { cursorMaxMode }),
938
+ ...(cursorMaxModeRoutes === undefined ? {} : { cursorMaxModeRoutes }),
847
939
  };
848
940
  // The default wire id is the family's declared `defaultMember` when live,
849
941
  // else the highest-priority live member. Omitted when it equals the
@@ -6,8 +6,11 @@ import { resolveModelPolicy } from "./resolve";
6
6
  * Rule-owned maxima by provider/id/api. Resolve once per process rather than
7
7
  * walking the static policy cascade on every catalog rebuild. Null caches the
8
8
  * absence of a curated maximum; undefined means the key has not been resolved.
9
+ * Bounded: one entry per distinct model; the wire-id set is bounded.
9
10
  */
10
11
  const ruleMaximumCache = new Map<string, number | null>();
12
+ const clampOverrideCache = new Map<string, boolean>();
13
+ const RULE_POLICY_CACHE_MAX = 8192;
11
14
 
12
15
  /**
13
16
  * Extended-context capacity. Curated maxima correct stale lower discovery
@@ -20,6 +23,7 @@ export function resolveMaxContextWindow(model: Model): number | undefined {
20
23
  if (curated === undefined) {
21
24
  const maximum = resolveModelPolicy(toModelSpec(model)).catalog.maxContextWindow;
22
25
  curated = typeof maximum === "number" && Number.isFinite(maximum) && maximum > 0 ? maximum : null;
26
+ if (ruleMaximumCache.size >= RULE_POLICY_CACHE_MAX) ruleMaximumCache.clear();
23
27
  ruleMaximumCache.set(key, curated);
24
28
  }
25
29
 
@@ -49,7 +53,13 @@ export function codexOverrideCeiling(model: Model): number | undefined {
49
53
  * it here keeps provider deployment contracts out of TypeScript.
50
54
  */
51
55
  export function clampsContextOverride(model: Model): boolean {
52
- return resolveModelPolicy(toModelSpec(model)).catalog.clampContextOverride === true;
56
+ const key = `${model.provider} ${model.id} ${model.api}`;
57
+ const cached = clampOverrideCache.get(key);
58
+ if (cached !== undefined) return cached;
59
+ const clamps = resolveModelPolicy(toModelSpec(model)).catalog.clampContextOverride === true;
60
+ if (clampOverrideCache.size >= RULE_POLICY_CACHE_MAX) clampOverrideCache.clear();
61
+ clampOverrideCache.set(key, clamps);
62
+ return clamps;
53
63
  }
54
64
 
55
65
  /**
@@ -34,7 +34,7 @@ import type {
34
34
  import { isAnthropicSigningProxyUrl, isAzureAnthropicRoute, isOfficialAnthropicApiUrl } from "./anthropic";
35
35
  import { applyCompatOverrides } from "./apply";
36
36
  import { API_COMPAT_RECORDS, AXES, type CompatRecordName } from "./axes";
37
- import { resolveCascade } from "./cascade";
37
+ import { hasModelScopedEffortsRule, resolveCascade } from "./cascade";
38
38
  import { compareRevision, parseRevision, type Revision } from "./revision";
39
39
  import { classifyModel, stripThinkingVariantSuffix } from "./taxonomy";
40
40
  import type { ModelIdentity, ResolvedAxes, ResolveTarget } from "./types";
@@ -305,7 +305,11 @@ interface OpenAIDetection {
305
305
  isOpenRouter: boolean;
306
306
  }
307
307
 
308
- function detectOpenAI(spec: ModelSpec<"openai-completions" | "openrouter">, facts: IdentityFacts): OpenAIDetection {
308
+ function detectOpenAI(
309
+ spec: ModelSpec<"openai-completions" | "openrouter">,
310
+ facts: IdentityFacts,
311
+ reasoningCapable: boolean,
312
+ ): OpenAIDetection {
309
313
  const provider = spec.provider;
310
314
  const baseUrl = spec.baseUrl;
311
315
  const hostModel = { provider, baseUrl };
@@ -315,7 +319,7 @@ function detectOpenAI(spec: ModelSpec<"openai-completions" | "openrouter">, fact
315
319
  const isXiaomiHost = modelMatchesHost(hostModel, "xiaomi");
316
320
  const isDirectDeepseekApi = modelMatchesHost(hostModel, "deepseekDirect");
317
321
  const isDeepseekFamily = modelMatchesHost(hostModel, "deepseekFamily") || facts.is("deepseek");
318
- const isDeepseekReasoning = isDeepseekFamily && Boolean(spec.reasoning);
322
+ const isDeepseekReasoning = isDeepseekFamily && reasoningCapable;
319
323
  const isLocalOpenAICompatBackend =
320
324
  PROXY_OPENAI_COMPAT_PROVIDERS[provider] !== true &&
321
325
  (LOCAL_OPENAI_COMPAT_PROVIDERS[provider] === true || hasLocalLoopbackBaseUrl(baseUrl));
@@ -348,6 +352,7 @@ function detectOpenAI(spec: ModelSpec<"openai-completions" | "openrouter">, fact
348
352
  function detectOpenAICompat(
349
353
  spec: ModelSpec<"openai-completions" | "openrouter">,
350
354
  d: OpenAIDetection,
355
+ reasoningCapable: boolean,
351
356
  ): ResolvedOpenAICompat {
352
357
  const provider = spec.provider;
353
358
  const baseUrl = spec.baseUrl;
@@ -435,12 +440,12 @@ function detectOpenAICompat(
435
440
  ? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS
436
441
  : facts.is("mimo") && hostMatchesUrl(baseUrl, "xiaomi")
437
442
  ? 300_000
438
- : spec.reasoning &&
443
+ : reasoningCapable &&
439
444
  facts.is("kimi") &&
440
445
  (facts.family("k3") || facts.family("k2.7-code")) &&
441
446
  hostMatchesUrl(baseUrl, "moonshotNative")
442
447
  ? 300_000
443
- : spec.reasoning && facts.is("deepseek") && hostMatchesUrl(baseUrl, "deepseekDirect")
448
+ : reasoningCapable && facts.is("deepseek") && hostMatchesUrl(baseUrl, "deepseekDirect")
444
449
  ? 300_000
445
450
  : d.isLocalServingBackend
446
451
  ? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS
@@ -476,13 +481,14 @@ function detectOpenAICompat(
476
481
  // provider rule without changing Copilot Responses rows.
477
482
  supportsReasoningParams: provider !== "github-copilot",
478
483
  supportsSamplingParams: !(facts.is("openai") && (facts.family("o-series") || facts.revGte("5"))),
479
- supportsPenaltyAndStopParams: !(isGrok && Boolean(spec.reasoning)),
484
+ supportsPenaltyAndStopParams: !(isGrok && reasoningCapable),
480
485
  reasoningEffortMap: {},
481
486
  supportsUsageInStreaming: !isCerebrasHost,
482
487
  alwaysSendMaxTokens: facts.is("kimi"),
483
488
  disableReasoningOnForcedToolChoice:
484
489
  !d.isClinePass && ((facts.is("kimi") && !isMoonshotKimiK3) || isAnthropicModel),
485
- disableReasoningOnToolChoice: !d.isClinePass && isDeepseekFamily && Boolean(spec.reasoning) && !d.isOpenRouter,
490
+ disableReasoningOnToolChoice: !d.isClinePass && isDeepseekFamily && reasoningCapable && !d.isOpenRouter,
491
+ disableReasoningWithTools: false,
486
492
  supportsToolChoice: d.isClinePass || !d.isDirectDeepseekReasoning,
487
493
  supportsForcedToolChoice:
488
494
  !d.requiresEnabledThinking && !(d.isOpenCodeHost && d.isDeepseekReasoning) && !(d.isClinePass && isQwen),
@@ -506,12 +512,12 @@ function detectOpenAICompat(
506
512
  reasoningContentField: d.isClinePass ? "reasoning" : "reasoning_content",
507
513
  requiresReasoningContentForToolCalls:
508
514
  (facts.is("kimi") && !d.isOpenCodeProvider) ||
509
- (isDeepseekFamily && Boolean(spec.reasoning)) ||
515
+ (isDeepseekFamily && reasoningCapable) ||
510
516
  d.isXiaomiMimo ||
511
- (d.isOpenRouter && Boolean(spec.reasoning)),
517
+ (d.isOpenRouter && reasoningCapable),
512
518
  requiresReasoningContentForAllAssistantTurns:
513
- ((isDeepseekFamily && Boolean(spec.reasoning)) || d.isXiaomiMimo) && !d.isOpenRouter,
514
- allowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !d.isXiaomiMimo,
519
+ ((isDeepseekFamily && reasoningCapable) || d.isXiaomiMimo) && !d.isOpenRouter,
520
+ allowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !reasoningCapable) && !d.isXiaomiMimo,
515
521
  replayReasoningContent: d.isLocalOpenAICompatBackend,
516
522
  qwenPreserveThinking:
517
523
  (thinkingFormat === "qwen" || thinkingFormat === "qwen-chat-template") && d.isLocalOpenAICompatBackend,
@@ -577,6 +583,11 @@ const DSML_HEALING_PROVIDERS: Record<string, true> = {
577
583
  nanogpt: true,
578
584
  "opencode-go": true,
579
585
  openrouter: true,
586
+ // Transparent gateways / user-configured hosts forward the upstream model's
587
+ // native chat template unchanged, so a deepseek-classed model behind them
588
+ // still emits DSML tool-call envelopes and needs the DSML healer.
589
+ litellm: true,
590
+ nous: true,
580
591
  };
581
592
 
582
593
  /**
@@ -635,7 +646,7 @@ function fixupOpenAICompat(
635
646
  compat.omitReasoningEffort = true;
636
647
  }
637
648
 
638
- const axisWhenThinking = spec.reasoning ? objectPayload(axes.wire.whenThinking) : undefined;
649
+ const axisWhenThinking = compatReasoning(spec, axes) ? objectPayload(axes.wire.whenThinking) : undefined;
639
650
  const whenThinkingPolicy =
640
651
  spec.compat?.whenThinking ??
641
652
  axisWhenThinking ??
@@ -662,8 +673,9 @@ function resolveOpenAICompletionsPolicy(
662
673
  facts: IdentityFacts,
663
674
  axes: ResolvedAxes,
664
675
  ): ResolvedOpenAICompat {
665
- const d = detectOpenAI(spec, facts);
666
- const compat = detectOpenAICompat(spec, d);
676
+ const reasoningCapable = compatReasoning(spec, axes);
677
+ const d = detectOpenAI(spec, facts, reasoningCapable);
678
+ const compat = detectOpenAICompat(spec, d, reasoningCapable);
667
679
  applyWireAxes(compat, axes.wire, "openai-completions");
668
680
  applyCompatOverrides(compat, spec.compat);
669
681
  overlayEffortMapAxis(compat, axes, spec.compat);
@@ -689,7 +701,7 @@ function resolveOpenAIResponsesPolicy(
689
701
  const supportsPromptCacheBreakpoints =
690
702
  isOfficialOpenAIEndpoint(provider, baseUrl) && facts.is("openai") && facts.revGte("5.6");
691
703
  const thinkingFormat: ResolvedOpenAISharedCompat["thinkingFormat"] = isOpenRouter ? "openrouter" : "openai";
692
- const reasoningCapable = Boolean(spec.reasoning);
704
+ const reasoningCapable = compatReasoning(spec, axes);
693
705
  const isLocalServingBackend =
694
706
  (PROXY_OPENAI_COMPAT_PROVIDERS[provider] !== true && LOCAL_OPENAI_COMPAT_PROVIDERS[provider] === true) ||
695
707
  hasLocalLoopbackBaseUrl(baseUrl);
@@ -729,6 +741,7 @@ function resolveOpenAIResponsesPolicy(
729
741
  filterReasoningHistory: isOpenRouter && isAnthropicModel,
730
742
  disableReasoningOnForcedToolChoice: facts.is("kimi"),
731
743
  disableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,
744
+ disableReasoningWithTools: false,
732
745
  supportsToolChoice: true,
733
746
  supportsForcedToolChoice: provider !== "opencode-go" && provider !== "opencode-zen",
734
747
  supportsNamedToolChoice: true,
@@ -860,7 +873,8 @@ function resolveAnthropicPolicy(
860
873
  supportsSamplingParams: !facts.anthropicAdaptiveGenAtLeast("4.7"),
861
874
  requiresToolResultId: false,
862
875
  requiresThinkingEnabled,
863
- replayUnsignedThinking: !signingEndpoint && (Boolean(spec.reasoning) || modelMatchesHost(spec, "deepseekFamily")),
876
+ replayUnsignedThinking:
877
+ !signingEndpoint && (compatReasoning(spec, axes) || modelMatchesHost(spec, "deepseekFamily")),
864
878
  escapeBuiltinToolNames: false,
865
879
  injectClaudeCodeInstruction: true,
866
880
  stripImageInput: false,
@@ -884,7 +898,7 @@ function resolveBedrockPolicy(spec: ModelSpec<"bedrock-converse-stream">, axes:
884
898
  promptCacheMaximumCheckpoints: 0,
885
899
  };
886
900
  // Reasoning capability is a mechanism gate; adaptive-lineage duration is rule-owned.
887
- compat.streamIdleTimeoutMs = spec.reasoning ? BEDROCK_REASONING_STREAM_IDLE_TIMEOUT_MS : undefined;
901
+ compat.streamIdleTimeoutMs = compatReasoning(spec, axes) ? BEDROCK_REASONING_STREAM_IDLE_TIMEOUT_MS : undefined;
888
902
  applyWireAxes(compat, axes.wire, "bedrock-converse-stream");
889
903
  applyCompatOverrides(compat, spec.compat);
890
904
  return compat;
@@ -1047,6 +1061,18 @@ function readRuleThinking(axes: ResolvedAxes): RuleThinking {
1047
1061
  return out;
1048
1062
  }
1049
1063
 
1064
+ /**
1065
+ * Compat-time reasoning capability. `axes.reasoning` also promotes targets on
1066
+ * any exact `thinking-efforts` rule (the cascade's thinking-axis gate), but
1067
+ * compat may only be repaired where the provider contract opted in with
1068
+ * `thinking-upgrade-neutral`; everywhere else a spec that reports no reasoning
1069
+ * stays the authoritative capability surface.
1070
+ */
1071
+ function compatReasoning<TApi extends Api>(spec: ModelSpec<TApi>, axes: ResolvedAxes): boolean {
1072
+ if (spec.reasoning) return true;
1073
+ return axes.reasoning && readRuleThinking(axes).upgradeNeutral === true;
1074
+ }
1075
+
1050
1076
  /** Identity-derived `requiresEffort` default (mandatory-reasoning lineages). */
1051
1077
  function impliesMandatoryReasoning(facts: IdentityFacts, modelId: string): boolean {
1052
1078
  if (facts.identity.thinkingVariant) return true;
@@ -1245,3 +1271,16 @@ export function resolveModelPolicy(spec: ModelSpec<Api>): ResolvedModelPolicy<Ap
1245
1271
  catalog: axes.catalog,
1246
1272
  };
1247
1273
  }
1274
+
1275
+ /**
1276
+ * Whether reviewed rules know THIS model's effort ladder, as opposed to it
1277
+ * inheriting a provider-wide default or {@link resolveThinkingPolicy} falling
1278
+ * through to the neutral wire ladder.
1279
+ *
1280
+ * Discovery uses this to tell "omp knows this model's tiers" apart from "omp
1281
+ * is guessing them", so catalog-published tiers can correct the guess without
1282
+ * ever overriding reviewed knowledge.
1283
+ */
1284
+ export function hasModelScopedEffortLadder<TApi extends Api>(spec: ModelSpec<TApi>): boolean {
1285
+ return hasModelScopedEffortsRule(buildResolveTarget(spec, resolveIdentity(spec)));
1286
+ }
@@ -288,15 +288,17 @@ behavior {
288
288
  route "openai-completions" prefix="openai/" strip-prefix=#true
289
289
  }
290
290
  model-limits provider="github-copilot" { limits "gpt-5.6" context=272000 max-tokens=128000 }
291
+ exclude-discovery-modes "embedding" "moderation" provider="litellm"
291
292
  exclude-models provider="nanogpt" substring="embed" substring="tts"
292
293
  plan-requirement provider="openai-codex" { tier "pro" substring="-spark" }
294
+ retry-reset-timezone provider="zai" offset="+08:00"
293
295
  pricing-peer provider="google-antigravity" peers="google" "google-vertex" {
294
296
  alias "gemini-3-pro" peer-id="gemini-3-pro-preview"
295
297
  }
296
298
  }
297
299
  ```
298
300
 
299
- Matcher properties on `route` / `exclude-models` / `tier` nodes are `exact=` / `prefix=` / `substring=` / `glob=`, repeatable. `strip-prefix=#true` on a prefix route strips the matched prefix off the wire id. Values are copied verbatim from the TS constants they replaced; runtime accessors live in `src/compat/behavior.ts`.
301
+ `exclude-discovery-modes` takes one or more exact, case-sensitive upstream mode strings plus `provider=`; discovery mappers preserve missing, malformed, and unknown modes unless the provider policy explicitly lists them. Matcher properties on `route` / `exclude-models` / `tier` nodes are `exact=` / `prefix=` / `substring=` / `glob=`, repeatable. `strip-prefix=#true` on a prefix route strips the matched prefix off the wire id. Values are copied verbatim from the TS constants they replaced; runtime accessors live in `src/compat/behavior.ts`.
300
302
 
301
303
  ## Auth grammar
302
304
 
@@ -84,7 +84,15 @@ class "deepseek" {
84
84
  models "*v4.1-flash*" priority=10 {
85
85
  strip-image-input #false
86
86
  }
87
- on "ollama-cloud" "nvidia" "deepseek" "fireworks" "nanogpt" "opencode-go" "openrouter" {
87
+ // V4.1 Flash is natively multimodal (vision encoder + projector, image-text
88
+ // pre-training) but its release id carries no `vision` token to key on.
89
+ // Same priority as the V4.1 Flash carve-out: the OpenCode Go provider rule
90
+ // declares the same value for its `deepseek-flash` lane and would otherwise
91
+ // tie at equal rank.
92
+ models "deepseek-flash" priority=10 {
93
+ strip-image-input #false
94
+ }
95
+ on "ollama-cloud" "nvidia" "deepseek" "fireworks" "nanogpt" "opencode-go" "openrouter" "litellm" "nous" {
88
96
  stream-markup-healing-pattern "dsml"
89
97
  }
90
98
  thinking-loop-guard "deepseek"
@@ -1,6 +1,12 @@
1
1
  // Model-lineage compat for "kimi"; regenerated from the frozen census using class taxonomy selectors.
2
2
 
3
3
  class "kimi" {
4
+ // Kimi chat templates can revise bytes they have already streamed: a leaned-on
5
+ // thinking opener reclassifies visible text as reasoning, and native
6
+ // section/tool-call tokens are carved out of the visible channel mid-stream.
7
+ // Declared revision-possible so finished prose stays in the live viewport
8
+ // instead of retiring into native scrollback ahead of the turn.
9
+ stream-revision "possible"
4
10
  // Replaces the K2.6 reasoning stream-idle baseline on OpenAI-compatible routes.
5
11
  on "baseten" "cline-pass" "cloudflare-ai-gateway" "coreweave" "deepinfra" "firepass" "fireworks" \
6
12
  "huggingface" "moonshot" "novita" "nvidia" "opencode-go" "opencode-zen" "openrouter" \
@@ -114,10 +114,17 @@ provider "alibaba-token-plan" {
114
114
  models "deepseek-v4-pro" {
115
115
  thinking-format "qwen"
116
116
  }
117
- // residue: taxonomy ranks and exact globs do not isolate these models.
118
- models "qwen3.8-max" {
119
- thinking-default-level "xhigh"
120
- thinking-efforts "low" "medium" "xhigh"
117
+ // Max Preview rides only Alibaba's binary enable_thinking toggle: it stays on
118
+ // the Qwen dialect and must not emit an unsupported reasoning_effort field.
119
+ models "qwen3.8-max-preview" {
120
+ thinking-efforts "low" "high" "xhigh"
121
+ thinking-requires-effort #true
122
+ }
123
+ // Max and Flash steer thinking depth through the OpenAI reasoning_effort
124
+ // control and replay reasoning_content across turns (preserve_thinking
125
+ // defaults on for both).
126
+ models "qwen3.8-max" "qwen3.8-flash" {
127
+ replay-reasoning-content #true
121
128
  when-thinking {
122
129
  extra-body {
123
130
  enable_thinking #true
@@ -125,9 +132,10 @@ provider "alibaba-token-plan" {
125
132
  thinking-format "openai"
126
133
  }
127
134
  }
128
- // residue: taxonomy ranks and exact globs do not isolate these models.
129
- models "qwen3.8-max-preview" {
130
- thinking-efforts "low" "high" "xhigh"
131
- thinking-requires-effort #true
135
+ // Max exposes a distinct low/medium/xhigh ladder; Flash retains its
136
+ // provider-authored minimal/low/medium/high ladder.
137
+ models "qwen3.8-max" {
138
+ thinking-default-level "xhigh"
139
+ thinking-efforts "low" "medium" "xhigh"
132
140
  }
133
141
  }
@@ -3,7 +3,15 @@
3
3
  provider "amazon-bedrock" {
4
4
  default-model "us.anthropic.claude-opus-4-8"
5
5
 
6
+ // The Converse assembler positions content blocks by wire index
7
+ // (`amazon-bedrock.ts`), so a block can land above text already rendered.
8
+ stream-revision "possible"
6
9
  class "anthropic" {
10
+ family "fable" {
11
+ revision ">=5.1 <5.2" {
12
+ thinking-efforts "low" "medium" "high" "xhigh" "max"
13
+ }
14
+ }
7
15
  family "opus" {
8
16
  revision ">=4.6 <4.7" {
9
17
  thinking-mode "anthropic-adaptive"
@@ -42,4 +50,26 @@ provider "amazon-bedrock" {
42
50
  models "moonshot.kimi-k2-thinking" {
43
51
  thinking-requires-effort #true
44
52
  }
53
+ // The Qwen rows arrive with maxTokens copied from the upstream context
54
+ // window (262000 on a 262144 window), so a request that does not set an
55
+ // output budget of its own asks Bedrock for more than the model accepts
56
+ // and gets a 400 back ("you requested 262000 output tokens"). AWS
57
+ // publishes the real caps on the model cards — 8K for Qwen3-Next 80B A3B
58
+ // and Qwen3 VL 235B A22B, 16K for Qwen3 Coder Next — and the context
59
+ // windows already match, so only the output side needs patching (#12089).
60
+ models "qwen.qwen3-next-80b-a3b" {
61
+ limits-patch {
62
+ max-tokens 8000
63
+ }
64
+ }
65
+ models "qwen.qwen3-vl-235b-a22b" {
66
+ limits-patch {
67
+ max-tokens 8000
68
+ }
69
+ }
70
+ models "qwen.qwen3-coder-next" {
71
+ limits-patch {
72
+ max-tokens 16000
73
+ }
74
+ }
45
75
  }
@@ -19,4 +19,10 @@ provider "azure" {
19
19
  models "gpt-5.1-codex-mini" {
20
20
  thinking-efforts "medium" "high"
21
21
  }
22
+ // Azure Chat Completions rejects Astra reasoning whenever function tools are
23
+ // present; the documented escape hatch is an explicit `none` effort.
24
+ models "gpt-6-astra*" {
25
+ disable-reasoning-with-tools #true
26
+ reasoning-disable-mode "none-effort"
27
+ }
22
28
  }
@@ -16,6 +16,16 @@ provider "cerebras" {
16
16
  input-modalities "text" "image"
17
17
  }
18
18
  thinking-mode "effort"
19
+ // Cerebras accepts OpenAI `reasoning_effort`, not Qwen-native thinking
20
+ // fields such as `enable_thinking`.
21
+ class "qwen" {
22
+ thinking-format "openai"
23
+ }
24
+ // Qwen 3.8 accepts low/medium/high; `none` is its wire-only off value.
25
+ models "qwen-3.8-27b" {
26
+ thinking-efforts "low" "medium" "high"
27
+ reasoning-disable-mode "none-effort"
28
+ }
19
29
  // residue: taxonomy ranks and exact globs do not isolate these models.
20
30
  models "zai-glm-4.7" {
21
31
  thinking-efforts "minimal" "low" "medium" "high" "xhigh"
@@ -1,10 +1,13 @@
1
1
  // Command Code Provider API deployment contract. Model capabilities and effort
2
2
  // ladders follow command-code@1.44.0: the thinking-efforts below reproduce the
3
3
  // bundle's 36-id effort registry 1:1 (verified against its effort map), plus
4
- // the 5 Muse Spark lineage ids whose ladder OMP's classes/meta.kdl carries.
5
- // Every other served id exposes no effort dial upstream (`modelSupports-
6
- // ReasoningEffort` is false for them in the CLI), so a missing ladder here
7
- // is a deliberate absence, not a gap.
4
+ // the 5 Muse Spark lineage ids whose ladder OMP's classes/meta.kdl carries and
5
+ // `deepseek/deepseek-v4.1-flash`, which postdates that bundle (see its rule).
6
+ // Every id the 1.44.0 census serves without an effort dial keeps a missing
7
+ // ladder here as a deliberate absence, not a gap (`modelSupportsReasoning-
8
+ // Effort` is false for those ids in that bundle). Later bundles (1.53.0) hand
9
+ // dials to a few further ids — MiniMaxAI/MiniMax-M3 and meta/muse-spark-1.3's
10
+ // `max` — which stay out of this change.
8
11
  provider "commandcode" {
9
12
  default-model "claude-sonnet-4-6"
10
13
  env "COMMAND_CODE_API_KEY" "COMMANDCODE_API_KEY"
@@ -54,6 +57,19 @@ provider "commandcode" {
54
57
  supports-reasoning-effort #true
55
58
  thinking-efforts "low" "high" "max"
56
59
  }
60
+ // `deepseek/deepseek-v4.1-flash` postdates command-code@1.44.0, so it is
61
+ // absent from that bundle's effort registry; command-code@1.53.0 lists it
62
+ // in the same low/high/max group as Kimi K3 and GLM-5.3, and the live docs
63
+ // model table carries the same scale plus image input.
64
+ models "deepseek/deepseek-v4.1-flash" {
65
+ supports-reasoning-effort #true
66
+ thinking-efforts "low" "high" "max"
67
+ // The class's `token="vision"` exemption misses this id (its name
68
+ // carries no `vision` token) even though the deployment serves it with
69
+ // image input, so the opt-out is explicit here.
70
+ strip-image-input #false
71
+ input-modalities "text" "image"
72
+ }
57
73
  models "google/gemini-3.1-flash-lite" "google/gemini-3.5-flash" \
58
74
  "google/gemini-3.5-flash-lite" "google/gemini-3.6-flash" "google/gemini-3.7-flash" \
59
75
  "google/gemini-3.8-flash" "gpt-5.4-mini" "xai/grok-4.5" {
@@ -11,6 +11,15 @@ provider "cursor" {
11
11
  class "anthropic" {
12
12
  family "fable" {
13
13
  requires-cursor-tool-schema-projection #true
14
+ // Cursor advertised Context window 300k (Max 1M recovered elsewhere).
15
+ context-window-floor 300000
16
+ }
17
+ family "opus" {
18
+ revision ">=5 <6" {
19
+ // Cursor advertised Claude Opus 5 Context window 300k; Max 1M
20
+ // stays recovered on labeled/maxMode rows via Math.max.
21
+ context-window-floor 300000
22
+ }
14
23
  }
15
24
  }
16
25
  // GetUsableModels advertises no input modalities or context windows.
@@ -21,11 +30,34 @@ provider "cursor" {
21
30
  input-modalities "text" "image"
22
31
  context-window-floor 1000000
23
32
  }
33
+ family "k2.7-code" {
34
+ // Cursor advertised Kimi K2.7 Code Context window 262k.
35
+ context-window-floor 262000
36
+ }
37
+ }
38
+ class "xai" {
39
+ family "grok" {
40
+ revision ">=4.5 <4.7" {
41
+ // Cursor advertised Grok 4.5/4.6 Context window 256k (product
42
+ // cap; not xAI native 500k). Max context is unmarked.
43
+ context-window-floor 256000
44
+ }
45
+ }
46
+ }
47
+ class "openai" {
48
+ revision ">=5.6 <5.7" {
49
+ // Cursor advertised GPT-5.6 Sol/Terra/Luna Context window 272k;
50
+ // raises unlabeled -fast variants from the 200k default.
51
+ context-window-floor 272000
52
+ }
24
53
  }
25
54
  models "k3" "*/k3" {
26
55
  input-modalities "text" "image"
27
56
  context-window-floor 1000000
28
57
  }
58
+ models "default" {
59
+ context-window-floor 256000
60
+ }
29
61
  // Cursor-only families verified to accept selectedImages even though the
30
62
  // roster carries no modality metadata.
31
63
  models "cursor-grok-4" "cursor-grok-4.*" "cursor-grok-4-*" "cursor-grok-4:*" "cursor-grok-4_*" {
@@ -17,8 +17,8 @@ provider "deepseek" {
17
17
  // First-party peak USD / 1M tokens; all other UTC hours receive 50% off.
18
18
  // Source: https://api-docs.deepseek.com/quick_start/pricing (2026-09-10).
19
19
  // residue: the pricing page's Flash-priced names, not a taxonomy family: the
20
- // bare alias carries no family, and the retired `v4-flash`/`-vision-exp` ids
21
- // are still accepted and billed at the Flash card.
20
+ // retired `v4-flash`/`-vision-exp` ids are still accepted and billed at the
21
+ // Flash card.
22
22
  models "deepseek-flash" "deepseek-v4-flash" "deepseek-v4-flash-vision-exp" {
23
23
  clamp-output-to-model-max #true
24
24
  time-based-cost {
@@ -43,9 +43,9 @@ provider "deepseek" {
43
43
  cache-write 0
44
44
  }
45
45
  }
46
- // residue: upstream discovery has not seeded the bare alias's limits; the
47
- // pricing page documents this SKU (DeepSeek-V4.1-Flash) as 1M context / 384K
48
- // output, and without them the agent cannot enforce its context budget.
46
+ // residue: the pricing page documents this SKU (DeepSeek-V4.1-Flash) as 1M
47
+ // context / 384K output; pinned as a reviewed correction so the documented
48
+ // window survives an upstream metadata gap.
49
49
  models "deepseek-flash" {
50
50
  limits-patch {
51
51
  context-window 1000000