@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
package/CHANGELOG.md CHANGED
@@ -2,6 +2,45 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.2.2] - 2026-09-16
6
+
7
+ ### Added
8
+
9
+ - OpenAI-compatible model discovery now fills in reasoning-effort tiers for unrecognized models using the shared catalog’s published reasoning options, while preserving explicit discovery metadata and reviewed model rules.
10
+
11
+ ### Fixed
12
+
13
+ - Fixed recovery of corrupted model caches so private backups are preserved and concurrent recovery cannot overwrite a cache that has already been restored.
14
+ - Fixed pricing for Devin (SWE-2, SWE-1.7, and GLM-5.2 High) and Kimi Code models when upstream discovery omits cost information. SWE-2 now reflects its promotional pricing through December 31, 2026, then switches to list pricing on January 1, 2027.
15
+ - Fixed pricing and chat routing for Devin Fusion models so composite models use their own headline rates and supported Fusion lanes connect directly instead of failing through an incompatible routing path.
16
+
17
+ ## [18.2.1] - 2026-09-15
18
+
19
+ ### Added
20
+
21
+ - DeepSeek V4.1 Flash (`deepseek-flash`) now accepts image inputs and resolves its reasoning metadata, name, and effort ladder from upstream instead of shipping as a text-only row without them.
22
+
23
+ ### Fixed
24
+
25
+ - Fixed Cerebras Qwen 3.8 reasoning control by enabling correct effort tiers and disable behavior
26
+ - Fixed Cerebras Qwen models using incorrect reasoning format, enabling proper support for thinking modes
27
+ - Fixed Command Code's `deepseek/deepseek-v4.1-flash` row exposing its documented low/high/max thinking levels and image input, and made live discovery resolve the reasoning contract its rules declare ([#1666](https://github.com/can1357/oh-my-pi/issues/1666), [#11703](https://github.com/can1357/oh-my-pi/pull/11703) by [@aliefe04](https://github.com/aliefe04)).
28
+ - Fixed Meta Model API and Muse Code requests failing with 400 whenever omp forced a tool choice: `api.meta.ai/v1` accepts only `tool_choice: "auto"`, so `none`, `required`, and named choices (subagent final-retry `yield`, forced tools, structured output, compaction handoff) are now omitted instead of sent ([#11635](https://github.com/can1357/oh-my-pi/pull/11635) by [@quantmind-br](https://github.com/quantmind-br)).
29
+ - Bedrock's Qwen rows no longer ask for more output tokens than the model accepts, which Bedrock rejected with a 400. ([#12117](https://github.com/can1357/oh-my-pi/pull/12117) by [@Huang-404-Q](https://github.com/Huang-404-Q))
30
+ - Gemini 2.5 Flash Lite on Vertex AI no longer requests `maxOutputTokens=65536`, which the endpoint rejects with a 400; the output cap is clamped to 65535 ([#10595](https://github.com/can1357/oh-my-pi/pull/10595) by [@WeMingT](https://github.com/WeMingT)).
31
+ - Fixed Meta muse-spark models on OpenRouter wedging every turn with `400 Referenced reasoning item ... was not found or has expired`: replayed Responses reasoning history is now filtered for the muse-spark family, matching the existing Anthropic-on-OpenRouter treatment ([#10675](https://github.com/can1357/oh-my-pi/pull/10675) by [@Giardi77](https://github.com/Giardi77)).
32
+ - Fixed Ollama Cloud model discovery synthesizing a generic `minimal`/`low`/`medium`/`high` effort ladder for every thinking-capable model, which shadowed the per-model compat rules and made `max` unreachable on the DeepSeek V4 line (including the served `deepseek-v4.1-flash`, `deepseek-v4-flash:0731`, and `deepseek-v4-pro:0813` ids): discovery now leaves the ladder to the rule tree, so those models advertise the wire-exact `low`/`high`/`max` and GLM-5.3 exposes `low`/`high`/`max` ([#8334](https://github.com/can1357/oh-my-pi/issues/8334)).
33
+ - Fixed Meta Model API and Muse Code requests failing with 400 whenever omp forced a tool choice: `api.meta.ai/v1` accepts only `tool_choice: "auto"`, so `none`, `required`, and named choices (subagent final-retry `yield`, forced tools, structured output, compaction handoff) are now omitted instead of sent.
34
+ - Fixed Azure GPT-6 Astra Chat Completions policy to disable reasoning with function tools and encode thinking-off as `reasoning_effort: "none"` ([#11052](https://github.com/can1357/oh-my-pi/issues/11052)).
35
+ - Fixed Z.AI and Zhipu timezone-naive quota reset timestamps resolving eight hours late by declaring their UTC+8 reset timezone ([#11014](https://github.com/can1357/oh-my-pi/issues/11014)).
36
+ - Fixed Amazon Bedrock Claude Fable 5.1 effort metadata to expose `xhigh` and `max` instead of the unsupported `minimal` level. ([#10788](https://github.com/can1357/oh-my-pi/pull/10788) by [@voonfoo](https://github.com/voonfoo))
37
+ - LiteLLM discovery no longer exposes known task-specific models, including embedding, media, moderation, reranking, and search models, as coding models.
38
+ - Fixed custom `anthropic-messages` providers whose adaptive effort ladder exposes `minimal` sending `output_config.effort: "minimal"`, which the Anthropic Messages API rejects with `400 level "minimal" not supported`; the adaptive effort mapper now clamps `minimal` to `low` ([#10994](https://github.com/can1357/oh-my-pi/issues/10994)).
39
+ - Qwen 3.8 models on Alibaba Token Plan now send the selected reasoning effort and replay reasoning history across turns.
40
+ - Fixed DeepInfra model cost reporting so promotional pricing is reflected: the `metadata.discount` fraction is now applied to input, output, and cache-read rates ([#10935](https://github.com/can1357/oh-my-pi/issues/10935)).
41
+ - Fixed LiteLLM model discovery leaking ClinePass display names and pricing into models with colliding ids ([#10932](https://github.com/can1357/oh-my-pi/issues/10932)).
42
+ - Raised Cursor context windows to Cursor's documented sizes (Grok 4.5/4.6 256k, default/Auto 256k, Kimi K2.7 Code 262k, GPT-5.6 272k, Claude Opus 5 and Fable 300k by default) so compaction no longer fires too early, without lowering existing 1M Max-mode windows.
43
+
5
44
  ## [18.2.0] - 2026-09-15
6
45
 
7
46
  ### Breaking Changes
@@ -29,6 +29,8 @@ export declare function cursorModelParameters(model: string): readonly {
29
29
  * preserve quota semantics for newly discovered ids.
30
30
  */
31
31
  export declare function quotaTierFor(provider: string, model: string): string | undefined;
32
+ /** UTC offset for a provider's timezone-naive absolute retry-reset timestamps. */
33
+ export declare function retryResetTimezoneOffsetFor(provider: string): string | undefined;
32
34
  /** Whether a provider has catalog-authored model quota scopes. */
33
35
  export declare function hasQuotaTierPolicy(provider: string): boolean;
34
36
  /** The provider-default wire model for a model-less hosted operation. */
@@ -55,11 +57,20 @@ export declare function modelLimitsFor(provider: string, model: string): {
55
57
  } | undefined;
56
58
  /** Whether a provider roster entry is a declared non-chat/unsupported SKU. */
57
59
  export declare function isExcludedModel(provider: string, model: string): boolean;
60
+ /** Whether an exact upstream discovery mode is excluded from a provider's coding-model roster. */
61
+ export declare function isExcludedDiscoveryMode(provider: string, mode: string): boolean;
58
62
  /**
59
63
  * Whether a provider is retired: its entire roster is excluded from the
60
64
  * generated bundle and must never be resurrected from the previous snapshot.
61
65
  */
62
66
  export declare function isRetiredProvider(provider: string): boolean;
67
+ /**
68
+ * Whether a provider's bundled rows may seed cross-provider bare-id enrichment
69
+ * references. Gateway-namespaced providers (ClinePass) are isolated so their
70
+ * limits, pricing, and reasoning controls never contaminate an unrelated proxy
71
+ * model that happens to advertise the same bare id.
72
+ */
73
+ export declare function isBareIdReferenceProvider(provider: string): boolean;
63
74
  /** The declared subscription tier required to use a provider model id, if any. */
64
75
  export declare function planRequirementFor(provider: string, model: string): string | undefined;
65
76
  /** Cross-provider pricing-peer resolution for one provider model id. */
@@ -30,3 +30,13 @@ export declare function resolveCascade(target: ResolveTarget): ResolvedAxes;
30
30
  * mutable rule data. Bundled target lookups use {@link resolveCascade}.
31
31
  */
32
32
  export declare function resolveCascadeRules(cascade: CompiledCascade, target: ResolveTarget): ResolvedAxes;
33
+ /**
34
+ * Whether the effort ladder this target resolves to comes from a rule scoped
35
+ * to the model's identity (a recognized class, family, revision, or an explicit
36
+ * model selector), rather than a provider/api-wide or fallback unknown-class
37
+ * rule that any unrecognized id at that provider inherits.
38
+ *
39
+ * Discovery reads this to tell reviewed tiers apart from a blanket default, so
40
+ * catalog-published tiers can correct the latter and never the former.
41
+ */
42
+ export declare function hasModelScopedEffortsRule(target: ResolveTarget): boolean;
@@ -124,6 +124,14 @@ export declare function deriveThinkingPairFamilies<TSpec extends VariantSpecLike
124
124
  * unrelated carriers (GitHub Copilot `-1m` context variants) never match.
125
125
  */
126
126
  export declare function isCollapsedVariantSpec(spec: VariantSpecLike): boolean;
127
+ /**
128
+ * Whether a Cursor wire id names an extended tier that upstream serves only in
129
+ * max mode. The compiled taxonomy identifies `xhigh`/`extra-high`/`max`
130
+ * efforts and their optional service lanes. This is an inference, not an
131
+ * upstream marker: it is the only per-tier signal available for bundled rows
132
+ * and for routes live discovery never advertised.
133
+ */
134
+ export declare function isCursorMaxModeWireId(wireModelId: string): boolean;
127
135
  /**
128
136
  * Collapse a full mixed-provider list: per provider, the compiled reviewed table, Cursor's
129
137
  * conservative live effort-sibling rule, and the automatic `X`/`X-thinking`
@@ -13,3 +13,13 @@ export interface ResolvedModelPolicy<TApi extends Api = Api> {
13
13
  * complete compat record, thinking metadata, and catalog-data corrections.
14
14
  */
15
15
  export declare function resolveModelPolicy<TApi extends Api>(spec: ModelSpec<TApi>): ResolvedModelPolicy<TApi>;
16
+ /**
17
+ * Whether reviewed rules know THIS model's effort ladder, as opposed to it
18
+ * inheriting a provider-wide default or {@link resolveThinkingPolicy} falling
19
+ * through to the neutral wire ladder.
20
+ *
21
+ * Discovery uses this to tell "omp knows this model's tiers" apart from "omp
22
+ * is guessing them", so catalog-published tiers can correct the guess without
23
+ * ever overriding reviewed knowledge.
24
+ */
25
+ export declare function hasModelScopedEffortLadder<TApi extends Api>(spec: ModelSpec<TApi>): boolean;
@@ -272,6 +272,11 @@ export interface CompiledExcludeModels {
272
272
  provider: string;
273
273
  match: CompiledMatchList;
274
274
  }
275
+ /** Exact upstream discovery modes excluded from one provider's coding-model roster. */
276
+ export interface CompiledExcludeDiscoveryModes {
277
+ provider: string;
278
+ modes: string[];
279
+ }
275
280
  /** Provider plan-requirement tiers keyed by matcher token lists. */
276
281
  export interface CompiledPlanRequirement {
277
282
  provider: string;
@@ -289,6 +294,11 @@ export interface CompiledPricingPeer {
289
294
  peerId: string;
290
295
  }[];
291
296
  }
297
+ /** Provider timezone assumption for offset-less absolute retry-reset timestamps. */
298
+ export interface CompiledRetryResetTimezone {
299
+ provider: string;
300
+ offset: string;
301
+ }
292
302
  /** Compiled runtime behavior vocabulary (`runtime/behavior.kdl`). */
293
303
  export interface CompiledBehavior {
294
304
  openaiResponsesHeuristic?: CompiledResponsesHeuristic;
@@ -299,10 +309,13 @@ export interface CompiledBehavior {
299
309
  hostedDefaults: CompiledHostedDefault[];
300
310
  apiRoutes: CompiledApiRoutes[];
301
311
  modelLimits: CompiledModelLimits[];
312
+ excludeDiscoveryModes: CompiledExcludeDiscoveryModes[];
302
313
  excludeModels: CompiledExcludeModels[];
303
314
  planRequirements: CompiledPlanRequirement[];
304
315
  pricingPeers: CompiledPricingPeer[];
316
+ retryResetTimezones: CompiledRetryResetTimezone[];
305
317
  retiredProviders: string[];
318
+ referenceIsolatedProviders: string[];
306
319
  }
307
320
  /**
308
321
  * A string setting from `auth/*.kdl` that may be overridden by environment
@@ -647,4 +660,12 @@ export interface ResolvedAxes {
647
660
  wire: Record<string, unknown>;
648
661
  thinking: Record<string, unknown>;
649
662
  catalog: Record<string, unknown>;
663
+ /**
664
+ * Reasoning capability after the exact-model effort upgrade: `true` when the
665
+ * target reported reasoning or an exact rule declares a ladder for it (the
666
+ * reviewed correction to metadata-less discovery rows). Compat resolvers
667
+ * read this instead of the raw spec flag, or one id resolves two different
668
+ * wire contracts depending on whether it came from discovery or the bake.
669
+ */
670
+ reasoning: boolean;
650
671
  }
@@ -1,3 +1,4 @@
1
+ import type { FetchImpl } from "@oh-my-pi/pi-utils";
1
2
  import { type VariantCollapseTable } from "../compat/collapse.js";
2
3
  import type { ModelSpec } from "../types.js";
3
4
  export declare const ANTIGRAVITY_PRIMARY_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
@@ -37,6 +38,7 @@ export interface AntigravityDiscoveryAgentModelSort {
37
38
  export interface AntigravityDiscoveryApiResponse {
38
39
  models?: Record<string, AntigravityDiscoveryApiModel>;
39
40
  agentModelSorts?: AntigravityDiscoveryAgentModelSort[];
41
+ imageGenerationModelIds?: string[];
40
42
  }
41
43
  /**
42
44
  * Options for fetching Antigravity discovery models.
@@ -53,7 +55,7 @@ export interface FetchAntigravityDiscoveryModelsOptions {
53
55
  /** Optional abort signal for request cancellation. */
54
56
  signal?: AbortSignal;
55
57
  /** Optional fetch implementation override for tests. */
56
- fetcher?: typeof fetch;
58
+ fetcher?: FetchImpl;
57
59
  /**
58
60
  * Hand collapse table to apply to the discovered list. Defaults to the
59
61
  * Antigravity (budget-transport) table; `googleGeminiCli` passes the
@@ -68,3 +70,10 @@ export interface FetchAntigravityDiscoveryModelsOptions {
68
70
  * Returns `[]` only when the endpoint responds successfully with no usable models.
69
71
  */
70
72
  export declare function fetchAntigravityDiscoveryModels(options: FetchAntigravityDiscoveryModelsOptions): Promise<ModelSpec<"google-gemini-cli">[] | null>;
73
+ /** Advertised image model and serving endpoint for one Antigravity account. */
74
+ export interface AntigravityImageModel {
75
+ id: string;
76
+ endpoint: string;
77
+ }
78
+ /** Resolves the first image-generation model advertised by an Antigravity account. */
79
+ export declare function fetchAntigravityImageModel(options: FetchAntigravityDiscoveryModelsOptions): Promise<AntigravityImageModel | null>;
@@ -35,6 +35,13 @@ export declare function mapEffortToGoogleThinkingLevel<TApi extends Api>(effort:
35
35
  /**
36
36
  * Maps a normalized thinking effort to Anthropic adaptive effort values via
37
37
  * the model's baked `thinking.effortMap` (identity for unmapped efforts).
38
+ *
39
+ * The Anthropic adaptive wire vocabulary has no `minimal` tier (valid values:
40
+ * `low`, `medium`, `high`, `xhigh`, `max`); a model ladder that exposes
41
+ * `minimal` — custom `anthropic-messages` providers do, unlike the built-in
42
+ * Claude ladders — would otherwise serialize `output_config.effort: "minimal"`
43
+ * and 400 (`level "minimal" not supported`). Clamp it to the lowest real tier,
44
+ * mirroring {@link mapEffortToGoogleThinkingLevel}'s `minimal` handling.
38
45
  */
39
46
  export declare function mapEffortToAnthropicAdaptiveEffort<TApi extends Api>(model: ApiModel<TApi>, effort: Effort): "low" | "medium" | "high" | "xhigh" | "max" | "adaptive";
40
47
  /**
@@ -523,6 +523,8 @@ export interface FetchLiteLLMRichModelsOptions<TApi extends Api> {
523
523
  }
524
524
  export declare const OPENAI_COMPAT_DISCOVERY_DEFAULT_CONTEXT_WINDOW = 128000;
525
525
  export declare const OPENAI_COMPAT_DISCOVERY_DEFAULT_MAX_TOKENS = 32768;
526
+ /** Exclude only known non-conversational modes; unknown and non-string modes remain selectable for aliases. */
527
+ export declare function isSelectableLiteLLMModelMode(mode: unknown): boolean;
526
528
  export declare function normalizeLiteLLMManagementBaseUrl(baseUrl: string): string;
527
529
  export declare function resolveLiteLLMApi(entry: Record<string, unknown> | undefined, id: string, fallbackApi?: Api): Api;
528
530
  export declare function fetchLiteLLMRichModels<TApi extends Api>(options: FetchLiteLLMRichModelsOptions<TApi>): Promise<ModelSpec<TApi>[] | null>;
@@ -278,6 +278,12 @@ export interface OpenAICompat {
278
278
  * Default: auto-detected (DeepSeek reasoning models).
279
279
  */
280
280
  disableReasoningOnToolChoice?: boolean;
281
+ /**
282
+ * Disable reasoning whenever the request advertises function tools.
283
+ * Use for model surfaces that reject every tools-plus-reasoning combination.
284
+ * Default: false.
285
+ */
286
+ disableReasoningWithTools?: boolean;
281
287
  /** OpenRouter-specific routing preferences. Only used when baseUrl points to OpenRouter. */
282
288
  openRouterRouting?: OpenRouterRouting;
283
289
  /** Vercel AI Gateway routing preferences. Only used when baseUrl points to Vercel AI Gateway. */
@@ -372,12 +378,24 @@ export interface OpenAICompat {
372
378
  strictResponsesPairing?: boolean;
373
379
  /** Whether the Responses API accepts the `detail: "original"` image hint. Default: auto-detected (false for GitHub Copilot, which rejects it with a 400). */
374
380
  supportsImageDetailOriginal?: boolean;
381
+ /**
382
+ * Whether the Responses endpoint accepts `configuration_update` input items
383
+ * that change `reasoning.effort` mid-conversation while the request-level
384
+ * effort stays pinned for prompt caching (GPT-6 Astra). Default:
385
+ * rule-detected (`true` for `gpt-6-astra` on any host, `false` otherwise).
386
+ * Set `false` for custom `openai-responses` / `openai-codex-responses`
387
+ * endpoints that reject the item type with HTTP 400; effort changes are then
388
+ * sent as the top-level `reasoning.effort`.
389
+ */
390
+ supportsConfigurationUpdate?: boolean;
375
391
  /** Whether streamed reasoning deltas for the same field may repeat the full cumulative text snapshot. Default: false. */
376
392
  reasoningDeltasMayBeCumulative?: boolean;
377
393
  /** Strip leaked DeepSeek chat-template special tokens from visible content deltas. Default: auto-detected. */
378
394
  stripDeepseekSpecialTokens?: boolean;
379
395
  /** Heal leaked chat-template/tool-call/thinking markup from visible content deltas. Default: auto-detected. */
380
396
  streamMarkupHealingPattern?: OpenAIStreamMarkupHealingPattern;
397
+ /** Whether this wire may revise already-streamed text (`stream-revision` axis). Unassigned: append-only. */
398
+ streamRevision?: "none" | "possible";
381
399
  /** Treat an empty length-finished stream as a context-window error. Default: auto-detected. */
382
400
  emptyLengthFinishIsContextError?: boolean;
383
401
  /** Normalize tool call ids to OpenAI's 40-character limit. Default: auto-detected. */
@@ -548,6 +566,8 @@ export interface AnthropicCompat {
548
566
  export interface BedrockCompat {
549
567
  /** Whether this endpoint accepts no checkpoints, automatic caching, or explicit cachePoint blocks. */
550
568
  promptCacheMode?: "none" | "automatic" | "explicit";
569
+ /** Whether this wire may revise already-streamed text (`stream-revision` axis). Unassigned: append-only. */
570
+ streamRevision?: "none" | "possible";
551
571
  /** Whether explicit cachePoint blocks accept `ttl: "1h"`; omitted TTL means Bedrock's 5-minute default. */
552
572
  supportsLongPromptCacheRetention?: boolean;
553
573
  /**
@@ -571,6 +591,8 @@ export interface BedrockCompat {
571
591
  /** Fully-resolved Bedrock Converse prompt-cache capabilities, materialized once by `buildModel`. */
572
592
  export interface ResolvedBedrockCompat {
573
593
  promptCacheMode: NonNullable<BedrockCompat["promptCacheMode"]>;
594
+ /** See {@link BedrockCompat.streamRevision}. */
595
+ streamRevision?: BedrockCompat["streamRevision"];
574
596
  supportsLongPromptCacheRetention: boolean;
575
597
  promptCacheMinimumTokens: number;
576
598
  promptCacheMaximumCheckpoints: number;
@@ -632,6 +654,7 @@ export interface ResolvedOpenAISharedCompat {
632
654
  filterReasoningHistory: boolean;
633
655
  disableReasoningOnForcedToolChoice: boolean;
634
656
  disableReasoningOnToolChoice: boolean;
657
+ disableReasoningWithTools?: boolean;
635
658
  supportsToolChoice: boolean;
636
659
  supportsForcedToolChoice: boolean;
637
660
  supportsNamedToolChoice: boolean;
@@ -649,6 +672,8 @@ export interface ResolvedOpenAISharedCompat {
649
672
  requiresAssistantContentForToolCalls: boolean;
650
673
  stripDeepseekSpecialTokens: boolean;
651
674
  streamMarkupHealingPattern?: OpenAIStreamMarkupHealingPattern;
675
+ /** See {@link OpenAICompat.streamRevision}. */
676
+ streamRevision?: OpenAICompat["streamRevision"];
652
677
  /** See {@link OpenAICompat.streamFirstEventTimeoutMs}. */
653
678
  streamFirstEventTimeoutMs?: number;
654
679
  reasoningDeltasMayBeCumulative: boolean;
@@ -689,7 +714,7 @@ export interface ResolvedOpenAISharedCompat {
689
714
  * `buildModel`; request handlers read fields and never detect, resolve, or
690
715
  * allocate.
691
716
  */
692
- export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat & Required<Omit<OpenAICompat, "supportsDeveloperRole" | "supportsReasoningEffort" | "reasoningEffortMap" | "supportsReasoningParams" | "supportsReasoningSummary" | "supportsSamplingParams" | "supportsPenaltyAndStopParams" | "thinkingFormat" | "kimiApiFormat" | "reasoningDisableMode" | "omitReasoningEffort" | "includeEncryptedReasoning" | "filterReasoningHistory" | "disableReasoningOnForcedToolChoice" | "disableReasoningOnToolChoice" | "supportsToolChoice" | "supportsForcedToolChoice" | "supportsNamedToolChoice" | "reasoningContentField" | "requiresReasoningContentForToolCalls" | "requiresReasoningContentForAllAssistantTurns" | "allowsSyntheticReasoningContentForToolCalls" | "replayReasoningContent" | "qwenPreserveThinking" | "qwenTemplateReasoningEffort" | "requiresThinkingAsText" | "requiresMistralToolIds" | "requiresToolResultName" | "requiresAssistantAfterToolResult" | "requiresAssistantContentForToolCalls" | "stripDeepseekSpecialTokens" | "streamMarkupHealingPattern" | "reasoningDeltasMayBeCumulative" | "emptyLengthFinishIsContextError" | "usesOpenAIToolCallIdLimit" | "promptCacheSessionHeader" | "supportsPromptCacheBreakpoints" | "promptCacheBreakpointTtl" | "openRouterRouting" | "isOpenRouterHost" | "supportsStrictMode" | "supportsLongPromptCacheRetention" | "alwaysSendMaxTokens" | "clampOutputToModelMax" | "wireModelIdMode" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode" | "toolSchemaFlavor" | "streamFirstEventTimeoutMs" | "streamIdleTimeoutMs" | "cacheControlFormat" | "thinkingKeep" | "strictResponsesPairing" | "supportsImageDetailOriginal" | "stripImageInput" | "thinkingLoopGuard" | "whenThinking">> & {
717
+ export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat & Required<Omit<OpenAICompat, "supportsDeveloperRole" | "supportsReasoningEffort" | "reasoningEffortMap" | "supportsReasoningParams" | "supportsReasoningSummary" | "supportsSamplingParams" | "supportsPenaltyAndStopParams" | "thinkingFormat" | "kimiApiFormat" | "reasoningDisableMode" | "omitReasoningEffort" | "includeEncryptedReasoning" | "filterReasoningHistory" | "disableReasoningOnForcedToolChoice" | "disableReasoningOnToolChoice" | "disableReasoningWithTools" | "supportsToolChoice" | "supportsForcedToolChoice" | "supportsNamedToolChoice" | "reasoningContentField" | "requiresReasoningContentForToolCalls" | "requiresReasoningContentForAllAssistantTurns" | "allowsSyntheticReasoningContentForToolCalls" | "replayReasoningContent" | "qwenPreserveThinking" | "qwenTemplateReasoningEffort" | "requiresThinkingAsText" | "requiresMistralToolIds" | "requiresToolResultName" | "requiresAssistantAfterToolResult" | "requiresAssistantContentForToolCalls" | "stripDeepseekSpecialTokens" | "streamMarkupHealingPattern" | "reasoningDeltasMayBeCumulative" | "emptyLengthFinishIsContextError" | "usesOpenAIToolCallIdLimit" | "promptCacheSessionHeader" | "supportsPromptCacheBreakpoints" | "promptCacheBreakpointTtl" | "openRouterRouting" | "isOpenRouterHost" | "supportsStrictMode" | "supportsLongPromptCacheRetention" | "alwaysSendMaxTokens" | "clampOutputToModelMax" | "wireModelIdMode" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode" | "toolSchemaFlavor" | "streamFirstEventTimeoutMs" | "streamIdleTimeoutMs" | "streamRevision" | "cacheControlFormat" | "thinkingKeep" | "strictResponsesPairing" | "supportsImageDetailOriginal" | "supportsConfigurationUpdate" | "stripImageInput" | "thinkingLoopGuard" | "whenThinking">> & {
693
718
  vercelGatewayRouting?: OpenAICompat["vercelGatewayRouting"];
694
719
  extraBody?: OpenAICompat["extraBody"];
695
720
  cacheControlFormat?: OpenAICompat["cacheControlFormat"];
@@ -988,6 +1013,16 @@ export interface Model<TApi extends Api = Api> {
988
1013
  gitlabDuoWorkflowRootNamespaceId?: string;
989
1014
  /** Cursor `max_mode` request flag returned by `GetUsableModels` for premium models that require max mode. */
990
1015
  cursorMaxMode?: boolean;
1016
+ /**
1017
+ * Per-wire-id `max_mode` markers for the members a collapsed Cursor row
1018
+ * routes to, recorded by `collapseVariants` from live `GetUsableModels`
1019
+ * rows. {@link cursorMaxMode} on a collapsed row is an OR across members,
1020
+ * so it cannot tell a `-low` route that needs no max mode from an Opus
1021
+ * `-fast` route that does; transports look the routed wire id up here
1022
+ * first. Absent on raw rows (their own `cursorMaxMode` already describes
1023
+ * their single wire id) and on bundled snapshots that predate discovery.
1024
+ */
1025
+ cursorMaxModeRoutes?: Readonly<Record<string, boolean>>;
991
1026
  cost: ModelCost;
992
1027
  /** Premium Copilot requests charged per user-initiated request (defaults to 1). */
993
1028
  premiumMultiplier?: number;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-catalog",
4
- "version": "18.2.0",
4
+ "version": "18.2.2",
5
5
  "description": "Model catalog for omp: bundled model database, provider discovery descriptors, model identity, classification, and equivalence",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Stencil Labs, Inc.",
@@ -35,12 +35,12 @@
35
35
  "gen:proto": "bun scripts/generate-protocols.ts"
36
36
  },
37
37
  "dependencies": {
38
- "@oh-my-pi/omptype": "18.2.0",
39
- "@oh-my-pi/pi-utils": "18.2.0"
38
+ "@oh-my-pi/omptype": "18.2.2",
39
+ "@oh-my-pi/pi-utils": "18.2.2"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@bgotink/kdl": "0.4.0",
43
- "@oh-my-pi/pi-ai": "18.2.0",
43
+ "@oh-my-pi/pi-ai": "18.2.2",
44
44
  "@types/bun": "^1.3.14"
45
45
  },
46
46
  "engines": {
package/src/build.ts CHANGED
@@ -24,6 +24,41 @@ function objectPayload(value: unknown): object | undefined {
24
24
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
25
25
  }
26
26
 
27
+ /**
28
+ * Overwrite seeded fallback rates with the latest dated card whose
29
+ * `effectiveFrom` is already due. Leaves the seed unchanged when no card
30
+ * has started, and never attaches a `timeBased` tariff.
31
+ */
32
+ function applyEffectiveFallbackRates(
33
+ cost: { input: number; output: number; cacheRead: number; cacheWrite: number },
34
+ effectiveRates: unknown,
35
+ now = Date.now(),
36
+ ): void {
37
+ const rates = objectPayload(effectiveRates);
38
+ if (rates === undefined) return;
39
+ let latestFrom = Number.NEGATIVE_INFINITY;
40
+ let latest: object | undefined;
41
+ for (const entry of Object.values(rates)) {
42
+ const payload = objectPayload(entry);
43
+ if (payload === undefined) continue;
44
+ const date = Reflect.get(payload, "effectiveFrom");
45
+ if (typeof date !== "string") continue;
46
+ const from = Date.parse(date);
47
+ if (!Number.isFinite(from) || from > now || from < latestFrom) continue;
48
+ latestFrom = from;
49
+ latest = payload;
50
+ }
51
+ if (latest === undefined) return;
52
+ const input = numberField(latest, "input");
53
+ if (input !== undefined) cost.input = input;
54
+ const output = numberField(latest, "output");
55
+ if (output !== undefined) cost.output = output;
56
+ const cacheRead = numberField(latest, "cacheRead");
57
+ if (cacheRead !== undefined) cost.cacheRead = cacheRead;
58
+ const cacheWrite = numberField(latest, "cacheWrite");
59
+ if (cacheWrite !== undefined) cost.cacheWrite = cacheWrite;
60
+ }
61
+
27
62
  /** Narrow a compiled `input-modalities` axis value to the model input union. */
28
63
  function isInputModalities(value: unknown): value is ("text" | "image")[] {
29
64
  return Array.isArray(value) && value.every(entry => entry === "text" || entry === "image");
@@ -139,6 +174,30 @@ export function applyCatalogCorrections(
139
174
  const cacheWrite = numberField(patch, "cacheWrite");
140
175
  if (cacheWrite !== undefined) model.cost.cacheWrite = cacheWrite;
141
176
  }
177
+ const fallback = objectPayload(catalog.costFallback);
178
+ if (fallback !== undefined) {
179
+ const base = model.cost;
180
+ const hasTokenPrice = base.input !== 0 || base.output !== 0 || base.cacheRead !== 0 || base.cacheWrite !== 0;
181
+ if (!hasTokenPrice) {
182
+ // Upstream reported no token price (plan-included or promo-free
183
+ // rows): seed the reviewed list price instead of overwriting real
184
+ // discovery data the way `cost-patch` would.
185
+ model.cost = { ...model.cost };
186
+ const input = numberField(fallback, "input");
187
+ if (input !== undefined) model.cost.input = input;
188
+ const output = numberField(fallback, "output");
189
+ if (output !== undefined) model.cost.output = output;
190
+ const cacheRead = numberField(fallback, "cacheRead");
191
+ if (cacheRead !== undefined) model.cost.cacheRead = cacheRead;
192
+ const cacheWrite = numberField(fallback, "cacheWrite");
193
+ if (cacheWrite !== undefined) model.cost.cacheWrite = cacheWrite;
194
+ // Dated fallback rates overwrite the seeded numbers when they have
195
+ // already taken effect. They are not a recurring tariff: wrapping
196
+ // them in `timeBased` with empty peak windows would report
197
+ // permanent off-peak and never wake at the dated boundary.
198
+ applyEffectiveFallbackRates(model.cost, Reflect.get(fallback, "effectiveRates"));
199
+ }
200
+ }
142
201
  if (catalog.timeBased !== undefined) {
143
202
  model.cost = { ...model.cost, timeBased: materializeTimeBasedCost(catalog.timeBased) };
144
203
  }
@@ -93,6 +93,7 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
93
93
  "clamp-output-to-model-max": wire("clampOutputToModelMax", OAI),
94
94
  "disable-reasoning-on-forced-tool-choice": wire("disableReasoningOnForcedToolChoice", OAI),
95
95
  "disable-reasoning-on-tool-choice": wire("disableReasoningOnToolChoice", OAI),
96
+ "disable-reasoning-with-tools": wire("disableReasoningWithTools", ["openai"]),
96
97
  "drop-thinking-when-reasoning-effort": wire("dropThinkingWhenReasoningEffort", ["openai"]),
97
98
  "empty-length-finish-is-context-error": wire("emptyLengthFinishIsContextError", OAI),
98
99
  "extra-body": { ...wire("extraBody", ["openai"], "object"), verbatimKeys: true },
@@ -235,6 +236,19 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
235
236
  "supports-function-part-id": wire("supportsFunctionPartId", ["google"]),
236
237
 
237
238
  // ── wire: shared across surfaces ──
239
+ /**
240
+ * Whether this wire may revise text it has already streamed: bytes
241
+ * reclassified out of the visible channel (a leaned-on thinking opener),
242
+ * carved into a tool call, reordered by content-block index, or replaced
243
+ * wholesale by an authoritative final payload. Unassigned means the wire
244
+ * only appends, so the transcript may retire finished lines into native
245
+ * scrollback while the turn is still streaming (see
246
+ * `AssistantMessageComponent`). Declare `possible` only with a citable
247
+ * mechanism: the renderer also verifies published rows every frame and stops
248
+ * retiring the block on the first mismatch, so this axis decides where
249
+ * mid-stream retirement is attempted, not whether it is safe.
250
+ */
251
+ "stream-revision": wire("streamRevision", [...OAI, "bedrock"], "scalar", ["none", "possible"]),
238
252
  "stream-first-event-timeout-ms": wire("streamFirstEventTimeoutMs", [...OAI, "google"]),
239
253
  "stream-idle-timeout-ms": wire("streamIdleTimeoutMs", [...OAI, "anthropic", "bedrock", "google"]),
240
254
  "strip-image-input": wire("stripImageInput", [...OAI, "anthropic", "google"]),
@@ -274,6 +288,7 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
274
288
  "context-promotion-target": { key: "contextPromotionTarget", set: "catalog", shape: "scalar" },
275
289
  "context-window-floor": { key: "contextWindowFloor", set: "catalog", shape: "scalar" },
276
290
  "cost-patch": { key: "costPatch", set: "catalog", shape: "object" },
291
+ "cost-fallback": { key: "costFallback", set: "catalog", shape: "object" },
277
292
  "delegation-bias": { key: "delegationBias", set: "catalog", shape: "scalar", values: DELEGATION_BIASES },
278
293
  "edit-prompt-variant": { key: "editPromptVariant", set: "catalog", shape: "scalar", values: ["full", "compact"] },
279
294
  "edit-revision": { key: "editRevision", set: "catalog", shape: "scalar" },
@@ -2,8 +2,8 @@
2
2
  * Typed accessors over the compiled runtime-behavior vocabulary
3
3
  * (`rules/runtime/behavior.kdl`): provider/model heuristics that run before
4
4
  * or outside exact bundled-model lookup — responses routing, API routing,
5
- * quota tiers, plan requirements, model limits, roster exclusions, hosted
6
- * defaults, and pricing peers.
5
+ * quota tiers, plan requirements, model limits, roster and discovery-mode
6
+ * exclusions, hosted defaults, and pricing peers.
7
7
  */
8
8
  import { globMatch } from "./cascade";
9
9
  import rules from "./rules.json";
@@ -103,6 +103,11 @@ export function quotaTierFor(provider: string, model: string): string | undefine
103
103
  return undefined;
104
104
  }
105
105
 
106
+ /** UTC offset for a provider's timezone-naive absolute retry-reset timestamps. */
107
+ export function retryResetTimezoneOffsetFor(provider: string): string | undefined {
108
+ return behavior.retryResetTimezones.find(rule => rule.provider === provider)?.offset;
109
+ }
110
+
106
111
  /** Whether a provider has catalog-authored model quota scopes. */
107
112
  export function hasQuotaTierPolicy(provider: string): boolean {
108
113
  return behavior.quotaTiers.some(rule => rule.provider === provider);
@@ -176,6 +181,11 @@ export function isExcludedModel(provider: string, model: string): boolean {
176
181
  return behavior.excludeModels.some(rule => rule.provider === provider && matchesList(rule.match, lower, lower));
177
182
  }
178
183
 
184
+ /** Whether an exact upstream discovery mode is excluded from a provider's coding-model roster. */
185
+ export function isExcludedDiscoveryMode(provider: string, mode: string): boolean {
186
+ return behavior.excludeDiscoveryModes.some(rule => rule.provider === provider && rule.modes.includes(mode));
187
+ }
188
+
179
189
  /**
180
190
  * Whether a provider is retired: its entire roster is excluded from the
181
191
  * generated bundle and must never be resurrected from the previous snapshot.
@@ -184,6 +194,16 @@ export function isRetiredProvider(provider: string): boolean {
184
194
  return behavior.retiredProviders.includes(provider);
185
195
  }
186
196
 
197
+ /**
198
+ * Whether a provider's bundled rows may seed cross-provider bare-id enrichment
199
+ * references. Gateway-namespaced providers (ClinePass) are isolated so their
200
+ * limits, pricing, and reasoning controls never contaminate an unrelated proxy
201
+ * model that happens to advertise the same bare id.
202
+ */
203
+ export function isBareIdReferenceProvider(provider: string): boolean {
204
+ return !behavior.referenceIsolatedProviders.includes(provider);
205
+ }
206
+
187
207
  /** The declared subscription tier required to use a provider model id, if any. */
188
208
  export function planRequirementFor(provider: string, model: string): string | undefined {
189
209
  const rule = behavior.planRequirements.find(candidate => candidate.provider === provider);
@@ -293,6 +293,7 @@ function cloneAxes(axes: ResolvedAxes): ResolvedAxes {
293
293
  wire: cloneAxisRecord(axes.wire),
294
294
  thinking: cloneAxisRecord(axes.thinking),
295
295
  catalog: cloneAxisRecord(axes.catalog),
296
+ reasoning: axes.reasoning,
296
297
  };
297
298
  }
298
299
 
@@ -324,9 +325,33 @@ export function resolveCascadeRules(cascade: CompiledCascade, target: ResolveTar
324
325
  return cloneAxes(resolveOverIndex(buildRuleIndex(cascade), target));
325
326
  }
326
327
 
328
+ /**
329
+ * Whether the effort ladder this target resolves to comes from a rule scoped
330
+ * to the model's identity (a recognized class, family, revision, or an explicit
331
+ * model selector), rather than a provider/api-wide or fallback unknown-class
332
+ * rule that any unrecognized id at that provider inherits.
333
+ *
334
+ * Discovery reads this to tell reviewed tiers apart from a blanket default, so
335
+ * catalog-published tiers can correct the latter and never the former.
336
+ */
337
+ export function hasModelScopedEffortsRule(target: ResolveTarget): boolean {
338
+ const winners: WinnerTable = {};
339
+ for (const { rule, rank } of rankRelevantRules(getRuleIndex(), prepareTarget(target))) {
340
+ contest(winners, rule.compiled.thinking, rank, rule, target);
341
+ }
342
+ const winner = winners.efforts?.rule.compiled;
343
+ if (winner === undefined) return false;
344
+ return (
345
+ (winner.class !== undefined && winner.class !== "unknown") ||
346
+ winner.family !== undefined ||
347
+ winner.revision !== undefined ||
348
+ winner.models !== undefined
349
+ );
350
+ }
351
+
327
352
  function resolveOverIndex(index: RuleIndex, target: ResolveTarget): ResolvedAxes {
328
353
  const ranked = rankRelevantRules(index, prepareTarget(target));
329
- let reasoning = target.reasoning;
354
+ let reasoning = target.reasoning === true;
330
355
  if (!reasoning) {
331
356
  for (const { rule, rank } of ranked) {
332
357
  if (rule.hasExactEffortsRule && rank[0] === 2) {
@@ -347,5 +372,6 @@ function resolveOverIndex(index: RuleIndex, target: ResolveTarget): ResolvedAxes
347
372
  wire: collect(wire, rule => rule.wire),
348
373
  thinking: collect(thinking, rule => rule.thinking),
349
374
  catalog: collect(catalog, rule => rule.catalog),
375
+ reasoning,
350
376
  };
351
377
  }