@linxiraos/pi-catalog 1.1.25 → 1.1.27

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 (61) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/types/compat/auth-ids.d.ts +2 -2
  3. package/dist/types/compat/collapse.d.ts +7 -0
  4. package/dist/types/compat/image-tokenization.d.ts +52 -0
  5. package/dist/types/compat/provider-ids.d.ts +1 -1
  6. package/dist/types/hosts.d.ts +4 -0
  7. package/dist/types/model-cache.d.ts +6 -0
  8. package/dist/types/provider-models/special.d.ts +6 -0
  9. package/dist/types/types.d.ts +29 -4
  10. package/dist/types/wire/gemini-headers.d.ts +6 -3
  11. package/package.json +4 -4
  12. package/src/compat/auth-ids.ts +2 -0
  13. package/src/compat/axes.ts +6 -2
  14. package/src/compat/collapse.d.ts +7 -0
  15. package/src/compat/collapse.ts +11 -0
  16. package/src/compat/image-tokenization.ts +166 -0
  17. package/src/compat/provider-ids.d.ts +1 -1
  18. package/src/compat/provider-ids.ts +1 -0
  19. package/src/compat/resolve.ts +20 -23
  20. package/src/compat/rules/README.md +1 -1
  21. package/src/compat/rules/auth/_order.kdl +1 -1
  22. package/src/compat/rules/auth/snowflake.kdl +11 -0
  23. package/src/compat/rules/classes/anthropic.kdl +102 -7
  24. package/src/compat/rules/classes/deepseek.kdl +5 -3
  25. package/src/compat/rules/classes/gemini.kdl +12 -0
  26. package/src/compat/rules/classes/glm.kdl +1 -1
  27. package/src/compat/rules/classes/kimi.kdl +7 -0
  28. package/src/compat/rules/classes/meta.kdl +4 -4
  29. package/src/compat/rules/classes/openai.kdl +109 -1
  30. package/src/compat/rules/providers/amazon-bedrock.kdl +0 -7
  31. package/src/compat/rules/providers/cursor.kdl +6 -8
  32. package/src/compat/rules/providers/google-antigravity.kdl +8 -1
  33. package/src/compat/rules/providers/image-tokenization.kdl +42 -0
  34. package/src/compat/rules/providers/meta.kdl +3 -3
  35. package/src/compat/rules/providers/muse-code.kdl +7 -0
  36. package/src/compat/rules/providers/ollama.kdl +3 -0
  37. package/src/compat/rules/providers/openai-codex.kdl +1 -0
  38. package/src/compat/rules/providers/opencode-go.kdl +11 -0
  39. package/src/compat/rules/providers/snowflake.kdl +135 -0
  40. package/src/compat/rules/providers/tool-result-images.kdl +16 -0
  41. package/src/compat/rules/runtime/behavior.kdl +1 -1
  42. package/src/compat/rules/taxonomy/_collapse.kdl +34 -2
  43. package/src/compat/rules/taxonomy/deepseek.kdl +4 -0
  44. package/src/compat/rules/taxonomy/kimi.kdl +5 -0
  45. package/src/compat/rules.json +1 -1
  46. package/src/discovery/cursor.ts +26 -3
  47. package/src/hosts.d.ts +4 -0
  48. package/src/hosts.ts +2 -0
  49. package/src/model-cache.d.ts +6 -0
  50. package/src/model-cache.ts +10 -0
  51. package/src/models.json +1 -1
  52. package/src/provider-models/cache-provider-id.ts +10 -4
  53. package/src/provider-models/cline-pass.ts +33 -0
  54. package/src/provider-models/google.ts +2 -0
  55. package/src/provider-models/openai-compat.ts +70 -15
  56. package/src/provider-models/special.d.ts +6 -0
  57. package/src/provider-models/special.ts +11 -4
  58. package/src/types.d.ts +29 -4
  59. package/src/types.ts +34 -3
  60. package/src/wire/gemini-headers.d.ts +6 -3
  61. package/src/wire/gemini-headers.ts +39 -21
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.1.27] - 2026-10-07
6
+
7
+ - 版本线推进至 1.1.27;本版无独立用户可见变化。
8
+
9
+ ### Added
10
+
11
+ - Added an optional `statefulResponses` compat field for OpenAI Responses models, kept through OpenRouter's Responses dispatch ([#13686](https://github.com/can1357/oh-my-pi/pull/13686) by [@alphastorm](https://github.com/alphastorm)).
12
+ - Added 15 Snowflake Cortex models with account-specific endpoints, Cortex compatibility rules, and estimated account-billed pricing ([#14507](https://github.com/can1357/oh-my-pi/pull/14507) by [@jorgoose](https://github.com/jorgoose)).
13
+ - Added the `image-tokenization` axis, which declares how GPT-5.2+, Claude and Gemini 3 lines bill input images on every host, with wire-API fallback rules for other models, plus `imageTokens()` to price one image ([#14286](https://github.com/can1357/oh-my-pi/pull/14286) by [@will-bogusz](https://github.com/will-bogusz)).
14
+
15
+ ### Changed
16
+
17
+ - Changed OpenAI Responses endpoints other than OpenAI, Azure OpenAI, and Codex (custom and local servers, proxies including `azure`/`openai-codex` providers pointed at a non-Azure/non-Codex `baseUrl`, OpenRouter) to default `supportsImageDetailOriginal` to `false`, so snapcompact frames and computer screenshots go out as `detail: "auto"` instead of failing on servers that reject `original`; set `compat.supportsImageDetailOriginal: true` to opt a host in ([#13687](https://github.com/can1357/oh-my-pi/pull/13687) by [@alphastorm](https://github.com/alphastorm)).
18
+ - Muse Code can now store Responses results on Meta's side (`store-responses`), so a turn whose connection drops can be recovered instead of re-run. Storage is opt-in via the omp setting `providers.muse-code.storeResponses` or `PI_MUSE_STORE_RESPONSES=1` ([#14293](https://github.com/can1357/oh-my-pi/pull/14293) and [#14534](https://github.com/can1357/oh-my-pi/pull/14534) by [@abilliontokens](https://github.com/abilliontokens)).
19
+
5
20
  ## [1.1.25] - 2026-10-03
6
21
 
7
22
  - 版本线推进至 1.1.25;本版无独立用户可见变化。
@@ -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" | "commandcode" | "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" | "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" | "siliconflow" | "siliconflow-cn" | "singularityapi-dev" | "singularityapi-tech" | "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" | "commandcode" | "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" | "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" | "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" | "commandcode" | "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" | "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" | "singularityapi-dev" | "singularityapi-tech" | "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" | "commandcode" | "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" | "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" | "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";
@@ -102,6 +102,13 @@ export interface VariantCollapseTable {
102
102
  * Discovery uses this accessor to preserve injectable table seams.
103
103
  */
104
104
  export declare function reviewedCollapseTable(provider: string): VariantCollapseTable | undefined;
105
+ /**
106
+ * Logical id of the reviewed `provider` family claiming `memberId` as a member
107
+ * or extra alias, or `undefined` when no reviewed family owns it. Cursor's rich
108
+ * discovery names lanes with it so they keep the id the legacy-slug collapse
109
+ * (and the bundled catalog) assigns.
110
+ */
111
+ export declare function reviewedVariantFamilyId(provider: string, memberId: string): string | undefined;
105
112
  /**
106
113
  * The global automatic rule: derive an `X` + `X-thinking` family for every
107
114
  * pair where both ids are live in `specs` (trailing or infix token). Gates:
@@ -0,0 +1,52 @@
1
+ import type { ModelIdentity } from "./types.js";
2
+ /** OpenAI image `detail` levels; `undefined` means the provider default (`auto`). */
3
+ export type ImageDetail = "auto" | "low" | "high" | "original";
4
+ export interface ImageSize {
5
+ width: number;
6
+ height: number;
7
+ }
8
+ /** One OpenAI `detail` level's sizing: a pixel-dimension limit, then an optional patch budget. */
9
+ export interface PatchSizing {
10
+ /** Longest side after the pixel-dimension fit. */
11
+ maxEdge: number;
12
+ /** Resizing patch budget; absent when the level keeps the image's size. */
13
+ patchBudget?: number;
14
+ }
15
+ /** OpenAI 32px patches × a per-model multiplier, sized per `detail` level. */
16
+ export interface OpenAiPatchTokenization {
17
+ regime: "openai-patch";
18
+ multiplier: number;
19
+ low: PatchSizing;
20
+ high: PatchSizing;
21
+ original: PatchSizing;
22
+ /** The level `auto` (and an omitted `detail`) sizes like. */
23
+ auto: "high" | "original";
24
+ }
25
+ /** One model line's image billing rule: the vendor formula and its per-model numbers. */
26
+ export type ImageTokenization = OpenAiPatchTokenization
27
+ /** Anthropic 28px patches, resized to fit a padded-edge limit and a visual-token budget. */
28
+ | {
29
+ regime: "anthropic-patch";
30
+ maxEdge: number;
31
+ maxTokens: number;
32
+ }
33
+ /** A fixed per-image budget regardless of pixels (Gemini 3 `media_resolution`). */
34
+ | {
35
+ regime: "fixed";
36
+ tokens: number;
37
+ };
38
+ /** What reads the image: a built model, any `{ id }` with optional host and identity, or just a wire `{ api }`. */
39
+ export interface ImageTokenizationTarget {
40
+ id?: string;
41
+ provider?: string;
42
+ api?: string;
43
+ identity?: Pick<ModelIdentity, "class" | "family" | "revision">;
44
+ }
45
+ /**
46
+ * The reader's image billing rule: its lineage rule, else its wire API's
47
+ * fallback; undefined when neither covers it (an unclassified model on an
48
+ * unregistered API).
49
+ */
50
+ export declare function resolveImageTokenization(target: ImageTokenizationTarget): ImageTokenization | undefined;
51
+ /** Billed input tokens for one image of `size` under `rule`; only `openai-patch` reads `detail`. */
52
+ export declare function imageTokens(rule: ImageTokenization, size: ImageSize, detail?: ImageDetail): number;
@@ -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" | "commandcode" | "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" | "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" | "siliconflow" | "siliconflow-cn" | "singularityapi-dev" | "singularityapi-tech" | "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" | "commandcode" | "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" | "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" | "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";
@@ -15,6 +15,10 @@ export declare const KNOWN_HOSTS: {
15
15
  readonly providers: readonly ["openai"];
16
16
  readonly urlMarkers: readonly ["api.openai.com"];
17
17
  };
18
+ /** URL-only: a Codex provider rerouted through a proxy does not imply the subscription backend's capabilities. */
19
+ readonly openaiCodex: {
20
+ readonly urlMarkers: readonly ["chatgpt.com/backend-api", "chat.openai.com/backend-api"];
21
+ };
18
22
  readonly azureOpenAI: {
19
23
  readonly providers: readonly ["azure"];
20
24
  readonly urlMarkers: readonly [".openai.azure.com", "azure.com/openai", "models.inference.ai.azure.com"];
@@ -35,6 +35,12 @@ export interface CacheEntry<TApi extends Api = Api> {
35
35
  */
36
36
  staticFingerprint: string;
37
37
  }
38
+ /**
39
+ * Closes the shared handle to the default `<agent-dir>/models.db`; the next
40
+ * default-path access reopens it. Call before deleting an agent directory the
41
+ * cache was opened under — Windows cannot remove a database that is still open.
42
+ */
43
+ export declare function closeModelCache(): void;
38
44
  export declare function readModelCache<TApi extends Api>(providerId: string, ttlMs: number, now: () => number, dbPath?: string): CacheEntry<TApi> | null;
39
45
  /**
40
46
  * Persist a provider snapshot.
@@ -24,6 +24,12 @@ export interface OpenAICodexModelManagerConfig {
24
24
  * keeps the previous/bundled catalog instead.
25
25
  */
26
26
  resolveAccounts?: () => Promise<readonly OpenAICodexAccount[] | null>;
27
+ /**
28
+ * Codex backend base URL (e.g. a Codex-compatible gateway from `models.yml`).
29
+ * Defaults to the official ChatGPT backend. Also scopes the discovery cache,
30
+ * so a gateway roster never serves the official endpoint and vice versa.
31
+ */
32
+ baseUrl?: string;
27
33
  clientVersion?: string;
28
34
  fetch?: FetchImpl;
29
35
  }
@@ -378,6 +378,8 @@ export interface OpenAICompat {
378
378
  supportsReasoningParams?: boolean;
379
379
  /** Whether Responses requests may include `reasoning.summary`. Default: true except on known incompatible hosts. */
380
380
  supportsReasoningSummary?: boolean;
381
+ /** Whether to chain OpenAI Responses turns using stored response ids. Unset uses the official-endpoint default; env and call options take precedence. */
382
+ statefulResponses?: boolean;
381
383
  /**
382
384
  * Whether the endpoint accepts explicit sampling parameters (`temperature`,
383
385
  * `top_p`, `top_k`, `min_p`, penalties). OpenAI proprietary reasoning models
@@ -398,7 +400,7 @@ export interface OpenAICompat {
398
400
  alwaysSendMaxTokens?: boolean;
399
401
  /** Whether Responses-API tool-call/result history must be strictly paired. Default: auto-detected (Azure OpenAI, GitHub Copilot). */
400
402
  strictResponsesPairing?: boolean;
401
- /** Whether the Responses API accepts the `detail: "original"` image hint. Default: auto-detected (false for GitHub Copilot, which rejects it with a 400). */
403
+ /** Whether the Responses API accepts the `detail: "original"` image hint. Default: true for OpenAI, Azure OpenAI, and Codex; false for other hosts. Explicit overrides win. */
402
404
  supportsImageDetailOriginal?: boolean;
403
405
  /**
404
406
  * Whether the Responses endpoint accepts `configuration_update` input items
@@ -651,6 +653,12 @@ export interface BedrockCompat {
651
653
  * `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` alias, then the 300s default.
652
654
  */
653
655
  streamIdleTimeoutMs?: number;
656
+ /**
657
+ * Whether the served model accepts explicit sampling parameters
658
+ * (`temperature`, `topP`, …). Class rules set `false` for model lines that
659
+ * reject them on every host. Unassigned: accepted.
660
+ */
661
+ supportsSamplingParams?: boolean;
654
662
  /**
655
663
  * Whether the model accepts a forced `toolChoice` (`any` / `tool`). Claude
656
664
  * Opus/Sonnet 5.5 reject it outright; the request builder downgrades forced
@@ -666,6 +674,8 @@ export interface ResolvedBedrockCompat {
666
674
  supportsLongPromptCacheRetention: boolean;
667
675
  promptCacheMinimumTokens: number;
668
676
  promptCacheMaximumCheckpoints: number;
677
+ /** See {@link BedrockCompat.supportsSamplingParams}. */
678
+ supportsSamplingParams?: boolean;
669
679
  supportsForcedToolChoice: boolean;
670
680
  /**
671
681
  * Stream-watchdog idle-timeout fallback in ms for hosts with no keepalive
@@ -788,7 +798,7 @@ export interface ResolvedOpenAISharedCompat {
788
798
  * `buildModel`; request handlers read fields and never detect, resolve, or
789
799
  * allocate.
790
800
  */
791
- 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" | "mistralReasoningContentParts" | "requiresReasoningContentForToolCalls" | "requiresReasoningContentForAllAssistantTurns" | "allowsSyntheticReasoningContentForToolCalls" | "syntheticReasoningContentFallback" | "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" | "supportsSteering" | "stripImageInput" | "thinkingLoopGuard" | "whenThinking">> & {
801
+ export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat & Required<Omit<OpenAICompat, "supportsDeveloperRole" | "supportsReasoningEffort" | "reasoningEffortMap" | "supportsReasoningParams" | "supportsReasoningSummary" | "statefulResponses" | "supportsSamplingParams" | "supportsPenaltyAndStopParams" | "thinkingFormat" | "kimiApiFormat" | "reasoningDisableMode" | "omitReasoningEffort" | "includeEncryptedReasoning" | "filterReasoningHistory" | "disableReasoningOnForcedToolChoice" | "disableReasoningOnToolChoice" | "disableReasoningWithTools" | "supportsToolChoice" | "supportsForcedToolChoice" | "supportsNamedToolChoice" | "reasoningContentField" | "mistralReasoningContentParts" | "requiresReasoningContentForToolCalls" | "requiresReasoningContentForAllAssistantTurns" | "allowsSyntheticReasoningContentForToolCalls" | "syntheticReasoningContentFallback" | "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" | "supportsSteering" | "stripImageInput" | "thinkingLoopGuard" | "whenThinking">> & {
792
802
  vercelGatewayRouting?: OpenAICompat["vercelGatewayRouting"];
793
803
  extraBody?: OpenAICompat["extraBody"];
794
804
  cacheControlFormat?: OpenAICompat["cacheControlFormat"];
@@ -835,6 +845,16 @@ export interface ResolvedOpenAIResponsesCompat extends ResolvedOpenAISharedCompa
835
845
  * filling `"auto"`.
836
846
  */
837
847
  supportsReasoningSummary: boolean;
848
+ /** Optional chaining override; unset falls back to officialEndpoint at request time. */
849
+ statefulResponses?: boolean;
850
+ /**
851
+ * Whether the host can store Responses results server-side (`store: true`).
852
+ * Rule-owned: hosts that keep generating after a client disconnect so a
853
+ * dropped stream can resume via `GET /responses/{id}`. Storage is opt-in
854
+ * per request (`storeResponses` stream option, else
855
+ * `PI_MUSE_STORE_RESPONSES`); stored runs retain prompts and outputs on the provider.
856
+ */
857
+ storeResponses: boolean;
838
858
  streamIdleTimeoutMs?: number;
839
859
  vercelGatewayRouting?: OpenAICompat["vercelGatewayRouting"];
840
860
  /** The model sits behind Vercel AI Gateway's Responses endpoint. */
@@ -918,9 +938,11 @@ export interface DevinCompat {
918
938
  modelRouter?: boolean;
919
939
  /** Whether the upstream model supports native parallel tool calls. */
920
940
  supportsParallelToolCalls?: boolean;
941
+ /** See {@link BedrockCompat.supportsSamplingParams}. */
942
+ supportsSamplingParams?: boolean;
921
943
  }
922
944
  /** Fully-resolved devin-agent compat view. */
923
- export type ResolvedDevinCompat = Required<DevinCompat>;
945
+ export type ResolvedDevinCompat = Required<Omit<DevinCompat, "supportsSamplingParams">> & Pick<DevinCompat, "supportsSamplingParams">;
924
946
  /**
925
947
  * Compatibility settings for the Google API family (google-generative-ai,
926
948
  * google-vertex, google-gemini-cli). Class-driven defaults come from the
@@ -955,13 +977,16 @@ export interface GoogleCompat {
955
977
  stripImageInput?: boolean;
956
978
  /** Thinking-loop watchdog guard family applied to streamed reasoning. */
957
979
  thinkingLoopGuard?: "gemini" | "deepseek" | "xai";
980
+ /** See {@link BedrockCompat.supportsSamplingParams}. */
981
+ supportsSamplingParams?: boolean;
958
982
  }
959
983
  /** Fully-resolved google-API compat view, materialized once by `buildModel`. */
960
- export type ResolvedGoogleCompat = Required<Omit<GoogleCompat, "streamFirstEventTimeoutMs" | "streamIdleTimeoutMs" | "thinkingLoopGuard" | "antigravityUsageLabel">> & {
984
+ export type ResolvedGoogleCompat = Required<Omit<GoogleCompat, "streamFirstEventTimeoutMs" | "streamIdleTimeoutMs" | "thinkingLoopGuard" | "antigravityUsageLabel" | "supportsSamplingParams">> & {
961
985
  streamFirstEventTimeoutMs?: number;
962
986
  streamIdleTimeoutMs?: number;
963
987
  thinkingLoopGuard?: GoogleCompat["thinkingLoopGuard"];
964
988
  antigravityUsageLabel?: string;
989
+ supportsSamplingParams?: boolean;
965
990
  };
966
991
  /** Sparse, user-authored compat overrides for a given API (models.json / config vocabulary). */
967
992
  export type CompatConfigOf<TApi extends Api> = TApi extends "openai-completions" | "openrouter" | "openai-responses" | "azure-openai-responses" | "openai-codex-responses" ? OpenAICompat : TApi extends "anthropic-messages" ? AnthropicCompat : TApi extends "bedrock-converse-stream" ? BedrockCompat : TApi extends "devin-agent" ? DevinCompat : TApi extends "google-generative-ai" | "google-vertex" | "google-gemini-cli" ? GoogleCompat : undefined;
@@ -24,7 +24,7 @@ export declare const getGeminiCliHeaders: (modelId?: string) => {
24
24
  * client the version and manifest are captured from, independent of the host
25
25
  * platform. Overrides: PI_AI_ANTIGRAVITY_VERSION / _CL / _OS / _ARCH.
26
26
  */
27
- export declare const DEFAULT_ANTIGRAVITY_VERSION = "2.8.0";
27
+ export declare const DEFAULT_ANTIGRAVITY_VERSION = "2.19.1";
28
28
  /** Current Antigravity client version: env override → manifest-discovered → pinned fallback. */
29
29
  export declare function getAntigravityVersion(): string;
30
30
  /**
@@ -35,8 +35,11 @@ export declare function parseAntigravityManifestVersion(yamlText: string): strin
35
35
  /**
36
36
  * Resolves the latest Antigravity release from the official update manifest.
37
37
  * Success is cached for the process lifetime; failures are silent (the pinned
38
- * fallback stays valid) and clear the in-flight cache so a later call retries.
39
- * Skipped entirely when PI_AI_ANTIGRAVITY_VERSION is set.
38
+ * fallback stays valid) and suppress further lookups for
39
+ * `ANTIGRAVITY_VERSION_RETRY_MS`. Skipped entirely when PI_AI_ANTIGRAVITY_VERSION is set.
40
+ *
41
+ * The lookup is shared by concurrent callers, so it is bounded only by its own
42
+ * timeout; `signal` ends this caller's wait without cancelling the lookup.
40
43
  */
41
44
  export declare function ensureAntigravityVersion(fetcher?: FetchImpl, signal?: AbortSignal): Promise<void>;
42
45
  /** Antigravity `User-Agent` header value; rebuilt when the discovered version changes. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linxiraos/pi-catalog",
3
- "version": "1.1.25",
3
+ "version": "1.1.27",
4
4
  "description": "Model catalog for omp: bundled model database, provider discovery descriptors, model identity, classification, and equivalence",
5
5
  "keywords": [
6
6
  "ai",
@@ -90,12 +90,12 @@
90
90
  "gen:proto": "bun scripts/generate-protocols.ts"
91
91
  },
92
92
  "dependencies": {
93
- "@linxiraos/pi-omptype": "1.1.25",
94
- "@linxiraos/pi-utils": "1.1.25"
93
+ "@linxiraos/pi-omptype": "1.1.27",
94
+ "@linxiraos/pi-utils": "1.1.27"
95
95
  },
96
96
  "devDependencies": {
97
97
  "@bgotink/kdl": "0.4.0",
98
- "@linxiraos/pi-ai": "1.1.25",
98
+ "@linxiraos/pi-ai": "1.1.27",
99
99
  "@types/bun": "^1.3.14"
100
100
  },
101
101
  "engines": {
@@ -72,6 +72,7 @@ export type AuthProviderId =
72
72
  | "siliconflow-cn"
73
73
  | "singularityapi-dev"
74
74
  | "singularityapi-tech"
75
+ | "snowflake"
75
76
  | "stencil"
76
77
  | "stepfun"
77
78
  | "synthetic"
@@ -156,6 +157,7 @@ export type LoginProviderId =
156
157
  | "siliconflow-cn"
157
158
  | "singularityapi-dev"
158
159
  | "singularityapi-tech"
160
+ | "snowflake"
159
161
  | "stencil"
160
162
  | "stepfun"
161
163
  | "synthetic"
@@ -169,6 +169,7 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
169
169
  "requires-thinking-as-text": wire("requiresThinkingAsText", ["openai"]),
170
170
  "requires-tool-result-name": wire("requiresToolResultName", ["openai"]),
171
171
  "strict-responses-pairing": wire("strictResponsesPairing", ["openai-responses"]),
172
+ "stateful-responses": wire("statefulResponses", ["openai-responses"]),
172
173
  "requires-reasoning-off-juice-instruction": wire("requiresReasoningOffJuiceInstruction", ["openai-responses"]),
173
174
  "supports-all-turns-reasoning-context": wire("supportsAllTurnsReasoningContext", ["openai-responses"]),
174
175
  "supports-configuration-update": wire("supportsConfigurationUpdate", ["openai-responses"]),
@@ -194,6 +195,7 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
194
195
  "supports-reasoning-effort": wire("supportsReasoningEffort", OAI),
195
196
  "supports-reasoning-params": wire("supportsReasoningParams", OAI),
196
197
  "supports-reasoning-summary": wire("supportsReasoningSummary", ["openai-responses"]),
198
+ "store-responses": wire("storeResponses", ["openai-responses"]),
197
199
  "supports-store": wire("supportsStore", ["openai"]),
198
200
  "supports-strict-mode": wire("supportsStrictMode", OAI),
199
201
  "supports-tool-choice": wire("supportsToolChoice", OAI),
@@ -293,7 +295,7 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
293
295
  "stream-idle-timeout-ms": wire("streamIdleTimeoutMs", [...OAI, "anthropic", "bedrock", "google"]),
294
296
  "strip-image-input": wire("stripImageInput", [...OAI, "anthropic", "google"]),
295
297
  "supports-forced-tool-choice": wire("supportsForcedToolChoice", [...OAI, "anthropic", "bedrock"]),
296
- "supports-sampling-params": wire("supportsSamplingParams", [...OAI, "anthropic"]),
298
+ "supports-sampling-params": wire("supportsSamplingParams", [...OAI, "anthropic", "bedrock", "devin", "google"]),
297
299
  "thinking-loop-guard": wire("thinkingLoopGuard", [...OAI, "anthropic", "google"], "scalar", [
298
300
  "gemini",
299
301
  "deepseek",
@@ -304,7 +306,7 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
304
306
  "thinking-default-level": { key: "defaultLevel", set: "thinking", shape: "scalar", values: EFFORTS },
305
307
  "thinking-effort-budgets": { key: "effortBudgets", set: "thinking", shape: "object" },
306
308
  "thinking-effort-map": { key: "effortMap", set: "thinking", shape: "object" },
307
- "thinking-efforts": { key: "efforts", set: "thinking", shape: "array", values: EFFORTS },
309
+ "thinking-efforts": { key: "efforts", set: "thinking", shape: "array", values: EFFORTS, emptyArray: true },
308
310
  "thinking-mode": {
309
311
  key: "mode",
310
312
  set: "thinking",
@@ -390,6 +392,8 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
390
392
  },
391
393
  "web-search-model": { key: "webSearchModel", set: "catalog", shape: "scalar" },
392
394
  "hosted-image": { key: "hostedImage", set: "catalog", shape: "scalar", values: [true, false] },
395
+ /** How the model line bills an input image; shape and formulas in `./image-tokenization`. */
396
+ "image-tokenization": { key: "imageTokenization", set: "catalog", shape: "object" },
393
397
  "image-model": { key: "imageModel", set: "catalog", shape: "scalar" },
394
398
  "limits-patch": { key: "limitsPatch", set: "catalog", shape: "object" },
395
399
  "long-context-cost": { key: "longContext", set: "catalog", shape: "object" },
@@ -102,6 +102,13 @@ export interface VariantCollapseTable {
102
102
  * Discovery uses this accessor to preserve injectable table seams.
103
103
  */
104
104
  export declare function reviewedCollapseTable(provider: string): VariantCollapseTable | undefined;
105
+ /**
106
+ * Logical id of the reviewed `provider` family claiming `memberId` as a member
107
+ * or extra alias, or `undefined` when no reviewed family owns it. Cursor's rich
108
+ * discovery names lanes with it so they keep the id the legacy-slug collapse
109
+ * (and the bundled catalog) assigns.
110
+ */
111
+ export declare function reviewedVariantFamilyId(provider: string, memberId: string): string | undefined;
105
112
  /**
106
113
  * The global automatic rule: derive an `X` + `X-thinking` family for every
107
114
  * pair where both ids are live in `specs` (trailing or infix token). Gates:
@@ -492,6 +492,17 @@ export function reviewedCollapseTable(provider: string): VariantCollapseTable |
492
492
  return REVIEWED_COLLAPSE_TABLES[provider] ?? REVIEWED_COLLAPSE_TABLES[provider.toLowerCase()];
493
493
  }
494
494
 
495
+ /**
496
+ * Logical id of the reviewed `provider` family claiming `memberId` as a member
497
+ * or extra alias, or `undefined` when no reviewed family owns it. Cursor's rich
498
+ * discovery names lanes with it so they keep the id the legacy-slug collapse
499
+ * (and the bundled catalog) assigns.
500
+ */
501
+ export function reviewedVariantFamilyId(provider: string, memberId: string): string | undefined {
502
+ const table = reviewedCollapseTable(provider);
503
+ return table ? getAliasIndex(table).resolve(memberId.trim()) : undefined;
504
+ }
505
+
495
506
  /**
496
507
  * The global automatic rule: derive an `X` + `X-thinking` family for every
497
508
  * pair where both ids are live in `specs` (trailing or infix token). Gates:
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Billed input tokens per image, from the `image-tokenization` rules in the
3
+ * compat cascade: a model line's lineage rule (`rules/classes/*.kdl`) applies
4
+ * on every host, and readers without one fall back to the rule for the wire
5
+ * API carrying the request (`rules/providers/image-tokenization.kdl`).
6
+ */
7
+ import { isRecord } from "@linxiraos/pi-utils/type-guards";
8
+ import { classifyModel } from "../identity";
9
+ import { resolveCascade } from "./cascade";
10
+ import type { ModelIdentity } from "./types";
11
+
12
+ /** OpenAI image `detail` levels; `undefined` means the provider default (`auto`). */
13
+ export type ImageDetail = "auto" | "low" | "high" | "original";
14
+
15
+ export interface ImageSize {
16
+ width: number;
17
+ height: number;
18
+ }
19
+
20
+ /** One OpenAI `detail` level's sizing: a pixel-dimension limit, then an optional patch budget. */
21
+ export interface PatchSizing {
22
+ /** Longest side after the pixel-dimension fit. */
23
+ maxEdge: number;
24
+ /** Resizing patch budget; absent when the level keeps the image's size. */
25
+ patchBudget?: number;
26
+ }
27
+
28
+ /** OpenAI 32px patches × a per-model multiplier, sized per `detail` level. */
29
+ export interface OpenAiPatchTokenization {
30
+ regime: "openai-patch";
31
+ multiplier: number;
32
+ low: PatchSizing;
33
+ high: PatchSizing;
34
+ original: PatchSizing;
35
+ /** The level `auto` (and an omitted `detail`) sizes like. */
36
+ auto: "high" | "original";
37
+ }
38
+
39
+ /** One model line's image billing rule: the vendor formula and its per-model numbers. */
40
+ export type ImageTokenization =
41
+ | OpenAiPatchTokenization
42
+ /** Anthropic 28px patches, resized to fit a padded-edge limit and a visual-token budget. */
43
+ | { regime: "anthropic-patch"; maxEdge: number; maxTokens: number }
44
+ /** A fixed per-image budget regardless of pixels (Gemini 3 `media_resolution`). */
45
+ | { regime: "fixed"; tokens: number };
46
+
47
+ /** A `class: "unknown"` identity for targets resolved by wire API alone. */
48
+ const UNCLASSIFIED: Pick<ModelIdentity, "class"> = { class: "unknown" };
49
+
50
+ function isImageTokenization(value: unknown): value is ImageTokenization {
51
+ return (
52
+ isRecord(value) &&
53
+ (value.regime === "openai-patch" || value.regime === "anthropic-patch" || value.regime === "fixed")
54
+ );
55
+ }
56
+
57
+ /** What reads the image: a built model, any `{ id }` with optional host and identity, or just a wire `{ api }`. */
58
+ export interface ImageTokenizationTarget {
59
+ id?: string;
60
+ provider?: string;
61
+ api?: string;
62
+ identity?: Pick<ModelIdentity, "class" | "family" | "revision">;
63
+ }
64
+
65
+ /**
66
+ * The reader's image billing rule: its lineage rule, else its wire API's
67
+ * fallback; undefined when neither covers it (an unclassified model on an
68
+ * unregistered API).
69
+ */
70
+ export function resolveImageTokenization(target: ImageTokenizationTarget): ImageTokenization | undefined {
71
+ const provider = target.provider ?? "";
72
+ const model = target.id ?? "";
73
+ const identity: Pick<ModelIdentity, "class" | "family" | "revision"> =
74
+ target.identity ?? (model ? classifyModel(provider, model, { lenient: true }) : UNCLASSIFIED);
75
+ const rule = resolveCascade({
76
+ provider,
77
+ api: target.api ?? "",
78
+ class: identity.class,
79
+ model,
80
+ reasoning: false,
81
+ ...(identity.family !== undefined && { family: identity.family }),
82
+ ...(identity.revision !== undefined && { revision: identity.revision }),
83
+ }).catalog.imageTokenization;
84
+ return isImageTokenization(rule) ? rule : undefined;
85
+ }
86
+
87
+ /** Scale `size` down (never up) so its longest side is at most `maxEdge`. */
88
+ function fitLongEdge(size: ImageSize, maxEdge: number): ImageSize {
89
+ const longest = Math.max(size.width, size.height);
90
+ if (longest <= maxEdge) return size;
91
+ const scale = maxEdge / longest;
92
+ return {
93
+ width: Math.max(1, Math.round(size.width * scale)),
94
+ height: Math.max(1, Math.round(size.height * scale)),
95
+ };
96
+ }
97
+
98
+ const OPENAI_PATCH_PX = 32;
99
+
100
+ /** <https://developers.openai.com/api/docs/guides/images-vision#patch-based-image-tokenization> */
101
+ function openAiPatches(size: ImageSize, sizing: PatchSizing): number {
102
+ const { width, height } = fitLongEdge(size, sizing.maxEdge);
103
+ const patches = Math.ceil(width / OPENAI_PATCH_PX) * Math.ceil(height / OPENAI_PATCH_PX);
104
+ const budget = sizing.patchBudget;
105
+ if (budget === undefined || patches <= budget) return patches;
106
+
107
+ const shrink = Math.sqrt((OPENAI_PATCH_PX * OPENAI_PATCH_PX * budget) / (width * height));
108
+ const scaledW = (width * shrink) / OPENAI_PATCH_PX;
109
+ const scaledH = (height * shrink) / OPENAI_PATCH_PX;
110
+ const adjusted = shrink * Math.min(Math.floor(scaledW) / scaledW, Math.floor(scaledH) / scaledH);
111
+ const resizedW = Math.floor(width * adjusted);
112
+ const resizedH = Math.floor(height * adjusted);
113
+ if (resizedW <= 0 || resizedH <= 0) return budget;
114
+ return Math.min(budget, Math.ceil(resizedW / OPENAI_PATCH_PX) * Math.ceil(resizedH / OPENAI_PATCH_PX));
115
+ }
116
+
117
+ const ANTHROPIC_PATCH_PX = 28;
118
+
119
+ /** Round half to even, like the API's resize (and Python's `round`). */
120
+ function roundTiesToEven(value: number): number {
121
+ const floor = Math.floor(value);
122
+ if (value - floor !== 0.5) return Math.round(value);
123
+ return floor % 2 === 0 ? floor : floor + 1;
124
+ }
125
+
126
+ /**
127
+ * Anthropic visual tokens after the API's resize: the largest
128
+ * aspect-preserving size whose 28px-padded edges fit `maxEdge` and whose
129
+ * patch count fits `maxTokens`, found by bisecting the long edge. Port of the
130
+ * reference implementation:
131
+ * <https://platform.claude.com/docs/en/build-with-claude/vision-coordinates#resize-your-image-before-uploading>
132
+ */
133
+ function anthropicTokens(size: ImageSize, maxEdge: number, maxTokens: number): number {
134
+ const long = Math.max(size.width, size.height);
135
+ const shortSide = Math.min(size.width, size.height);
136
+ const aspect = long / shortSide;
137
+ const patches = (l: number, s: number) => Math.ceil(l / ANTHROPIC_PATCH_PX) * Math.ceil(s / ANTHROPIC_PATCH_PX);
138
+ const fits = (l: number, s: number) =>
139
+ Math.ceil(l / ANTHROPIC_PATCH_PX) * ANTHROPIC_PATCH_PX <= maxEdge &&
140
+ Math.ceil(s / ANTHROPIC_PATCH_PX) * ANTHROPIC_PATCH_PX <= maxEdge &&
141
+ patches(l, s) <= maxTokens;
142
+ const short = (l: number) => Math.max(roundTiesToEven(l / aspect), 1);
143
+ if (fits(long, shortSide)) return patches(long, shortSide);
144
+ let lo = 1; // always fits
145
+ let hi = long; // never fits
146
+ while (lo + 1 < hi) {
147
+ const mid = Math.floor((lo + hi) / 2);
148
+ if (fits(mid, short(mid))) lo = mid;
149
+ else hi = mid;
150
+ }
151
+ return patches(lo, short(lo));
152
+ }
153
+
154
+ /** Billed input tokens for one image of `size` under `rule`; only `openai-patch` reads `detail`. */
155
+ export function imageTokens(rule: ImageTokenization, size: ImageSize, detail?: ImageDetail): number {
156
+ switch (rule.regime) {
157
+ case "fixed":
158
+ return rule.tokens;
159
+ case "openai-patch": {
160
+ const level = detail === "low" || detail === "high" || detail === "original" ? detail : rule.auto;
161
+ return Math.ceil(openAiPatches(size, rule[level]) * rule.multiplier);
162
+ }
163
+ case "anthropic-patch":
164
+ return anthropicTokens(size, rule.maxEdge, rule.maxTokens);
165
+ }
166
+ }
@@ -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" | "commandcode" | "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" | "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" | "siliconflow" | "siliconflow-cn" | "singularityapi-dev" | "singularityapi-tech" | "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" | "commandcode" | "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" | "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" | "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";
@@ -66,6 +66,7 @@ export type KnownProvider =
66
66
  | "siliconflow-cn"
67
67
  | "singularityapi-dev"
68
68
  | "singularityapi-tech"
69
+ | "snowflake"
69
70
  | "stepfun"
70
71
  | "synthetic"
71
72
  | "together"