@oh-my-pi/pi-catalog 18.1.16 → 18.1.18

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 (36) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +20 -0
  3. package/dist/types/compat/auth-ids.d.ts +2 -2
  4. package/dist/types/models.d.ts +10 -6
  5. package/dist/types/pricing.d.ts +5 -0
  6. package/dist/types/provider-models/descriptor-types.d.ts +15 -0
  7. package/dist/types/provider-models/descriptors.d.ts +12 -0
  8. package/dist/types/provider-models/openai-compat.d.ts +25 -0
  9. package/dist/types/types.d.ts +43 -2
  10. package/dist/types/wire/github-copilot.d.ts +19 -0
  11. package/package.json +4 -4
  12. package/src/build.ts +16 -2
  13. package/src/compat/auth-ids.ts +2 -0
  14. package/src/compat/axes.ts +9 -0
  15. package/src/compat/resolve.ts +18 -4
  16. package/src/compat/rules/README.md +37 -0
  17. package/src/compat/rules/auth/_order.kdl +1 -1
  18. package/src/compat/rules/auth/commandcode.kdl +13 -0
  19. package/src/compat/rules/classes/anthropic.kdl +24 -0
  20. package/src/compat/rules/providers/amazon-bedrock.kdl +7 -0
  21. package/src/compat/rules/providers/anthropic.kdl +8 -0
  22. package/src/compat/rules/providers/commandcode.kdl +601 -0
  23. package/src/compat/rules/providers/deepseek.kdl +72 -1
  24. package/src/compat/rules/providers/opencode-go.kdl +10 -0
  25. package/src/compat/rules/providers/openrouter.kdl +4 -0
  26. package/src/compat/rules/runtime/behavior.kdl +5 -0
  27. package/src/compat/rules.json +2007 -133
  28. package/src/model-manager.ts +27 -13
  29. package/src/models.json +7512 -461
  30. package/src/models.ts +93 -19
  31. package/src/pricing.ts +136 -0
  32. package/src/provider-models/descriptor-types.ts +15 -0
  33. package/src/provider-models/descriptors.ts +15 -0
  34. package/src/provider-models/openai-compat.ts +91 -0
  35. package/src/types.ts +46 -1
  36. package/src/wire/github-copilot.ts +26 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.1.18] - 2026-09-11
6
+
7
+ ### Added
8
+
9
+ - `supports-server-compaction` compat axis (`compat.supportsServerCompaction`): whether a model line accepts Anthropic server-side compaction (`compact-2026-01-12`). Class rules enable it for Opus 4.6+, Sonnet 4.6+, and Fable/Mythos 5 on every Anthropic-messages host; the default is `false`.
10
+
11
+ ### Fixed
12
+
13
+ - OpenCode Go's DeepSeek Flash lanes (`deepseek-flash`, `deepseek-v4.1-flash`) now declare image input. The gateway serves them with vision despite the IDs carrying no vision suffix, so the class-wide `strip-image-input` rule was dropping attachments the endpoint reads; the modality is declared too, since live discovery seeds these lanes text-only ([#11774](https://github.com/can1357/oh-my-pi/pull/11774) by [@STRML](https://github.com/STRML)).
14
+ - Amazon Bedrock OpenAI models, plus unclassified profiles such as opaque application-inference-profile ARNs, now carry the compatibility policy required to preserve image-bearing tool results ([#11681](https://github.com/can1357/oh-my-pi/issues/11681)).
15
+ - DeepSeek V4.1 Flash requests now honor the documented 384K output maximum instead of being capped at 64K ([#11769](https://github.com/can1357/oh-my-pi/issues/11769)).
16
+
17
+ ## [18.1.17] - 2026-09-10
18
+
19
+ ### Added
20
+
21
+ - Added DeepSeek V4.1 Flash on OpenRouter with image input and low/high/max reasoning levels ([#11592](https://github.com/can1357/oh-my-pi/pull/11592) by [@mazzanfar](https://github.com/mazzanfar)).
22
+ - Added DeepSeek cost estimates that follow published peak/off-peak rates.
23
+ - Added dated, announced price changes to the catalog, so rates switch on their effective date (e.g. DeepSeek Pro moving to Flash rates).
24
+ - Added Command Code as a built-in provider with API-key login, live model discovery, per-model pricing, native OpenAI/Anthropic-compatible routing, cache-aware token usage, and TTFT metrics ([#11391](https://github.com/can1357/oh-my-pi/pull/11391) by [@CherkaSSH](https://github.com/CherkaSSH)).
25
+
26
+ ### Fixed
27
+
28
+ - Fixed Command Code models outside the verified effort registry offering unsupported reasoning effort controls, and bundled the live Command Code catalog so fresh installs resolve the default model without waiting for discovery ([#11595](https://github.com/can1357/oh-my-pi/pull/11595) by [@H4vC](https://github.com/H4vC)).
29
+ - Fixed the bundled `deepseek-flash` row shipping without context limits: it now carries its documented 1M context / 384K output so offline context accounting enforces the real window.
30
+
5
31
  ## [18.1.16] - 2026-09-09
6
32
 
7
33
  - Updated Fire Pass (`firepass`) login validation probe to `accounts/fireworks/routers/glm-5p2-fast` and bundled `glm-5.2-fast` and `kimi-k3-fast` models in place of decommissioned `kimi-k2.6-turbo` ([#10859](https://github.com/can1357/oh-my-pi/pull/10859) by [@olegpulatov](https://github.com/olegpulatov)).
package/README.md CHANGED
@@ -38,6 +38,26 @@ bun add @oh-my-pi/pi-catalog
38
38
 
39
39
  Ships TypeScript source directly (no build step); requires Bun ≥ 1.3.14.
40
40
 
41
+ ## Cost calculation
42
+
43
+ The `models` subpath (also exported from the root) provides timestamp-aware pricing helpers:
44
+
45
+ | API | Result |
46
+ | --- | --- |
47
+ | `calculateCost(model, usage, timestamp?)` | Updates and returns `usage.cost` using `model.cost`. |
48
+ | `calculateUsageCost(cost, usage, timestamp?)` | Updates and returns `usage.cost` using a `ModelCost`. |
49
+ | `calculateUncachedInputCost(cost, promptInputTokens, timestamp?)` | Returns the cost of a fully uncached prompt. |
50
+ | `getTimeBasedPricingPeriod(cost, timestamp?)` | Returns `"peak"`, `"off-peak"`, or `undefined` without a schedule. |
51
+ | `getNextTimeBasedPricingTransition(cost, timestamp?)` | Returns the next actual peak/off-peak change strictly after the timestamp, or `undefined` if none exists. |
52
+
53
+ Timestamps are Unix milliseconds; omitted timestamps use the current time for scheduled pricing. Flat token prices are unaffected. Pricing selects the latest applicable effective rate card, then its long-context tier, then the peak/off-peak multiplier. A transition query concerns the recurring tariff, not dated rate-card changes.
54
+
55
+ `ModelCost.timeBased` is optional typed metadata (`TimeBasedCost`): `offPeakMultiplier`, `peakWindows` (UTC `weekdays`, Sunday = 0, and start-inclusive/end-exclusive `startMinute`/`endMinute`), and optional `effectiveRates`. Each effective rate is a complete `TokenCost` with an `effectiveFrom` Unix-millisecond timestamp and optional `longContext` tier, replacing the base card from that instant.
56
+
57
+ Pass the request-start timestamp when estimating request usage, then preserve the resulting monetary amounts rather than repricing history at display time. OMP does this using the assistant message timestamp; it is an estimation convention, not a claim about server billing across boundaries. Prefer monetary costs reported by a provider when available.
58
+
59
+ Schedules are materialized from the [`time-based-cost` KDL axis](src/compat/rules/README.md#time-based-pricing); this does not add a `timeBased` input field to the coding agent's `models.yml`. See [user-facing pricing behavior](../../docs/models.md#usage-costs-and-time-based-pricing) for DeepSeek rates, dates, and footer indicators.
60
+
41
61
  ## References
42
62
 
43
63
  - [Monorepo README](https://github.com/can1357/oh-my-pi#readme)
@@ -1,4 +1,4 @@
1
1
  /** Every provider with an `auth/<id>.kdl` policy. */
2
- export type AuthProviderId = "abliteration" | "aiand" | "aimlapi" | "alibaba-coding-plan" | "alibaba-token-plan" | "amazon-bedrock" | "anthropic" | "azure" | "baseten" | "bedrock-mantle" | "cerebras" | "cline-pass" | "cloudflare-ai-gateway" | "coreweave" | "cursor" | "deepinfra" | "deepseek" | "devin" | "exa" | "firepass" | "fireworks" | "github-copilot" | "gitlab-duo" | "gitlab-duo-agent" | "gmi-cloud" | "google" | "google-antigravity" | "google-gemini-cli" | "google-vertex" | "groq" | "huggingface" | "kagi" | "kilo" | "kimi-code" | "litellm" | "llama.cpp" | "lm-studio" | "meta" | "minimax" | "minimax-code" | "minimax-code-cn" | "mistral" | "moonshot" | "muse-code" | "nanogpt" | "novita" | "nvidia" | "ollama" | "ollama-cloud" | "openai" | "openai-codex" | "openai-codex-device" | "opencode-go" | "opencode-zen" | "openrouter" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "sakana" | "siliconflow" | "siliconflow-cn" | "synthetic" | "tavily" | "together" | "umans" | "venice" | "vercel-ai-gateway" | "vllm" | "wafer-serverless" | "xai" | "xai-oauth" | "xiaomi" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "xiaomi-token-plan-sgp" | "yolo-auto" | "zai" | "zai-coding-plan" | "zenmux" | "zhipu-coding-plan";
2
+ export type AuthProviderId = "abliteration" | "aiand" | "aimlapi" | "alibaba-coding-plan" | "alibaba-token-plan" | "amazon-bedrock" | "anthropic" | "azure" | "baseten" | "bedrock-mantle" | "cerebras" | "cline-pass" | "cloudflare-ai-gateway" | "commandcode" | "coreweave" | "cursor" | "deepinfra" | "deepseek" | "devin" | "exa" | "firepass" | "fireworks" | "github-copilot" | "gitlab-duo" | "gitlab-duo-agent" | "gmi-cloud" | "google" | "google-antigravity" | "google-gemini-cli" | "google-vertex" | "groq" | "huggingface" | "kagi" | "kilo" | "kimi-code" | "litellm" | "llama.cpp" | "lm-studio" | "meta" | "minimax" | "minimax-code" | "minimax-code-cn" | "mistral" | "moonshot" | "muse-code" | "nanogpt" | "novita" | "nvidia" | "ollama" | "ollama-cloud" | "openai" | "openai-codex" | "openai-codex-device" | "opencode-go" | "opencode-zen" | "openrouter" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "sakana" | "siliconflow" | "siliconflow-cn" | "synthetic" | "tavily" | "together" | "umans" | "venice" | "vercel-ai-gateway" | "vllm" | "wafer-serverless" | "xai" | "xai-oauth" | "xiaomi" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "xiaomi-token-plan-sgp" | "yolo-auto" | "zai" | "zai-coding-plan" | "zenmux" | "zhipu-coding-plan";
3
3
  /** Providers whose policy declares a `login` flow (the `/login` roster). */
4
- export type LoginProviderId = "abliteration" | "aiand" | "alibaba-coding-plan" | "alibaba-token-plan" | "anthropic" | "baseten" | "cerebras" | "cline-pass" | "cloudflare-ai-gateway" | "coreweave" | "cursor" | "deepinfra" | "deepseek" | "devin" | "exa" | "firepass" | "fireworks" | "github-copilot" | "gitlab-duo" | "gitlab-duo-agent" | "gmi-cloud" | "google-antigravity" | "google-gemini-cli" | "huggingface" | "kagi" | "kilo" | "kimi-code" | "litellm" | "llama.cpp" | "lm-studio" | "meta" | "minimax-code" | "minimax-code-cn" | "moonshot" | "muse-code" | "nanogpt" | "novita" | "nvidia" | "ollama" | "ollama-cloud" | "openai-codex" | "openai-codex-device" | "opencode-go" | "opencode-zen" | "openrouter" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "sakana" | "siliconflow" | "siliconflow-cn" | "synthetic" | "tavily" | "together" | "umans" | "venice" | "vercel-ai-gateway" | "vllm" | "wafer-serverless" | "xai" | "xai-oauth" | "xiaomi" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "xiaomi-token-plan-sgp" | "yolo-auto" | "zai" | "zai-coding-plan" | "zenmux" | "zhipu-coding-plan";
4
+ export type LoginProviderId = "abliteration" | "aiand" | "alibaba-coding-plan" | "alibaba-token-plan" | "anthropic" | "baseten" | "cerebras" | "cline-pass" | "cloudflare-ai-gateway" | "commandcode" | "coreweave" | "cursor" | "deepinfra" | "deepseek" | "devin" | "exa" | "firepass" | "fireworks" | "github-copilot" | "gitlab-duo" | "gitlab-duo-agent" | "gmi-cloud" | "google-antigravity" | "google-gemini-cli" | "huggingface" | "kagi" | "kilo" | "kimi-code" | "litellm" | "llama.cpp" | "lm-studio" | "meta" | "minimax-code" | "minimax-code-cn" | "moonshot" | "muse-code" | "nanogpt" | "novita" | "nvidia" | "ollama" | "ollama-cloud" | "openai-codex" | "openai-codex-device" | "opencode-go" | "opencode-zen" | "openrouter" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "sakana" | "siliconflow" | "siliconflow-cn" | "synthetic" | "tavily" | "together" | "umans" | "venice" | "vercel-ai-gateway" | "vllm" | "wafer-serverless" | "xai" | "xai-oauth" | "xiaomi" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "xiaomi-token-plan-sgp" | "yolo-auto" | "zai" | "zai-coding-plan" | "zenmux" | "zhipu-coding-plan";
@@ -4,12 +4,16 @@ export type GeneratedProvider = keyof typeof MODELS;
4
4
  export declare function getBundledModel<TApi extends Api = Api>(provider: GeneratedProvider, modelId: string): Model<TApi>;
5
5
  export declare function getBundledProviders(): KnownProvider[];
6
6
  export declare function getBundledModels(provider: GeneratedProvider): Model<Api>[];
7
- /** Price a prompt as fully uncached input under its active context-length tier. */
8
- export declare function calculateUncachedInputCost(cost: ModelCost, promptInputTokens: number): number;
9
- /** Price one usage record from a token rate card, including active context tiers. */
10
- export declare function calculateUsageCost(cost: ModelCost, usage: Usage): Usage["cost"];
11
- /** Price one usage record from its model's token rate card. */
12
- export declare function calculateCost<TApi extends Api>(model: Model<TApi>, usage: Usage): Usage["cost"];
7
+ /** Return the recurring UTC tariff period, independently of its monetary multiplier. */
8
+ export declare function getTimeBasedPricingPeriod(cost: ModelCost, timestamp?: number): "peak" | "off-peak" | undefined;
9
+ /** Return the next actual peak/off-peak change strictly after the Unix-ms timestamp. */
10
+ export declare function getNextTimeBasedPricingTransition(cost: ModelCost, timestamp?: number): number | undefined;
11
+ /** Price a fully uncached prompt at its request timestamp (Unix ms); scheduled prices default to now. */
12
+ export declare function calculateUncachedInputCost(cost: ModelCost, promptInputTokens: number, timestamp?: number): number;
13
+ /** Price usage at its request timestamp (Unix ms); only scheduled prices default to now. */
14
+ export declare function calculateUsageCost(cost: ModelCost, usage: Usage, timestamp?: number): Usage["cost"];
15
+ /** Price usage at its request timestamp (Unix ms); preserve the resulting monetary amounts for display. */
16
+ export declare function calculateCost<TApi extends Api>(model: Model<TApi>, usage: Usage, timestamp?: number): Usage["cost"];
13
17
  /**
14
18
  * Check if two models are equal by comparing both their id and provider.
15
19
  * Returns false if either model is null or undefined.
@@ -0,0 +1,5 @@
1
+ import type { TimeBasedCost } from "./types.js";
2
+ /** Validate serialized schedules before admitting cached model rows. */
3
+ export declare function isTimeBasedCost(value: unknown): value is TimeBasedCost;
4
+ /** KDL object children are named records; normalize them once while constructing the model. */
5
+ export declare function materializeTimeBasedCost(value: unknown): TimeBasedCost;
@@ -34,6 +34,14 @@ export interface ProviderDescriptor {
34
34
  dynamicModelsAuthoritative?: boolean;
35
35
  /** Catalog discovery configuration. Only providers with this field participate in generate-models.ts. */
36
36
  catalogDiscovery?: CatalogDiscoveryConfig;
37
+ /**
38
+ * When true, generator backfills never copy reasoning/input/limits from
39
+ * same-id rows on other providers into this provider's rows. Set for
40
+ * providers whose endpoint discovery is the deployment truth and whose
41
+ * corrections live in KDL, so a stencil.so or canonical-family reference
42
+ * cannot reintroduce foreign metadata.
43
+ */
44
+ skipCrossProviderReferenceFills?: boolean;
37
45
  }
38
46
  /** A provider descriptor that has catalog discovery configured. */
39
47
  export type CatalogProviderDescriptor = ProviderDescriptor & {
@@ -68,6 +76,13 @@ export interface ProviderCatalogEntry {
68
76
  readonly dynamicModelsAuthoritative?: boolean;
69
77
  /** Catalog discovery configuration for generate-models.ts. */
70
78
  readonly catalogDiscovery?: CatalogDiscoveryConfig;
79
+ /**
80
+ * When true, generator backfills never copy reasoning/input/limits from
81
+ * same-id rows on other providers into this provider's rows. Set for
82
+ * providers whose endpoint discovery is the deployment truth and whose
83
+ * corrections live in KDL.
84
+ */
85
+ readonly skipCrossProviderReferenceFills?: boolean;
71
86
  /**
72
87
  * Built bespoke by the coding-agent runtime (OAuth-token-driven managers);
73
88
  * excluded from `PROVIDER_DESCRIPTORS` even though models are discoverable.
@@ -96,6 +96,18 @@ export declare const CATALOG_PROVIDERS: readonly [{
96
96
  readonly catalogDiscovery: {
97
97
  readonly label: "Cloudflare AI Gateway";
98
98
  };
99
+ }, {
100
+ readonly id: "commandcode";
101
+ readonly defaultModel: "claude-sonnet-4-6";
102
+ readonly envVars: readonly ["COMMAND_CODE_API_KEY", "COMMANDCODE_API_KEY"];
103
+ readonly createModelManagerOptions: (config: ModelManagerConfig) => import("../index.js").ModelManagerOptions<import("../index.js").Api, unknown>;
104
+ readonly allowUnauthenticated: true;
105
+ readonly dynamicModelsAuthoritative: true;
106
+ readonly catalogDiscovery: {
107
+ readonly label: "Command Code";
108
+ readonly allowUnauthenticated: true;
109
+ };
110
+ readonly skipCrossProviderReferenceFills: true;
99
111
  }, {
100
112
  readonly id: "cursor";
101
113
  readonly defaultModel: "claude-4.6-opus-high";
@@ -720,3 +720,28 @@ export declare const MODELS_DEV_CATALOG_PROVIDER_IDS: readonly string[];
720
720
  * stricter startup budget and for deterministic timeout tests.
721
721
  */
722
722
  export declare function modelsDevCatalogFallback(providerId: string, fetchImpl?: FetchImpl, timeoutMs?: number): ModelsDevFallback<Api> | undefined;
723
+ /**
724
+ * Configuration for the Command Code Provider API model manager.
725
+ *
726
+ * `baseUrl` overrides the Provider API base path for testing; it is
727
+ * normalized to the shared `/provider` root (a trailing `/v1` is stripped)
728
+ * so Claude ids route to the Anthropic-compatible Messages endpoint at the
729
+ * root while every other id uses chat completions under `/v1`.
730
+ */
731
+ export interface CommandCodeModelManagerConfig {
732
+ apiKey?: string;
733
+ baseUrl?: string;
734
+ fetch?: FetchImpl;
735
+ }
736
+ /**
737
+ * Builds the Command Code model manager: a mixed-protocol OpenAI-compatible
738
+ * discovery client. The public `/v1/models` catalog is fetched once per
739
+ * options instance; `mapModel` pins each row's transport from the
740
+ * `api-routes` table (Claude ids to `anthropic-messages`, everything else to
741
+ * `openai-completions`) and seeds neutral capability defaults. Reviewed
742
+ * Command Code policy (effort ladders, pricing, limits, modalities) is
743
+ * applied later by `buildModel` from `providers/commandcode.kdl` — the
744
+ * mapper never inherits another provider's reasoning, rates, image support,
745
+ * or context window.
746
+ */
747
+ export declare function commandCodeModelManagerOptions(config?: CommandCodeModelManagerConfig): ModelManagerOptions<Api>;
@@ -409,6 +409,20 @@ export interface OpenAICompat {
409
409
  export interface AnthropicCompat {
410
410
  /** Whether thinking requests may include `context_management` and its beta header. Default: true. */
411
411
  supportsContextManagement?: boolean;
412
+ /**
413
+ * Whether the model lineage supports Anthropic server-side compaction
414
+ * (`compact-2026-01-12`: the `compact_20260112` edit and replayed
415
+ * `compaction` blocks). Rule-owned per model line; the beta covers the
416
+ * adaptive-thinking generation onward and rejects older lines. Default: false.
417
+ */
418
+ supportsServerCompaction?: boolean;
419
+ /**
420
+ * Whether the model is served by the first-party Anthropic provider (its
421
+ * default route is the official API). Rule-owned on the provider; the
422
+ * compaction transport pairs it with a per-request effective-URL check
423
+ * because reroutes leave it stale-true. Default: false.
424
+ */
425
+ firstPartyProvider?: boolean;
412
426
  /**
413
427
  * Whether requests may carry `output_config.effort` (and its effort beta
414
428
  * header). Vertex AI rejects the field/header. Default: true.
@@ -756,6 +770,13 @@ export type ResolvedAnthropicCompat = Required<Omit<AnthropicCompat, "streamIdle
756
770
  * env headers, and cache-TTL shaping without per-request URL parsing.
757
771
  */
758
772
  officialEndpoint: boolean;
773
+ /**
774
+ * The model is served by the first-party Anthropic provider. Unlike
775
+ * `officialEndpoint` (URL-derived per deployment), this is provider
776
+ * identity for compaction gating, always paired with a per-request
777
+ * effective-URL check.
778
+ */
779
+ firstPartyProvider: boolean;
759
780
  };
760
781
  /**
761
782
  * Compatibility settings for the devin-agent (Codeium Cascade) API. Cascade
@@ -863,9 +884,27 @@ export interface LongContextTokenCost extends TokenCost {
863
884
  /** Whether the long-context tier starts exactly at `inputThreshold`. */
864
885
  inputThresholdInclusive?: boolean;
865
886
  }
866
- /** Base token rates plus an optional long-context tier. */
887
+ /** Recurring UTC peak interval; weekdays use Sunday = 0, and the end is exclusive. */
888
+ export interface PeakPricingWindow {
889
+ weekdays: readonly number[];
890
+ startMinute: number;
891
+ endMinute: number;
892
+ }
893
+ /** Complete replacement rate card effective from a Unix-millisecond timestamp. */
894
+ export interface EffectiveTokenCost extends TokenCost {
895
+ effectiveFrom: number;
896
+ longContext?: LongContextTokenCost;
897
+ }
898
+ /** Scheduled discounts applied after selecting the effective rate card and context tier. */
899
+ export interface TimeBasedCost {
900
+ offPeakMultiplier: number;
901
+ peakWindows: readonly PeakPricingWindow[];
902
+ effectiveRates?: readonly EffectiveTokenCost[];
903
+ }
904
+ /** Base token rates plus optional long-context and time-based pricing. */
867
905
  export interface ModelCost extends TokenCost {
868
906
  longContext?: LongContextTokenCost;
907
+ timeBased?: TimeBasedCost;
869
908
  }
870
909
  /**
871
910
  * Exact local content tokenizer family for a model.
@@ -892,6 +931,8 @@ export interface Model<TApi extends Api = Api> {
892
931
  requiresGlyphTokenization?: boolean;
893
932
  /** Whether this model requires Cursor's tool-schema combiner projection. */
894
933
  requiresCursorToolSchemaProjection?: boolean;
934
+ /** Whether this model requires tool-result images hoisted into sibling user content blocks. */
935
+ requiresToolResultImageHoisting?: boolean;
895
936
  /**
896
937
  * Model id to send on the wire when it differs from `id`. Used by catalog
897
938
  * variants that present one upstream model under several local entries —
@@ -1065,7 +1106,7 @@ export interface Model<TApi extends Api = Api> {
1065
1106
  * vocabulary of `buildModel`. Identical to `Model` except `compat` carries the
1066
1107
  * sparse override shape and nothing is resolved yet.
1067
1108
  */
1068
- export interface ModelSpec<TApi extends Api = Api> extends Omit<Model<TApi>, "compat" | "identity" | "compatConfig" | "requiresGlyphTokenization" | "requiresCursorToolSchemaProjection" | "supportsComputerUseConfig"> {
1109
+ export interface ModelSpec<TApi extends Api = Api> extends Omit<Model<TApi>, "compat" | "identity" | "compatConfig" | "requiresGlyphTokenization" | "requiresCursorToolSchemaProjection" | "requiresToolResultImageHoisting" | "supportsComputerUseConfig"> {
1069
1110
  /** Sparse compatibility overrides; resolved into `Model.compat` by `buildModel`. */
1070
1111
  compat?: CompatConfigOf<TApi>;
1071
1112
  }
@@ -11,6 +11,16 @@ export declare const COPILOT_CAPI_IDENTITY_HEADERS: {
11
11
  readonly "Copilot-Harness-Id": "copilot-sdk";
12
12
  readonly "Openai-Intent": "conversation-agent";
13
13
  };
14
+ /**
15
+ * Chat-surface identity for Copilot chat and model-policy requests.
16
+ * Some Business organizations gate premium models per client surface and
17
+ * block the CLI identity while allowing chat (issue #11372), so chat traffic
18
+ * defaults to this id with `COPILOT_INTEGRATION_ID` as an explicit pin and a
19
+ * one-shot CLI retry on denial. Model discovery keeps the CLI identity above:
20
+ * it unlocks enterprise/experimental models and listing is not policy-gated
21
+ * the way chat completions are.
22
+ */
23
+ export declare const COPILOT_CHAT_INTEGRATION_ID: "copilot-chat";
14
24
  /**
15
25
  * Copilot API version sent on `api.githubcopilot.com` requests (`/models`,
16
26
  * chat endpoints). Newer versions unlock tiered context metadata: `/models`
@@ -42,6 +52,15 @@ export declare const COPILOT_DISCOVERY_HEADERS: {
42
52
  };
43
53
  /** Preserve model-specific headers while enforcing the current Copilot API identity. */
44
54
  export declare function mergeCopilotApiHeaders(headers?: Readonly<Record<string, string>>): Record<string, string>;
55
+ /**
56
+ * Validate an explicit `Copilot-Integration-Id` override.
57
+ * omp identifies as the Copilot CLI (`copilot-developer-cli`); some Business
58
+ * organizations allow Chat clients but block CLI/agentic ones, rejecting every
59
+ * model with HTTP 403 on an otherwise valid token (issue #11372). Callers pass
60
+ * the raw `COPILOT_INTEGRATION_ID` value; blank or CR/LF-bearing values are
61
+ * rejected so the header stays well-formed and the default identity applies.
62
+ */
63
+ export declare function normalizeCopilotIntegrationId(value: unknown): string | undefined;
45
64
  export type ParsedGitHubCopilotApiKey = {
46
65
  accessToken: string;
47
66
  enterpriseUrl?: string;
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.1.16",
4
+ "version": "18.1.18",
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.1.16",
39
- "@oh-my-pi/pi-utils": "18.1.16"
38
+ "@oh-my-pi/omptype": "18.1.18",
39
+ "@oh-my-pi/pi-utils": "18.1.18"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@bgotink/kdl": "0.4.0",
43
- "@oh-my-pi/pi-ai": "18.1.16",
43
+ "@oh-my-pi/pi-ai": "18.1.18",
44
44
  "@types/bun": "^1.3.14"
45
45
  },
46
46
  "engines": {
package/src/build.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  import { resolveModelPolicy } from "./compat/resolve";
11
11
  import type { ModelIdentity } from "./compat/types";
12
12
  import { resolveModelTokenizer } from "./model-tokenizer";
13
+ import { materializeTimeBasedCost } from "./pricing";
13
14
  import type { Api, Model, ModelSpec } from "./types";
14
15
  import { cleanModelName } from "./utils";
15
16
 
@@ -33,8 +34,8 @@ function isInputModalities(value: unknown): value is ("text" | "image")[] {
33
34
  * corrections (`cost-patch`, `limits-patch`, `long-context-cost`,
34
35
  * `context-window-floor`) overwrite upstream values; selection metadata
35
36
  * (`priority`, `apply-patch-tool-type`, `service-tier-cost`,
36
- * `requires-cursor-tool-schema-projection`) is rule-owned;
37
- * `context-promotion-target` fills only when the spec left it unset.
37
+ * `requires-cursor-tool-schema-projection`, `requires-tool-result-image-hoisting`)
38
+ * is rule-owned; `context-promotion-target` fills only when the spec left it unset.
38
39
  */
39
40
  function applyCatalogAssignments<TApi extends Api>(model: Model<TApi>, catalog: Record<string, unknown>): void {
40
41
  const serviceTierCost = objectPayload(catalog.serviceTierCost);
@@ -62,6 +63,12 @@ function applyCatalogAssignments<TApi extends Api>(model: Model<TApi>, catalog:
62
63
  } else {
63
64
  delete model.requiresCursorToolSchemaProjection;
64
65
  }
66
+ const requiresToolResultImageHoisting = catalog.requiresToolResultImageHoisting;
67
+ if (requiresToolResultImageHoisting === true) {
68
+ model.requiresToolResultImageHoisting = true;
69
+ } else {
70
+ delete model.requiresToolResultImageHoisting;
71
+ }
65
72
  const contextPromotionTarget = catalog.contextPromotionTarget;
66
73
  if (typeof contextPromotionTarget === "string" && model.contextPromotionTarget === undefined) {
67
74
  model.contextPromotionTarget = contextPromotionTarget;
@@ -126,6 +133,9 @@ export function applyCatalogCorrections(
126
133
  const cacheWrite = numberField(patch, "cacheWrite");
127
134
  if (cacheWrite !== undefined) model.cost.cacheWrite = cacheWrite;
128
135
  }
136
+ if (catalog.timeBased !== undefined) {
137
+ model.cost = { ...model.cost, timeBased: materializeTimeBasedCost(catalog.timeBased) };
138
+ }
129
139
  const limitsPatch = objectPayload(catalog.limitsPatch);
130
140
  if (limitsPatch !== undefined) {
131
141
  const contextWindow = numberField(limitsPatch, "contextWindow");
@@ -208,6 +218,10 @@ export function buildModel<TApi extends Api>(spec: ModelSpec<TApi>): Model<TApi>
208
218
  const supportsComputerUseConfig = explicitComputerUseConfig(spec);
209
219
  const model: Model<TApi> = {
210
220
  ...spec,
221
+ // An exact `thinking-efforts` rule upgrades a stale `reasoning: false`
222
+ // discovery default (see `resolveThinkingPolicy`); materialize the
223
+ // correction so transports and the picker see a reasoning-capable model.
224
+ reasoning: spec.reasoning || policy.thinking !== undefined,
211
225
  name: cleanModelName(spec.name),
212
226
  identity: policy.identity,
213
227
  requiresGlyphTokenization: policy.identity.class === "anthropic",
@@ -15,6 +15,7 @@ export type AuthProviderId =
15
15
  | "cerebras"
16
16
  | "cline-pass"
17
17
  | "cloudflare-ai-gateway"
18
+ | "commandcode"
18
19
  | "coreweave"
19
20
  | "cursor"
20
21
  | "deepinfra"
@@ -95,6 +96,7 @@ export type LoginProviderId =
95
96
  | "cerebras"
96
97
  | "cline-pass"
97
98
  | "cloudflare-ai-gateway"
99
+ | "commandcode"
98
100
  | "coreweave"
99
101
  | "cursor"
100
102
  | "deepinfra"
@@ -192,6 +192,7 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
192
192
  "disable-adaptive-thinking": wire("disableAdaptiveThinking", ["anthropic"]),
193
193
  "disable-strict-tools": wire("disableStrictTools", ["anthropic"]),
194
194
  "escape-builtin-tool-names": wire("escapeBuiltinToolNames", ["anthropic"]),
195
+ "first-party-provider": wire("firstPartyProvider", ["anthropic"]),
195
196
  "inject-claude-code-instruction": wire("injectClaudeCodeInstruction", ["anthropic"]),
196
197
  "official-endpoint": wire("officialEndpoint", ["anthropic", "openai-responses"]),
197
198
  "replay-unsigned-thinking": wire("replayUnsignedThinking", ["anthropic"]),
@@ -205,6 +206,7 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
205
206
  "supports-mid-conversation-system": wire("supportsMidConversationSystem", ["anthropic"]),
206
207
  "supports-mid-conversation-tool-changes": wire("supportsMidConversationToolChanges", ["anthropic"]),
207
208
  "supports-per-message-effort": wire("supportsPerMessageEffort", ["anthropic"]),
209
+ "supports-server-compaction": wire("supportsServerCompaction", ["anthropic"]),
208
210
  "supports-thinking-binding-controls": wire("supportsThinkingBindingControls", ["anthropic"]),
209
211
  "supports-turn-scoped-system": wire("supportsTurnScopedSystem", ["anthropic"]),
210
212
 
@@ -259,6 +261,7 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
259
261
  "thinking-prefix-binding": { key: "prefixBinding", set: "thinking", shape: "scalar" },
260
262
  "thinking-suppress-when-off": { key: "suppressWhenOff", set: "thinking", shape: "scalar" },
261
263
  "thinking-supports-display": { key: "supportsDisplay", set: "thinking", shape: "scalar" },
264
+ "thinking-upgrade-neutral": { key: "upgradeNeutral", set: "thinking", shape: "scalar" },
262
265
 
263
266
  // ── catalog metadata ──
264
267
  "apply-patch-tool-type": {
@@ -284,8 +287,14 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
284
287
  set: "catalog",
285
288
  shape: "scalar",
286
289
  },
290
+ "requires-tool-result-image-hoisting": {
291
+ key: "requiresToolResultImageHoisting",
292
+ set: "catalog",
293
+ shape: "scalar",
294
+ },
287
295
  priority: { key: "priority", set: "catalog", shape: "scalar" },
288
296
  "service-tier-cost": { key: "serviceTierCost", set: "catalog", shape: "object" },
297
+ "time-based-cost": { key: "timeBased", set: "catalog", shape: "object" },
289
298
  };
290
299
 
291
300
  /** Records applicable to each API family; used by `resolve.ts` when applying wire axes. */
@@ -843,6 +843,8 @@ function resolveAnthropicPolicy(
843
843
  officialEndpoint: official,
844
844
  signingEndpoint,
845
845
  supportsContextManagement: true,
846
+ supportsServerCompaction: false,
847
+ firstPartyProvider: false,
846
848
  supportsOutputEffort: true,
847
849
  disableStrictTools: isAzure,
848
850
  disableAdaptiveThinking: false,
@@ -1021,6 +1023,7 @@ interface RuleThinking {
1021
1023
  suppressWhenOff?: boolean;
1022
1024
  supportsDisplay?: boolean;
1023
1025
  prefixBinding?: boolean;
1026
+ upgradeNeutral?: boolean;
1024
1027
  }
1025
1028
 
1026
1029
  function readRuleThinking(axes: ResolvedAxes): RuleThinking {
@@ -1040,6 +1043,7 @@ function readRuleThinking(axes: ResolvedAxes): RuleThinking {
1040
1043
  if (typeof raw.suppressWhenOff === "boolean") out.suppressWhenOff = raw.suppressWhenOff;
1041
1044
  if (typeof raw.supportsDisplay === "boolean") out.supportsDisplay = raw.supportsDisplay;
1042
1045
  if (typeof raw.prefixBinding === "boolean") out.prefixBinding = raw.prefixBinding;
1046
+ if (typeof raw.upgradeNeutral === "boolean") out.upgradeNeutral = raw.upgradeNeutral;
1043
1047
  return out;
1044
1048
  }
1045
1049
 
@@ -1065,7 +1069,18 @@ function resolveThinkingPolicy<TApi extends Api>(
1065
1069
  axes: ResolvedAxes,
1066
1070
  compat: CompatOf<TApi>,
1067
1071
  ): ThinkingConfig | undefined {
1068
- if (!spec.reasoning) return undefined;
1072
+ const rule = readRuleThinking(axes);
1073
+ const explicitThinking =
1074
+ spec.thinking !== undefined && Array.isArray(spec.thinking.efforts) && spec.thinking.efforts.length > 0
1075
+ ? spec.thinking
1076
+ : undefined;
1077
+ // An explicit wire vocabulary is authoritative when discovery reports no
1078
+ // reasoning (e.g. Synthetic's `none`-only off-switch): reviewed KDL must
1079
+ // not re-expand it into an unadvertised ladder. Absent metadata is
1080
+ // repaired only where KDL opts in with `thinking-upgrade-neutral`
1081
+ // alongside an exact `thinking-efforts` ladder (the cascade upgrade for
1082
+ // stale source capability data); otherwise the neutral default holds.
1083
+ if (!spec.reasoning && (explicitThinking !== undefined || rule.upgradeNeutral !== true)) return undefined;
1069
1084
  if (
1070
1085
  spec.provider === "cline-pass" &&
1071
1086
  compat !== undefined &&
@@ -1075,9 +1090,8 @@ function resolveThinkingPolicy<TApi extends Api>(
1075
1090
  return undefined;
1076
1091
  }
1077
1092
  if (omitsWireReasoningEffort(spec.api, compat)) return undefined;
1078
- const rule = readRuleThinking(axes);
1079
- if (spec.thinking && Array.isArray(spec.thinking.efforts) && spec.thinking.efforts.length > 0) {
1080
- return fillExplicitThinking(spec, facts, compat, spec.thinking, rule);
1093
+ if (explicitThinking !== undefined) {
1094
+ return fillExplicitThinking(spec, facts, compat, explicitThinking, rule);
1081
1095
  }
1082
1096
  if (compat !== undefined && "trustExplicitThinkingOnly" in compat && compat.trustExplicitThinkingOnly === true) {
1083
1097
  return undefined;
@@ -207,6 +207,43 @@ The three value shapes are:
207
207
  A rule cannot assign the same resolved axis twice in one block.
208
208
  One object axis carries a computed form: `long-context-cost` accepts either the absolute rates (`input-threshold` + `input`/`output`/`cache-read`/`cache-write`) or `input-threshold` + `multiplier` (with optional `input-threshold-inclusive`), which derives the tier from the row's live base price at build time so the rule tracks upstream list-price updates (xAI's SuperGrok 200K tier). Rows without a token price carry no tier.
209
209
 
210
+ ### Time-based pricing
211
+
212
+ The catalog object axis `time-based-cost` materializes into `ModelCost.timeBased`. It uses **named child objects**, not KDL arrays or repeated anonymous windows. For example, inside a matching provider/model scope:
213
+
214
+ ```kdl
215
+ time-based-cost {
216
+ off-peak-multiplier 0.5
217
+ peak-windows {
218
+ morning {
219
+ weekdays "1,2,3,4,5"
220
+ start-minute 60
221
+ end-minute 240
222
+ }
223
+ afternoon {
224
+ weekdays "1,2,3,4,5"
225
+ start-minute 360
226
+ end-minute 600
227
+ }
228
+ }
229
+ effective-rates {
230
+ flash-pricing {
231
+ effective-from "2026-09-14T04:00:00Z"
232
+ input 0.30
233
+ output 1.20
234
+ cache-read 0.006
235
+ cache-write 0
236
+ }
237
+ }
238
+ }
239
+ ```
240
+
241
+ `morning`, `afternoon`, and `flash-pricing` are arbitrary unique object names, discarded when the payload is normalized into arrays. `weekdays` is a comma-separated string of distinct UTC weekday numbers (`0` = Sunday through `6` = Saturday), without spaces. Each window has integer minutes with `0 <= start-minute < end-minute <= 1440`; its start is inclusive and end exclusive. Split overnight windows across days. Outside the union of peak windows, the nonnegative `off-peak-multiplier` applies to token costs.
242
+
243
+ `effective-rates` is optional. Each entry requires a distinct valid ISO UTC `effective-from` (`YYYY-MM-DDTHH:mm:ssZ` or with three fractional-second digits) and all four nonnegative per-million-token rates. The latest entry at or before the request timestamp replaces the entire base card; before the first entry, the base card applies. An entry may contain a `long-context` object with `input-threshold`, optional `input-threshold-inclusive`, and all four absolute rates. Effective rates do not inherit the base card's long-context tier. Selection order is effective card, context tier, then tariff multiplier.
244
+
245
+ The recurring schedule and dated DeepSeek transition above come from [DeepSeek's official pricing](https://api-docs.deepseek.com/quick_start/pricing); see `providers/deepseek.kdl` for the complete rules and [the catalog API](../../../README.md#cost-calculation) for timestamp semantics. This is catalog policy metadata, not a supported schedule syntax for the coding agent's `models.yml`.
246
+
210
247
  ### Precedence and ambiguity
211
248
 
212
249
  Rules resolve independently per axis. A matching rule is ranked by:
@@ -5,7 +5,7 @@ login-order "openai-codex" "anthropic" "zai" "zai-coding-plan" "kimi-code" "open
5
5
  "cursor" "devin" "google-antigravity" "google-gemini-cli" "openai-codex-device" "xai" "xai-oauth" \
6
6
  "gitlab-duo" "gitlab-duo-agent" "alibaba-coding-plan" "alibaba-token-plan" "aiand" "abliteration" "zhipu-coding-plan" \
7
7
  "umans" "qwen-portal" "sakana" "minimax-code" "minimax-code-cn" "xiaomi" "xiaomi-token-plan-sgp" \
8
- "xiaomi-token-plan-ams" "xiaomi-token-plan-cn" "firepass" "cline-pass" "deepseek" "muse-code" "meta" "moonshot" \
8
+ "xiaomi-token-plan-ams" "xiaomi-token-plan-cn" "firepass" "cline-pass" "commandcode" "deepseek" "muse-code" "meta" "moonshot" \
9
9
  "cerebras" "baseten" "fireworks" "together" "nvidia" "novita" "deepinfra" "huggingface" "perplexity" \
10
10
  "qianfan" "venice" "siliconflow" "siliconflow-cn" "synthetic" "nanogpt" "wafer-serverless" "coreweave" \
11
11
  "vercel-ai-gateway" "cloudflare-ai-gateway" "litellm" "kilo" "zenmux" "opencode-zen" "opencode-go" \
@@ -0,0 +1,13 @@
1
+ // No `validate` probe on purpose: GET /provider/v1/models is public (200
2
+ // with no or bogus credentials, verified 2026-09-09), so a models-endpoint
3
+ // probe could never reject a bad key. A chat-completions probe would bill
4
+ // the key and 403 for Go-plan keys that are still valid CLI credentials.
5
+ auth "commandcode" {
6
+ name "Command Code"
7
+ env "COMMAND_CODE_API_KEY" "COMMANDCODE_API_KEY"
8
+ login "api-key" {
9
+ auth-url "https://commandcode.ai/studio"
10
+ instructions "Create or copy a Provider API key from Command Code Studio"
11
+ prompt "Paste your Command Code API key" placeholder="user_..."
12
+ }
13
+ }
@@ -7,6 +7,30 @@ class "anthropic" {
7
7
  thinking-prefix-binding #true
8
8
  }
9
9
  }
10
+ // Server-side compaction (compact-2026-01-12) is a lineage capability: the
11
+ // documented model list is the adaptive-thinking generation onward (Opus
12
+ // 4.6+, Sonnet 4.6+, Fable/Mythos 5+). Whether a host delivers the beta is
13
+ // decided at request time from the effective endpoint, not here.
14
+ family "fable" {
15
+ revision ">=5" {
16
+ supports-server-compaction #true
17
+ }
18
+ }
19
+ family "mythos" {
20
+ revision ">=5" {
21
+ supports-server-compaction #true
22
+ }
23
+ }
24
+ family "opus" {
25
+ revision ">=4.6" {
26
+ supports-server-compaction #true
27
+ }
28
+ }
29
+ family "sonnet" {
30
+ revision ">=4.6" {
31
+ supports-server-compaction #true
32
+ }
33
+ }
10
34
  on "anthropic" "cloudflare-ai-gateway" "google-vertex" {
11
35
  family "fable" {
12
36
  revision ">=5" {
@@ -15,6 +15,8 @@ provider "amazon-bedrock" {
15
15
  }
16
16
  class "openai" {
17
17
  thinking-mode "effort"
18
+ // OpenAI models reject Converse image blocks nested inside toolResult content.
19
+ requires-tool-result-image-hoisting #true
18
20
  }
19
21
  class "deepseek" {
20
22
  requires-reasoning-content-for-all-assistant-turns #true
@@ -28,6 +30,11 @@ provider "amazon-bedrock" {
28
30
  class "unknown" {
29
31
  thinking-efforts "minimal" "low" "medium" "high"
30
32
  thinking-mode "budget"
33
+ // Opaque application-inference-profile ARNs carry no vendor signal, so an
34
+ // OpenAI-backed profile classifies here and would otherwise nest images in
35
+ // toolResult content and hit the same rejection. Hoisting is accepted by
36
+ // every Converse model measured, so it is the safe default for unknowns.
37
+ requires-tool-result-image-hoisting #true
31
38
  }
32
39
  // residue: taxonomy ranks and exact globs do not isolate these models.
33
40
  models "moonshot.kimi-k2-thinking" {