@oh-my-pi/pi-catalog 18.14.2 → 18.16.0

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 (52) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/types/compat/auth-ids.d.ts +2 -2
  3. package/dist/types/compat/catalog-policy.d.ts +6 -4
  4. package/dist/types/compat/factory-droid.d.ts +7 -1
  5. package/dist/types/compat/provider-ids.d.ts +1 -1
  6. package/dist/types/compat/resolve.d.ts +1 -1
  7. package/dist/types/compat/revision.d.ts +2 -0
  8. package/dist/types/compat/server-side-fallback.d.ts +6 -4
  9. package/dist/types/compat/types.d.ts +3 -1
  10. package/dist/types/discovery/openai-compatible.d.ts +6 -0
  11. package/dist/types/models.d.ts +16 -0
  12. package/dist/types/pricing.d.ts +66 -6
  13. package/dist/types/provider-models/openai-compat.d.ts +18 -0
  14. package/dist/types/types.d.ts +11 -5
  15. package/dist/types/utils.d.ts +8 -0
  16. package/package.json +4 -4
  17. package/src/compat/auth-ids.ts +5 -0
  18. package/src/compat/axes.ts +5 -2
  19. package/src/compat/catalog-policy.ts +14 -10
  20. package/src/compat/collapse.ts +1 -1
  21. package/src/compat/factory-droid.ts +9 -1
  22. package/src/compat/provider-ids.ts +2 -0
  23. package/src/compat/resolve.ts +4 -2
  24. package/src/compat/revision.ts +10 -0
  25. package/src/compat/rules/README.md +2 -0
  26. package/src/compat/rules/auth/_order.kdl +1 -1
  27. package/src/compat/rules/auth/mistral.kdl +4 -0
  28. package/src/compat/rules/auth/modular.kdl +9 -0
  29. package/src/compat/rules/auth/scaleway.kdl +17 -0
  30. package/src/compat/rules/providers/factory-droid.kdl +219 -13
  31. package/src/compat/rules/providers/modular.kdl +35 -0
  32. package/src/compat/rules/providers/sambanova.kdl +2 -0
  33. package/src/compat/rules/providers/scaleway.kdl +179 -0
  34. package/src/compat/rules/runtime/behavior.kdl +13 -0
  35. package/src/compat/rules.json +1 -1
  36. package/src/compat/server-side-fallback.ts +7 -5
  37. package/src/compat/types.ts +3 -1
  38. package/src/discovery/codex.ts +2 -9
  39. package/src/discovery/factory-droid.ts +35 -11
  40. package/src/discovery/openai-compatible.ts +1 -1
  41. package/src/model-cache.ts +32 -58
  42. package/src/model-catalog-format.ts +5 -8
  43. package/src/model-manager.ts +27 -121
  44. package/src/model-tokenizer.ts +13 -12
  45. package/src/models.json +1 -1
  46. package/src/models.ts +32 -1
  47. package/src/pricing.ts +59 -94
  48. package/src/provider-models/descriptors.ts +2 -0
  49. package/src/provider-models/openai-compat.ts +60 -59
  50. package/src/types.ts +11 -4
  51. package/src/utils.ts +18 -0
  52. package/src/wire/factory-droid.ts +4 -3
package/CHANGELOG.md CHANGED
@@ -2,6 +2,36 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.16.0] - 2026-11-02
6
+
7
+ ### Added
8
+
9
+ - `/login mistral` now offers the Mistral Vibe browser sign-in beside `MISTRAL_API_KEY` ([#13875](https://github.com/can1357/oh-my-pi/pull/13875) by [@Gauthier-Huguenin](https://github.com/Gauthier-Huguenin), continued in [#17672](https://github.com/can1357/oh-my-pi/pull/17672) by [@abilliontokens](https://github.com/abilliontokens))
10
+ - Added `revisionAtLeast` to `compat/revision` and exported `withOpenAICompatibleDiscoveryTimeout` from `discovery/openai-compatible`
11
+ - Added the Factory Droid models droid 1.1.0 serves: GPT-6.1 Terra, GPT-6 Sol and Luna Fast Mode, GPT-6 Astra and GPT-6.1 Sol Ultra Fast (after the organization's high-cost opt-in), Nemotron 3.5 Super, Muse Spark Open, Muse Spark 1.2 and Beam 1.0, each listed once the account may use it ([#17766](https://github.com/can1357/oh-my-pi/pull/17766) by [@will-bogusz](https://github.com/will-bogusz))
12
+ - Added Factory Droid's dated credit rates: the credit rate follows Factory's promotions, discounted from a promotion's start and back to the base rate at its end, using the same dated rate cards as dollar prices (currently GPT-5.6 Sol and Gemini 3.7/3.8 Flash) ([#16309](https://github.com/can1357/oh-my-pi/pull/16309) by [@will-bogusz](https://github.com/will-bogusz))
13
+
14
+ ### Changed
15
+
16
+ - Factory Droid lists Opus 5.5 Fast, GPT-6.1 Sol and MiniMax M3 without the feature flags Factory retired, and no longer routes GPT-5.5 to Snowflake ([#17766](https://github.com/can1357/oh-my-pi/pull/17766) by [@will-bogusz](https://github.com/will-bogusz))
17
+ - Factory Droid Opus 5.5 now falls back to Opus 5 when Anthropic refuses a request, as droid does, while your account can use Opus 5 ([#17772](https://github.com/can1357/oh-my-pi/pull/17772) by [@will-bogusz](https://github.com/will-bogusz))
18
+
19
+ ### Fixed
20
+
21
+ - GPT-3.5 and GPT-4 (Turbo) resolve the `cl100k` tokenizer, so their context estimates count in cl100k_base rather than the o200k_base default for models without one ([#17607](https://github.com/can1357/oh-my-pi/pull/17607) by [@will-bogusz](https://github.com/will-bogusz))
22
+ - Added Scaleway Generative APIs as a built-in provider with 12 serverless chat models (glm-5.3 default), API-key login, and live model discovery ([#16687](https://github.com/can1357/oh-my-pi/pull/16687) by [@reda-maizate](https://github.com/reda-maizate))
23
+
24
+ ## [18.15.0] - 2026-11-01
25
+
26
+ ### Added
27
+
28
+ - Added Modular Cloud (`modular`) as a provider: `/login modular` with an API key, models and prices from the models.dev `modular` entry, and per-model reasoning controls for MiniMax M3.1 Flash, MiniMax M3, and Gemma 4 31B ([#17334](https://github.com/can1357/oh-my-pi/pull/17334) by [@tommyjtl](https://github.com/tommyjtl))
29
+ - SambaNova Cloud follow-up turns now reuse the provider's prefix cache, so multi-turn conversations stop re-processing earlier context ([#17555](https://github.com/can1357/oh-my-pi/pull/17555) by [@rjuez00](https://github.com/rjuez00))
30
+
31
+ ### Fixed
32
+
33
+ - Fixed newly discovered OpenAI-compatible models ignoring provider-reported context and output token limits ([#17561](https://github.com/can1357/oh-my-pi/pull/17561) by [@wolfiesch](https://github.com/wolfiesch))
34
+
5
35
  ## [18.14.2] - 2026-10-31
6
36
 
7
37
  ### Breaking Changes
@@ -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" | "apple" | "azure" | "baseten" | "bedrock-mantle" | "cerebras" | "charm-hyper" | "cline-pass" | "cloudflare-ai-gateway" | "cloudflare-workers-ai" | "commandcode" | "coralbricks" | "coreweave" | "cursor" | "deepinfra" | "deepseek" | "devin" | "exa" | "factory-droid" | "firepass" | "fireworks" | "github-copilot" | "gitlab-duo" | "gitlab-duo-agent" | "gmi-cloud" | "google" | "google-antigravity" | "google-gemini-cli" | "google-vertex" | "groq" | "helmcode" | "huggingface" | "inception" | "kagi" | "kilo" | "kimi-code" | "litellm" | "llama.cpp" | "lm-studio" | "local" | "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" | "sambanova" | "siliconflow" | "siliconflow-cn" | "singularityapi-dev" | "singularityapi-tech" | "snowflake" | "stencil" | "stepfun" | "synthetic" | "tavily" | "together" | "typesafe" | "umans" | "venice" | "vercel-ai-gateway" | "vllm" | "wafer-serverless" | "web" | "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" | "apple" | "azure" | "baseten" | "bedrock-mantle" | "cerebras" | "charm-hyper" | "cline-pass" | "cloudflare-ai-gateway" | "cloudflare-workers-ai" | "commandcode" | "coralbricks" | "coreweave" | "cursor" | "deepinfra" | "deepseek" | "devin" | "exa" | "factory-droid" | "firepass" | "fireworks" | "github-copilot" | "gitlab-duo" | "gitlab-duo-agent" | "gmi-cloud" | "google" | "google-antigravity" | "google-gemini-cli" | "google-vertex" | "groq" | "helmcode" | "huggingface" | "inception" | "kagi" | "kilo" | "kimi-code" | "litellm" | "llama.cpp" | "lm-studio" | "local" | "meta" | "minimax" | "minimax-code" | "minimax-code-cn" | "mistral" | "modular" | "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" | "sambanova" | "scaleway" | "siliconflow" | "siliconflow-cn" | "singularityapi-dev" | "singularityapi-tech" | "snowflake" | "stencil" | "stepfun" | "synthetic" | "tavily" | "together" | "typesafe" | "umans" | "venice" | "vercel-ai-gateway" | "vllm" | "wafer-serverless" | "web" | "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" | "charm-hyper" | "cline-pass" | "cloudflare-ai-gateway" | "cloudflare-workers-ai" | "commandcode" | "coralbricks" | "coreweave" | "cursor" | "deepinfra" | "deepseek" | "devin" | "exa" | "factory-droid" | "firepass" | "fireworks" | "github-copilot" | "gitlab-duo" | "gitlab-duo-agent" | "gmi-cloud" | "google-antigravity" | "google-gemini-cli" | "helmcode" | "huggingface" | "inception" | "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" | "sambanova" | "siliconflow" | "siliconflow-cn" | "singularityapi-dev" | "singularityapi-tech" | "snowflake" | "stencil" | "stepfun" | "synthetic" | "tavily" | "together" | "typesafe" | "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" | "charm-hyper" | "cline-pass" | "cloudflare-ai-gateway" | "cloudflare-workers-ai" | "commandcode" | "coralbricks" | "coreweave" | "cursor" | "deepinfra" | "deepseek" | "devin" | "exa" | "factory-droid" | "firepass" | "fireworks" | "github-copilot" | "gitlab-duo" | "gitlab-duo-agent" | "gmi-cloud" | "google-antigravity" | "google-gemini-cli" | "helmcode" | "huggingface" | "inception" | "kagi" | "kilo" | "kimi-code" | "litellm" | "llama.cpp" | "lm-studio" | "meta" | "minimax-code" | "minimax-code-cn" | "mistral" | "modular" | "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" | "sambanova" | "scaleway" | "siliconflow" | "siliconflow-cn" | "singularityapi-dev" | "singularityapi-tech" | "snowflake" | "stencil" | "stepfun" | "synthetic" | "tavily" | "together" | "typesafe" | "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";
@@ -1,7 +1,9 @@
1
1
  import type { CatalogPolicyInputs } from "../types.js";
2
+ import { type ResolveRoute } from "./resolve.js";
2
3
  /**
3
- * The `catalog` axis bag authored in the KDL cascade rules for a model,
4
- * memoized per distinct model. Frozen: the bag is shared across callers, so it
5
- * must stay read-only — extract the value you need rather than handing it on.
4
+ * The `catalog` axis bag authored in the KDL cascade rules for a model (on one
5
+ * upstream when `route` names it), memoized per distinct model. Frozen: the bag
6
+ * is shared across callers, so it must stay read-only — extract the value you
7
+ * need rather than handing it on.
6
8
  */
7
- export declare function resolveCatalogPolicy(model: CatalogPolicyInputs): Readonly<Record<string, unknown>>;
9
+ export declare function resolveCatalogPolicy(model: CatalogPolicyInputs, route?: ResolveRoute): Readonly<Record<string, unknown>>;
@@ -1,3 +1,4 @@
1
+ import { type DatedCostCard } from "../pricing.js";
1
2
  import type { Api, ModelSpec } from "../types.js";
2
3
  import { type FactoryDroidRegion, type FactoryDroidWire } from "../wire/factory-droid.js";
3
4
  /** Native token limits for one inference region. */
@@ -5,11 +6,16 @@ export interface FactoryDroidLimits {
5
6
  contextWindow: number | null;
6
7
  maxTokens: number | null;
7
8
  }
8
- /** Standard Credits rates: `input` is the per-token weight; `output` and `cacheRead` multiply it. */
9
+ /**
10
+ * Standard Credits rates: `input` is the per-token weight; `output` and
11
+ * `cacheRead` multiply it. `effectiveRates` are dated cards (ascending) that
12
+ * overwrite those legs from their date, as on `cost-patch`.
13
+ */
9
14
  export interface FactoryDroidCreditRates {
10
15
  input: number;
11
16
  output?: number;
12
17
  cacheRead?: number;
18
+ effectiveRates?: readonly DatedCostCard[];
13
19
  }
14
20
  /** Account gates on one registry model (the `entitlement` axis). */
15
21
  export interface FactoryDroidEntitlement {
@@ -1,2 +1,2 @@
1
1
  /** Every chat-model provider with a catalog entry (`default-model` in its provider rules). */
2
- export type KnownProvider = "abliteration" | "aiand" | "aimlapi" | "alibaba-coding-plan" | "alibaba-token-plan" | "amazon-bedrock" | "anthropic" | "apple" | "azure" | "baseten" | "bedrock-mantle" | "cerebras" | "charm-hyper" | "cline-pass" | "cloudflare-ai-gateway" | "cloudflare-workers-ai" | "commandcode" | "coralbricks" | "coreweave" | "cursor" | "deepinfra" | "deepseek" | "devin" | "factory-droid" | "firepass" | "fireworks" | "github-copilot" | "gitlab-duo" | "gitlab-duo-agent" | "gmi-cloud" | "google" | "google-antigravity" | "google-gemini-cli" | "google-vertex" | "groq" | "helmcode" | "huggingface" | "inception" | "kilo" | "kimi-code" | "litellm" | "lm-studio" | "local" | "meta" | "minimax" | "minimax-code" | "minimax-code-cn" | "mistral" | "moonshot" | "muse-code" | "nanogpt" | "novita" | "nvidia" | "ollama" | "ollama-cloud" | "openai" | "openai-codex" | "opencode-go" | "opencode-zen" | "openrouter" | "qianfan" | "qwen-portal" | "sakana" | "sambanova" | "siliconflow" | "siliconflow-cn" | "singularityapi-dev" | "singularityapi-tech" | "snowflake" | "stepfun" | "synthetic" | "together" | "typesafe" | "umans" | "venice" | "vercel-ai-gateway" | "vllm" | "wafer-serverless" | "web" | "xai" | "xai-oauth" | "xiaomi" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "xiaomi-token-plan-sgp" | "yolo-auto" | "zai" | "zenmux" | "zhipu-coding-plan";
2
+ export type KnownProvider = "abliteration" | "aiand" | "aimlapi" | "alibaba-coding-plan" | "alibaba-token-plan" | "amazon-bedrock" | "anthropic" | "apple" | "azure" | "baseten" | "bedrock-mantle" | "cerebras" | "charm-hyper" | "cline-pass" | "cloudflare-ai-gateway" | "cloudflare-workers-ai" | "commandcode" | "coralbricks" | "coreweave" | "cursor" | "deepinfra" | "deepseek" | "devin" | "factory-droid" | "firepass" | "fireworks" | "github-copilot" | "gitlab-duo" | "gitlab-duo-agent" | "gmi-cloud" | "google" | "google-antigravity" | "google-gemini-cli" | "google-vertex" | "groq" | "helmcode" | "huggingface" | "inception" | "kilo" | "kimi-code" | "litellm" | "lm-studio" | "local" | "meta" | "minimax" | "minimax-code" | "minimax-code-cn" | "mistral" | "modular" | "moonshot" | "muse-code" | "nanogpt" | "novita" | "nvidia" | "ollama" | "ollama-cloud" | "openai" | "openai-codex" | "opencode-go" | "opencode-zen" | "openrouter" | "qianfan" | "qwen-portal" | "sakana" | "sambanova" | "scaleway" | "siliconflow" | "siliconflow-cn" | "singularityapi-dev" | "singularityapi-tech" | "snowflake" | "stepfun" | "synthetic" | "together" | "typesafe" | "umans" | "venice" | "vercel-ai-gateway" | "vllm" | "wafer-serverless" | "web" | "xai" | "xai-oauth" | "xiaomi" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "xiaomi-token-plan-sgp" | "yolo-auto" | "zai" | "zenmux" | "zhipu-coding-plan";
@@ -21,7 +21,7 @@ export declare function resolveDiscoveryApi(spec: ModelSpec<Api>, providerType:
21
21
  * {@link resolveModelPolicy} without resolving the compat and thinking
22
22
  * policies, which dominate its cost on catalog-wide scans.
23
23
  */
24
- export declare function resolveCatalogAxes(spec: CatalogPolicyInputs): Record<string, unknown>;
24
+ export declare function resolveCatalogAxes(spec: CatalogPolicyInputs, route?: ResolveRoute): Record<string, unknown>;
25
25
  /**
26
26
  * Resolves the full policy surface for one model spec: structured identity,
27
27
  * complete compat record, thinking metadata, and catalog-data corrections.
@@ -22,6 +22,8 @@ export declare function parseRevision(value: string): Revision | undefined;
22
22
  export declare function parseRevisionPrefix(value: string): Revision | undefined;
23
23
  /** Lexicographic triple comparison: negative, zero, or positive. */
24
24
  export declare function compareRevision(a: Revision, b: Revision): number;
25
+ /** Whether a revision string parses and is at or above `floor`; `undefined` or unparsable is never. */
26
+ export declare function revisionAtLeast(revision: string | undefined, floor: string): boolean;
25
27
  /** Renders a revision as its canonical `major.minor.patch` string. */
26
28
  export declare function formatRevision(revision: Revision): string;
27
29
  /** Comparison operator inside a `revision ">=2.5 <4"` cascade constraint. */
@@ -1,7 +1,9 @@
1
- import type { Model } from "../types.js";
1
+ import type { CatalogPolicyInputs } from "../types.js";
2
+ import type { ResolveRoute } from "./resolve.js";
2
3
  /**
3
4
  * Ordered model ids for Anthropic's server-side refusal `fallbacks` chain,
4
- * authored on the `server-side-fallback-models` catalog axis. Empty when no
5
- * rule assigns a chain (the model or host does not support the feature).
5
+ * authored on the `server-side-fallback-models` catalog axis, on `route`'s
6
+ * upstream when it names one. Empty when no rule assigns a chain (the model,
7
+ * host or upstream does not support the feature).
6
8
  */
7
- export declare function serverSideFallbackModels(model: Model): readonly string[];
9
+ export declare function serverSideFallbackModels(model: CatalogPolicyInputs, route?: ResolveRoute): readonly string[];
@@ -714,6 +714,8 @@ export interface ResolvedAxes {
714
714
  export interface RequestPolicy {
715
715
  completionsReasoningMode?: "none" | "effort" | "opt-in" | "forced-on";
716
716
  completionsReasoningHistory?: "omit" | "preserved" | "interleaved";
717
+ /** Send `allow_missing_thinking` with the effort so tool-call turns may arrive without reasoning. */
718
+ completionsAllowMissingThinking?: boolean;
717
719
  anthropicThinking?: "adaptive" | "adaptive-summarized" | "budget-interleaved" | "budget-effort";
718
720
  /** Advertise `fine-grained-tool-streaming-2025-05-14` on requests that carry tools. */
719
721
  anthropicToolStreamingBeta?: boolean;
@@ -721,7 +723,7 @@ export interface RequestPolicy {
721
723
  openaiPlatformHeader?: string;
722
724
  responsesCacheRetention?: boolean;
723
725
  responsesVerbosity?: "low";
724
- responsesServiceTier?: "priority";
726
+ responsesServiceTier?: "priority" | "ultrafast";
725
727
  responsesParallelToolCalls?: boolean;
726
728
  responsesSafetyIdentifier?: boolean;
727
729
  /** Default `tool_choice` to `auto` when the request carries tools and the caller picks none. */
@@ -10,6 +10,12 @@ import type { Api, FetchImpl, ModelSpec, Provider } from "../types.js";
10
10
  * (issue #8315). 10s matches the coding-agent's remote-discovery budget.
11
11
  */
12
12
  export declare const DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS = 10000;
13
+ /**
14
+ * Uses a cancellable timer rather than the native abort-timeout helper so
15
+ * successful fast discovery requests do not leave armed timeout signals for
16
+ * concurrent GC to trip over later.
17
+ */
18
+ export declare function withOpenAICompatibleDiscoveryTimeout<T>(timeoutMs: number, run: (signal: AbortSignal) => Promise<T>): Promise<T>;
13
19
  /**
14
20
  * Minimal OpenAI-style model entry shape consumed by discovery.
15
21
  *
@@ -11,6 +11,22 @@ export declare function getBundledProviders(): readonly KnownProvider[];
11
11
  export declare function getBundledModels(provider: GeneratedProvider): Model<Api>[];
12
12
  /** Resolve display semantics without mistaking an absent rate card for a free model. */
13
13
  export declare function getModelPricingStatus(model: Pick<Model<Api>, "cost" | "pricingStatus">): ModelPricingStatus;
14
+ /** Factory Droid credit rate in force at a time, and the next dated change to it. */
15
+ export interface FactoryDroidCreditRate {
16
+ rate: number;
17
+ /** The earliest later `credit-rates` card that changes `rate`: the rate it sets and when (Unix ms). */
18
+ next?: {
19
+ rate: number;
20
+ at: number;
21
+ };
22
+ }
23
+ /**
24
+ * Factory Droid Standard Credits rate at `timestamp` (Unix ms, default now):
25
+ * the `input` of the `credit-rates` card due then (the latest at or before
26
+ * it, as for cost cards), else the base rate. Undefined for models without a
27
+ * credit rate.
28
+ */
29
+ export declare function getFactoryDroidCreditRate(model: Pick<Model, "factoryDroidCredits" | "factoryDroidCreditCards">, timestamp?: number): FactoryDroidCreditRate | undefined;
14
30
  /**
15
31
  * The base token card in force at `timestamp` (Unix ms, default now): the
16
32
  * latest due `timeBased` effective rate, else the row's own legs. Price
@@ -1,24 +1,84 @@
1
1
  import type { EffectiveTokenCost, ModelCost, TimeBasedCost, TokenCost } from "./types.js";
2
+ /** Serialized time-based pricing schedule, as cached model rows carry it. */
3
+ export declare const TimeBasedCostShape: import("@oh-my-pi/omptype").FluentType<{
4
+ effectiveRates?: ({
5
+ cacheRead: number;
6
+ cacheWrite: number;
7
+ input: number;
8
+ output: number;
9
+ } & {
10
+ effectiveFrom: number;
11
+ longContext?: ({
12
+ cacheRead: number;
13
+ cacheWrite: number;
14
+ input: number;
15
+ output: number;
16
+ } & {
17
+ inputThreshold: number;
18
+ inputThresholdInclusive?: boolean | undefined;
19
+ }) | undefined;
20
+ })[] | undefined;
21
+ offPeakMultiplier: number;
22
+ peakWindows: {
23
+ endMinute: number;
24
+ startMinute: number;
25
+ weekdays: number[];
26
+ }[];
27
+ }, {
28
+ effectiveRates?: ({
29
+ cacheRead: number;
30
+ cacheWrite: number;
31
+ input: number;
32
+ output: number;
33
+ } & {
34
+ effectiveFrom: number;
35
+ longContext?: ({
36
+ cacheRead: number;
37
+ cacheWrite: number;
38
+ input: number;
39
+ output: number;
40
+ } & {
41
+ inputThreshold: number;
42
+ inputThresholdInclusive?: boolean | undefined;
43
+ }) | undefined;
44
+ })[] | undefined;
45
+ offPeakMultiplier: number;
46
+ peakWindows: {
47
+ endMinute: number;
48
+ startMinute: number;
49
+ weekdays: number[];
50
+ }[];
51
+ }>;
2
52
  /** Validate serialized schedules before admitting cached model rows. */
3
53
  export declare function isTimeBasedCost(value: unknown): value is TimeBasedCost;
4
54
  /** KDL object children are named records; normalize them once while constructing the model. */
5
55
  export declare function materializeTimeBasedCost(value: unknown): TimeBasedCost;
56
+ /**
57
+ * The dated card due at `timestamp`: the one with the latest `effectiveFrom`
58
+ * at or before it, in any order, or undefined before the first card.
59
+ */
60
+ export declare function dueDatedCard<T extends {
61
+ effectiveFrom: number;
62
+ }>(cards: readonly T[], timestamp: number): T | undefined;
6
63
  /**
7
64
  * The latest `effectiveRates` card of a materialized schedule due at
8
65
  * `timestamp`, or undefined before the first one (or without a schedule).
9
66
  */
10
67
  export declare function dueEffectiveRate(schedule: TimeBasedCost | undefined, timestamp: number): EffectiveTokenCost | undefined;
11
- /** A `cost-patch`/`cost-fallback` dated card: the legs it overwrites from `effectiveFrom` (Unix ms). */
68
+ /**
69
+ * A dated `effective-rates` card of a `cost-patch`, `cost-fallback` or
70
+ * `credit-rates` rule: the legs it overwrites from `effectiveFrom` (Unix ms).
71
+ */
12
72
  export interface DatedCostCard extends Partial<TokenCost> {
13
73
  effectiveFrom: number;
14
74
  }
15
75
  /**
16
- * Parse the compiled `effective-rates` block of a `cost-patch` or
17
- * `cost-fallback` payload into dated cards, ascending by date. Throws on a
18
- * malformed card, so a typo in a rule fails the build instead of silently
19
- * leaving the old price in force.
76
+ * Parse the compiled `effective-rates` block of a `cost-patch`,
77
+ * `cost-fallback` or Factory Droid `credit-rates` payload into dated cards,
78
+ * ascending by date. Throws on a malformed card, so a typo in a rule fails the
79
+ * build instead of silently leaving the old price in force.
20
80
  */
21
- export declare function parseDatedCostCards(costAxis: unknown, axis: "cost-patch" | "cost-fallback"): DatedCostCard[];
81
+ export declare function parseDatedCostCards(costAxis: unknown, axis: "cost-patch" | "cost-fallback" | "credit-rates"): DatedCostCard[];
22
82
  /**
23
83
  * Earliest instant a rate card can name (the `Date` range minimum). A leading
24
84
  * card from here holds the price in force before the first dated change.
@@ -740,3 +740,21 @@ export declare function singularityApiDevModelManagerOptions(config?: Singularit
740
740
  * reviewed KDL rules own the wire shape, limits patch, and effort ladder.
741
741
  */
742
742
  export declare function singularityApiTechModelManagerOptions(config?: SingularityApiModelManagerConfig): ModelManagerOptions<Api>;
743
+ /**
744
+ * Scaleway discovery configuration: the API key plus optional base-URL and
745
+ * fetch overrides. Consumed by {@link scalewayModelManagerOptions}, and
746
+ * exported for extensions and tests that construct the manager directly.
747
+ */
748
+ export interface ScalewayModelManagerConfig {
749
+ apiKey?: string;
750
+ baseUrl?: string;
751
+ fetch?: FetchImpl;
752
+ }
753
+ /**
754
+ * Scaleway model manager: plain OpenAI-compatible chat completions at
755
+ * `api.scaleway.ai/v1` (fr-par serverless Generative APIs). `/v1/models`
756
+ * returns bare ids, so the authored seed in `providers/scaleway.kdl` is the
757
+ * reference each live row merges names, modalities, prices, and limits from;
758
+ * a successful fetch is the complete roster and prunes retired rows.
759
+ */
760
+ export declare function scalewayModelManagerOptions(config?: ScalewayModelManagerConfig): ModelManagerOptions<"openai-completions">;
@@ -1,5 +1,6 @@
1
1
  import type { ModelIdentity } from "./compat/types.js";
2
2
  import type { Effort } from "./effort.js";
3
+ import type { DatedCostCard } from "./pricing.js";
3
4
  export type { FetchImpl } from "@oh-my-pi/pi-utils";
4
5
  export type { KnownProvider } from "./provider-models/descriptors.js";
5
6
  export type KnownApi = "openai-completions" | "openai-responses" | "openrouter" | "openai-codex-responses" | "azure-openai-responses" | "anthropic-messages" | "bedrock-converse-stream" | "google-generative-ai" | "google-gemini-cli" | "google-vertex" | "ollama-chat" | "cursor-agent" | "factory-droid-agent" | "gitlab-duo-agent" | "devin-agent" | "apple-foundation-models";
@@ -1198,12 +1199,12 @@ export interface ModelCost extends TokenCost {
1198
1199
  /**
1199
1200
  * Exact local content tokenizer family for a model.
1200
1201
  *
1201
- * Absent means no first-party local tokenizer is known and consumers retain
1202
- * their estimate/default-tokenizer policy. The values name tokenizer
1202
+ * Absent means no first-party local tokenizer is known and consumers count
1203
+ * in o200k_base. The values name tokenizer
1203
1204
  * generations rather than providers: DeepSeek V3 through V4 share
1204
1205
  * `"deepseek-v3"`; Kimi K2 through K3 share `"kimi-k2"`.
1205
1206
  */
1206
- export type ModelTokenizer = "claude-v3" | "claude-v47" | "claude-v5" | "claude-v5-sonnet" | "qwen3" | "deepseek-v3" | "kimi-k2" | "glm5" | "o200k";
1207
+ export type ModelTokenizer = "claude-v3" | "claude-v47" | "claude-v5" | "claude-v5-sonnet" | "qwen3" | "deepseek-v3" | "kimi-k2" | "glm5" | "cl100k" | "o200k";
1207
1208
  /** One account's discovered entitlements on a model; see {@link Model.accountAccess}. */
1208
1209
  export interface ModelAccountAccess {
1209
1210
  /**
@@ -1393,8 +1394,12 @@ export interface Model<TApi extends Api = Api> {
1393
1394
  factoryDroidApiProviders?: string[];
1394
1395
  /** Factory Droid: `configured_order` when live routing chose the rotation; absent means registry order. */
1395
1396
  factoryDroidRoutingSource?: "configured_order";
1397
+ /** Factory Droid: server-side refusal fallbacks the account can run; absent when none are named. */
1398
+ factoryDroidRefusalFallbacks?: string[];
1396
1399
  /** Factory Droid Standard Credits base rate (relative per-token weight, not dollars). */
1397
1400
  factoryDroidCredits?: number;
1401
+ /** Factory Droid dated `credit-rates` cards, ascending; `getFactoryDroidCreditRate` picks the one due now. */
1402
+ factoryDroidCreditCards?: readonly DatedCostCard[];
1398
1403
  /** Canonical organization resolved during Factory discovery. */
1399
1404
  factoryDroidOrgId?: string;
1400
1405
  cost: ModelCost;
@@ -1467,8 +1472,9 @@ export interface Model<TApi extends Api = Api> {
1467
1472
  useResponsesLite?: boolean;
1468
1473
  /**
1469
1474
  * Service-tier ids the provider advertises for this model (Codex discovery
1470
- * `service_tiers[].id`, e.g. `priority`, `ultrafast`). Absent when the
1471
- * provider publishes no per-model tier list.
1475
+ * `service_tiers[].id`, or an OpenAI-compatible `/v1/models` row's
1476
+ * `service_tiers`, e.g. `priority`, `ultrafast`). Absent when the provider
1477
+ * publishes no per-model tier list.
1472
1478
  */
1473
1479
  serviceTiers?: readonly string[];
1474
1480
  /** Preferred model to switch to when context promotion is triggered (model id or provider/id). */
@@ -11,6 +11,14 @@ export declare function toPositiveNumber(value: unknown, fallback: number | null
11
11
  /** Positive finite number, or `null` when the value is missing/non-positive. */
12
12
  export declare function toPositiveNumberOrNull(value: unknown): number | null;
13
13
  export declare function toBoolean(value: unknown): boolean | undefined;
14
+ /**
15
+ * Service-tier ids from a discovery row's `service_tiers` array, deduplicated
16
+ * in advertisement order. Entries may be codex-rs `{ id, name, description }`
17
+ * objects or bare id strings (OpenAI-compatible relays). Returns `undefined`
18
+ * when the value is not an array; an empty array is kept, since it is a
19
+ * statement that the model offers no optional tier.
20
+ */
21
+ export declare function parseServiceTierIds(value: unknown): string[] | undefined;
14
22
  export declare function isAnthropicOAuthToken(key: string): boolean;
15
23
  /**
16
24
  * Normalize a model display name: drop the gateway author prefix and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-catalog",
3
- "version": "18.14.2",
3
+ "version": "18.16.0",
4
4
  "description": "Model catalog for omp: bundled model database, provider discovery descriptors, model identity, classification, and equivalence",
5
5
  "keywords": [
6
6
  "ai",
@@ -88,12 +88,12 @@
88
88
  "gen:proto": "bun scripts/generate-protocols.ts"
89
89
  },
90
90
  "dependencies": {
91
- "@oh-my-pi/omptype": "18.14.2",
92
- "@oh-my-pi/pi-utils": "18.14.2"
91
+ "@oh-my-pi/omptype": "18.16.0",
92
+ "@oh-my-pi/pi-utils": "18.16.0"
93
93
  },
94
94
  "devDependencies": {
95
95
  "@bgotink/kdl": "0.4.0",
96
- "@oh-my-pi/pi-ai": "18.14.2",
96
+ "@oh-my-pi/pi-ai": "18.16.0",
97
97
  "@types/bun": "^1.3.14"
98
98
  },
99
99
  "engines": {
@@ -53,6 +53,7 @@ export type AuthProviderId =
53
53
  | "minimax-code"
54
54
  | "minimax-code-cn"
55
55
  | "mistral"
56
+ | "modular"
56
57
  | "moonshot"
57
58
  | "muse-code"
58
59
  | "nanogpt"
@@ -72,6 +73,7 @@ export type AuthProviderId =
72
73
  | "qwen-portal"
73
74
  | "sakana"
74
75
  | "sambanova"
76
+ | "scaleway"
75
77
  | "siliconflow"
76
78
  | "siliconflow-cn"
77
79
  | "singularityapi-dev"
@@ -143,6 +145,8 @@ export type LoginProviderId =
143
145
  | "meta"
144
146
  | "minimax-code"
145
147
  | "minimax-code-cn"
148
+ | "mistral"
149
+ | "modular"
146
150
  | "moonshot"
147
151
  | "muse-code"
148
152
  | "nanogpt"
@@ -161,6 +165,7 @@ export type LoginProviderId =
161
165
  | "qwen-portal"
162
166
  | "sakana"
163
167
  | "sambanova"
168
+ | "scaleway"
164
169
  | "siliconflow"
165
170
  | "siliconflow-cn"
166
171
  | "singularityapi-dev"
@@ -109,6 +109,7 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
109
109
  "preserved",
110
110
  "interleaved",
111
111
  ]),
112
+ "completions-allow-missing-thinking": wire("completionsAllowMissingThinking", ["request"]),
112
113
  "anthropic-thinking": wire("anthropicThinking", ["request"], "scalar", [
113
114
  "adaptive",
114
115
  "adaptive-summarized",
@@ -119,7 +120,7 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
119
120
  "openai-platform-header": wire("openaiPlatformHeader", ["request"]),
120
121
  "responses-cache-retention": wire("responsesCacheRetention", ["request"]),
121
122
  "responses-verbosity": wire("responsesVerbosity", ["request"], "scalar", ["low"]),
122
- "responses-service-tier": wire("responsesServiceTier", ["request"], "scalar", ["priority"]),
123
+ "responses-service-tier": wire("responsesServiceTier", ["request"], "scalar", ["priority", "ultrafast"]),
123
124
  "responses-parallel-tool-calls": wire("responsesParallelToolCalls", ["request"]),
124
125
  "responses-safety-identifier": wire("responsesSafetyIdentifier", ["request"]),
125
126
  "responses-tool-choice-auto": wire("responsesToolChoiceAuto", ["request"]),
@@ -672,7 +673,9 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
672
673
  "region-limits-eu": { key: "regionLimitsEu", set: "catalog", shape: "object" },
673
674
  /**
674
675
  * Subscription credit rates: `input` is the per-token credit weight shown
675
- * as the model's multiplier; `output` and `cache-read` multiply it.
676
+ * as the model's multiplier; `output` and `cache-read` multiply it. An
677
+ * `effective-rates` block of dated cards overwrites them from each card's
678
+ * `effective-from`, as on `cost-patch`.
676
679
  */
677
680
  "credit-rates": { key: "creditRates", set: "catalog", shape: "object" },
678
681
  /**
@@ -1,29 +1,33 @@
1
1
  import { withoutUnverifiedThinking } from "../model-thinking";
2
2
  import type { CatalogPolicyInputs } from "../types";
3
- import { resolveCatalogAxes } from "./resolve";
3
+ import { type ResolveRoute, resolveCatalogAxes } from "./resolve";
4
4
 
5
5
  /**
6
6
  * Rule-owned catalog-axis policy by model. Resolve once per process rather
7
7
  * than walking the static policy cascade on every accessor call. The key
8
8
  * mirrors the cascade target inputs — `providerType ?? provider`, api, the
9
9
  * logical id plus `requestModelId` (the wire id `policyModelId` resolves
10
- * unknown logical ids under), and the reasoning flag; identity-derived
11
- * class/family/revision facts are a pure function of provider + id. Bounded:
12
- * one entry per distinct model.
10
+ * unknown logical ids under), the reasoning flag, and the route's upstream;
11
+ * identity-derived class/family/revision facts are a pure function of
12
+ * provider + id. Bounded: one entry per distinct model and upstream.
13
13
  */
14
14
  const catalogPolicyCache = new Map<string, Readonly<Record<string, unknown>>>();
15
15
  const CATALOG_POLICY_CACHE_MAX = 8192;
16
16
 
17
17
  /**
18
- * The `catalog` axis bag authored in the KDL cascade rules for a model,
19
- * memoized per distinct model. Frozen: the bag is shared across callers, so it
20
- * must stay read-only — extract the value you need rather than handing it on.
18
+ * The `catalog` axis bag authored in the KDL cascade rules for a model (on one
19
+ * upstream when `route` names it), memoized per distinct model. Frozen: the bag
20
+ * is shared across callers, so it must stay read-only — extract the value you
21
+ * need rather than handing it on.
21
22
  */
22
- export function resolveCatalogPolicy(model: CatalogPolicyInputs): Readonly<Record<string, unknown>> {
23
- const key = `${model.provider}\u0000${model.providerType ?? ""}\u0000${model.api}\u0000${model.id}\u0000${model.requestModelId ?? ""}\u0000${model.reasoning ? 1 : 0}`;
23
+ export function resolveCatalogPolicy(
24
+ model: CatalogPolicyInputs,
25
+ route?: ResolveRoute,
26
+ ): Readonly<Record<string, unknown>> {
27
+ const key = `${model.provider}\u0000${model.providerType ?? ""}\u0000${model.api}\u0000${model.id}\u0000${model.requestModelId ?? ""}\u0000${model.reasoning ? 1 : 0}\u0000${route?.upstream ?? ""}`;
24
28
  const cached = catalogPolicyCache.get(key);
25
29
  if (cached !== undefined) return cached;
26
- const policy = Object.freeze({ ...resolveCatalogAxes(withoutUnverifiedThinking(model)) });
30
+ const policy = Object.freeze({ ...resolveCatalogAxes(withoutUnverifiedThinking(model), route) });
27
31
  if (catalogPolicyCache.size >= CATALOG_POLICY_CACHE_MAX) catalogPolicyCache.clear();
28
32
  catalogPolicyCache.set(key, policy);
29
33
  return policy;
@@ -240,7 +240,7 @@ function compiledTemplate(compiled: CompiledVariantFamily): VariantFamilyTemplat
240
240
  const alternatives = [family.id, ...family.members, ...(family.extraAliases ?? [])].map(id =>
241
241
  id
242
242
  .split(REVISION_PLACEHOLDER)
243
- .map(part => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
243
+ .map(part => RegExp.escape(part))
244
244
  .join(REVISION_CAPTURE),
245
245
  );
246
246
  const template: VariantFamilyTemplate = {
@@ -6,6 +6,7 @@
6
6
  * to bind a model to the account serving it.
7
7
  */
8
8
  import { isRecord, once } from "@oh-my-pi/pi-utils";
9
+ import { type DatedCostCard, parseDatedCostCards } from "../pricing";
9
10
  import type { Api, ModelSpec } from "../types";
10
11
  import { FACTORY_DROID_WIRES, type FactoryDroidRegion, type FactoryDroidWire } from "../wire/factory-droid";
11
12
  import { apiRouteFor } from "./behavior";
@@ -20,11 +21,16 @@ export interface FactoryDroidLimits {
20
21
  maxTokens: number | null;
21
22
  }
22
23
 
23
- /** Standard Credits rates: `input` is the per-token weight; `output` and `cacheRead` multiply it. */
24
+ /**
25
+ * Standard Credits rates: `input` is the per-token weight; `output` and
26
+ * `cacheRead` multiply it. `effectiveRates` are dated cards (ascending) that
27
+ * overwrite those legs from their date, as on `cost-patch`.
28
+ */
24
29
  export interface FactoryDroidCreditRates {
25
30
  input: number;
26
31
  output?: number;
27
32
  cacheRead?: number;
33
+ effectiveRates?: readonly DatedCostCard[];
28
34
  }
29
35
 
30
36
  /** Account gates on one registry model (the `entitlement` axis). */
@@ -95,6 +101,7 @@ function readPolicy(spec: ModelSpec<"factory-droid-agent">): FactoryDroidModelPo
95
101
  const euMaxTokens = numberField(euLimits, "maxTokens");
96
102
  const creditOutput = numberField(credits, "output");
97
103
  const creditCacheRead = numberField(credits, "cacheRead");
104
+ const creditCards = parseDatedCostCards(credits, "credit-rates");
98
105
  const featureFlag = stringField(entitlement, "featureFlag");
99
106
  const deprecationFlag = stringField(entitlement, "deprecationFlag");
100
107
  const baseVariant = stringField(entitlement, "baseVariant");
@@ -116,6 +123,7 @@ function readPolicy(spec: ModelSpec<"factory-droid-agent">): FactoryDroidModelPo
116
123
  input: creditInput,
117
124
  ...(creditOutput !== undefined && { output: creditOutput }),
118
125
  ...(creditCacheRead !== undefined && { cacheRead: creditCacheRead }),
126
+ ...(creditCards.length > 0 && { effectiveRates: creditCards }),
119
127
  },
120
128
  }),
121
129
  ...(priceProvider !== undefined && {
@@ -50,6 +50,7 @@ export type KnownProvider =
50
50
  | "minimax-code"
51
51
  | "minimax-code-cn"
52
52
  | "mistral"
53
+ | "modular"
53
54
  | "moonshot"
54
55
  | "muse-code"
55
56
  | "nanogpt"
@@ -66,6 +67,7 @@ export type KnownProvider =
66
67
  | "qwen-portal"
67
68
  | "sakana"
68
69
  | "sambanova"
70
+ | "scaleway"
69
71
  | "siliconflow"
70
72
  | "siliconflow-cn"
71
73
  | "singularityapi-dev"
@@ -1337,8 +1337,10 @@ export function resolveDiscoveryApi(spec: ModelSpec<Api>, providerType: string):
1337
1337
  * {@link resolveModelPolicy} without resolving the compat and thinking
1338
1338
  * policies, which dominate its cost on catalog-wide scans.
1339
1339
  */
1340
- export function resolveCatalogAxes(spec: CatalogPolicyInputs): Record<string, unknown> {
1341
- return resolveCascade(buildResolveTarget(spec, resolveIdentity(spec))).catalog;
1340
+ export function resolveCatalogAxes(spec: CatalogPolicyInputs, route?: ResolveRoute): Record<string, unknown> {
1341
+ const target = buildResolveTarget(spec, resolveIdentity(spec));
1342
+ target.upstream = route?.upstream;
1343
+ return resolveCascade(target).catalog;
1342
1344
  }
1343
1345
 
1344
1346
  /**
@@ -76,6 +76,16 @@ export function compareRevision(a: Revision, b: Revision): number {
76
76
  return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
77
77
  }
78
78
 
79
+ /** Whether a revision string parses and is at or above `floor`; `undefined` or unparsable is never. */
80
+ export function revisionAtLeast(revision: string | undefined, floor: string): boolean {
81
+ if (revision === undefined) return false;
82
+ const parsedRevision = parseRevision(revision);
83
+ const parsedFloor = parseRevision(floor);
84
+ return (
85
+ parsedRevision !== undefined && parsedFloor !== undefined && compareRevision(parsedRevision, parsedFloor) >= 0
86
+ );
87
+ }
88
+
79
89
  /** Renders a revision as its canonical `major.minor.patch` string. */
80
90
  export function formatRevision(revision: Revision): string {
81
91
  return `${revision[0]}.${revision[1]}.${revision[2]}`;