@oh-my-pi/pi-catalog 18.6.0 → 18.6.3

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.6.3] - 2026-10-06
6
+
7
+ ### Added
8
+
9
+ - 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)).
10
+ - 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)).
11
+ - 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)).
12
+
13
+ ### Changed
14
+
15
+ - 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)).
16
+
17
+ ### Fixed
18
+
19
+ - 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)).
20
+
21
+ ## [18.6.2] - 2026-10-04
22
+
23
+ ### Fixed
24
+
25
+ - Fixed Google Antigravity listing models the signed-in account cannot use, such as Claude Opus 5.5 and Sonnet 5.5 on plans without them, which failed every request with `404 Requested entity was not found`. After a successful model refresh and on subsequent restarts, only models in the account's own Antigravity model list are offered ([#14328](https://github.com/can1357/oh-my-pi/issues/14328)).
26
+
5
27
  ## [18.6.0] - 2026-10-03
6
28
 
7
29
  ### Fixed
@@ -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";
@@ -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"];
@@ -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
@@ -796,7 +798,7 @@ export interface ResolvedOpenAISharedCompat {
796
798
  * `buildModel`; request handlers read fields and never detect, resolve, or
797
799
  * allocate.
798
800
  */
799
- 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">> & {
800
802
  vercelGatewayRouting?: OpenAICompat["vercelGatewayRouting"];
801
803
  extraBody?: OpenAICompat["extraBody"];
802
804
  cacheControlFormat?: OpenAICompat["cacheControlFormat"];
@@ -843,6 +845,16 @@ export interface ResolvedOpenAIResponsesCompat extends ResolvedOpenAISharedCompa
843
845
  * filling `"auto"`.
844
846
  */
845
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;
846
858
  streamIdleTimeoutMs?: number;
847
859
  vercelGatewayRouting?: OpenAICompat["vercelGatewayRouting"];
848
860
  /** The model sits behind Vercel AI Gateway's Responses endpoint. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-catalog",
3
- "version": "18.6.0",
3
+ "version": "18.6.3",
4
4
  "description": "Model catalog for omp: bundled model database, provider discovery descriptors, model identity, classification, and equivalence",
5
5
  "keywords": [
6
6
  "ai",
@@ -93,12 +93,12 @@
93
93
  "gen:proto": "bun scripts/generate-protocols.ts"
94
94
  },
95
95
  "dependencies": {
96
- "@oh-my-pi/omptype": "18.6.0",
97
- "@oh-my-pi/pi-utils": "18.6.0"
96
+ "@oh-my-pi/omptype": "18.6.3",
97
+ "@oh-my-pi/pi-utils": "18.6.3"
98
98
  },
99
99
  "devDependencies": {
100
100
  "@bgotink/kdl": "0.4.0",
101
- "@oh-my-pi/pi-ai": "18.6.0",
101
+ "@oh-my-pi/pi-ai": "18.6.3",
102
102
  "@types/bun": "^1.3.14"
103
103
  },
104
104
  "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),
@@ -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" },
@@ -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 "@oh-my-pi/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
+ }
@@ -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"
@@ -735,8 +735,15 @@ function resolveOpenAIResponsesPolicy(
735
735
  supportsPromptCacheBreakpoints,
736
736
  promptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? "30m" : undefined,
737
737
  strictResponsesPairing: isAzure || provider === "github-copilot",
738
- supportsImageDetailOriginal: !isXaiHost && !modelMatchesHost(hostModel, "githubCopilot"),
738
+ // Azure's provider id alone only implies support while its endpoint is
739
+ // resolved at runtime; an explicit non-Azure baseUrl is a proxy, like Codex.
740
+ supportsImageDetailOriginal:
741
+ isOpenAIUrl ||
742
+ hostMatchesUrl(baseUrl, "azureOpenAI") ||
743
+ (isAzure && !baseUrl) ||
744
+ hostMatchesUrl(baseUrl, "openaiCodex"),
739
745
  supportsReasoningSummary: !isXaiHost,
746
+ statefulResponses: undefined,
740
747
  supportsAllTurnsReasoningContext: false,
741
748
  supportsConfigurationUpdate: false,
742
749
  supportsSteering: false,
@@ -784,6 +791,7 @@ function resolveOpenAIResponsesPolicy(
784
791
  PROXY_OPENAI_COMPAT_PROVIDERS[backendProvider] !== true &&
785
792
  (LOCAL_OPENAI_COMPAT_PROVIDERS[backendProvider] === true || hasLocalLoopbackBaseUrl(baseUrl)),
786
793
  supportsObfuscationOptOut: isOpenAIUrl || provider === "openai",
794
+ storeResponses: false,
787
795
  officialEndpoint: isOfficialOpenAIEndpoint(provider, baseUrl),
788
796
  harmonyLeakMitigation: false,
789
797
  rejectRootObjectUnion: false,
@@ -845,6 +853,7 @@ function pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnly
845
853
  strictResponsesPairing: compat.strictResponsesPairing,
846
854
  supportsImageDetailOriginal: compat.supportsImageDetailOriginal,
847
855
  supportsObfuscationOptOut: compat.supportsObfuscationOptOut,
856
+ storeResponses: compat.storeResponses,
848
857
  supportsAllTurnsReasoningContext: compat.supportsAllTurnsReasoningContext,
849
858
  supportsConfigurationUpdate: compat.supportsConfigurationUpdate,
850
859
  supportsSteering: compat.supportsSteering,
@@ -853,6 +862,7 @@ function pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnly
853
862
  cacheControlFormat: compat.cacheControlFormat,
854
863
  requiresReasoningOffJuiceInstruction: compat.requiresReasoningOffJuiceInstruction,
855
864
  supportsReasoningSummary: compat.supportsReasoningSummary,
865
+ statefulResponses: compat.statefulResponses,
856
866
  isVercelGatewayHost: compat.isVercelGatewayHost,
857
867
  } satisfies ResponsesOnlyCompat;
858
868
  }
@@ -8,6 +8,6 @@ login-order "openai-codex" "anthropic" "zai" "zai-coding-plan" "kimi-code" "open
8
8
  "xiaomi-token-plan-ams" "xiaomi-token-plan-cn" "firepass" "cline-pass" "factory-droid" "commandcode" "charm-hyper" "deepseek" "stepfun" "muse-code" "meta" "moonshot" \
9
9
  "cerebras" "baseten" "helmcode" "fireworks" "together" "nvidia" "novita" "deepinfra" "huggingface" "perplexity" \
10
10
  "qianfan" "venice" "siliconflow" "siliconflow-cn" "synthetic" "nanogpt" "wafer-serverless" "coreweave" \
11
- "vercel-ai-gateway" "cloudflare-ai-gateway" "litellm" "kilo" "zenmux" "opencode-zen" "opencode-go" \
11
+ "vercel-ai-gateway" "cloudflare-ai-gateway" "snowflake" "litellm" "kilo" "zenmux" "opencode-zen" "opencode-go" \
12
12
  "yolo-auto" "tavily" "kagi" "exa" "parallel" "typesafe" "ollama" "ollama-cloud" "lm-studio" "llama.cpp" "vllm" \
13
13
  "gmi-cloud" "stencil" "singularityapi-dev" "singularityapi-tech"
@@ -0,0 +1,11 @@
1
+ // Snowflake Cortex REST API. Login runs Snowflake OAuth for local applications
2
+ // (built-in SNOWFLAKE$LOCAL_APPLICATION client, PKCE, loopback); the account
3
+ // host is prompted first, so the flow is a custom hook.
4
+ auth "snowflake" {
5
+ name "Snowflake Cortex"
6
+ login "custom" hook="snowflake"
7
+ refresh hook="snowflake"
8
+ api-key-format "structured"
9
+ callback-port 54551
10
+ paste-code #true
11
+ }
@@ -1,5 +1,70 @@
1
1
  // Model-lineage compat for "anthropic"; regenerated from the frozen census using class taxonomy selectors.
2
2
 
3
+ // Images bill ⌈w/28⌉·⌈h/28⌉ visual tokens on every host. Claude 4.7+ and
4
+ // Fable/Mythos read up to 2576px and 4,784 tokens; older lines fit 1568px and
5
+ // cap at 1,568 tokens, downscaling past it.
6
+ // https://platform.claude.com/docs/en/build-with-claude/vision#evaluate-image-size
7
+ // The class default is the high-res tier, so ids without a revision
8
+ // (`claude-opus-latest`, `~anthropic/claude-sonnet-latest`) and future lines
9
+ // are never undercounted; only Opus/Sonnet/Haiku revisions known to predate
10
+ // 4.7 take the standard tier. Separator-collapsed revisions parse as two
11
+ // digits (`claude-opus-45` is Opus 4.5 → 45), so 10–46 is standard too.
12
+ class "anthropic" {
13
+ image-tokenization {
14
+ regime "anthropic-patch"
15
+ max-edge 2576
16
+ max-tokens 4784
17
+ }
18
+ family "opus" {
19
+ revision "<4.7" {
20
+ image-tokenization {
21
+ regime "anthropic-patch"
22
+ max-edge 1568
23
+ max-tokens 1568
24
+ }
25
+ }
26
+ revision ">=10 <47" {
27
+ image-tokenization {
28
+ regime "anthropic-patch"
29
+ max-edge 1568
30
+ max-tokens 1568
31
+ }
32
+ }
33
+ }
34
+ family "sonnet" {
35
+ revision "<4.7" {
36
+ image-tokenization {
37
+ regime "anthropic-patch"
38
+ max-edge 1568
39
+ max-tokens 1568
40
+ }
41
+ }
42
+ revision ">=10 <47" {
43
+ image-tokenization {
44
+ regime "anthropic-patch"
45
+ max-edge 1568
46
+ max-tokens 1568
47
+ }
48
+ }
49
+ }
50
+ family "haiku" {
51
+ revision "<4.7" {
52
+ image-tokenization {
53
+ regime "anthropic-patch"
54
+ max-edge 1568
55
+ max-tokens 1568
56
+ }
57
+ }
58
+ revision ">=10 <47" {
59
+ image-tokenization {
60
+ regime "anthropic-patch"
61
+ max-edge 1568
62
+ max-tokens 1568
63
+ }
64
+ }
65
+ }
66
+ }
67
+
3
68
  class "anthropic" {
4
69
  family "fable" {
5
70
  revision ">=5.1" {
@@ -181,7 +246,7 @@ class "anthropic" {
181
246
  thinking-efforts "minimal" "low" "medium" "high" "xhigh"
182
247
  }
183
248
  on "amazon-bedrock" "anthropic" "cloudflare-ai-gateway" "github-copilot" "gitlab-duo" \
184
- "google-antigravity" "google-vertex" "opencode-zen" "vercel-ai-gateway" "zenmux" {
249
+ "google-antigravity" "google-vertex" "opencode-zen" "snowflake" "vercel-ai-gateway" "zenmux" {
185
250
  family "sonnet" {
186
251
  revision ">=3.7 <4.6" {
187
252
  thinking-mode "budget"
@@ -189,13 +254,13 @@ class "anthropic" {
189
254
  }
190
255
  }
191
256
  on "amazon-bedrock" "anthropic" "cloudflare-ai-gateway" "github-copilot" "gitlab-duo" \
192
- "google-vertex" "opencode-zen" "vercel-ai-gateway" {
257
+ "google-vertex" "opencode-zen" "snowflake" "vercel-ai-gateway" {
193
258
  family "haiku" {
194
259
  thinking-mode "budget"
195
260
  }
196
261
  }
197
262
  on "amazon-bedrock" "anthropic" "cloudflare-ai-gateway" "github-copilot" "gitlab-duo" \
198
- "google-vertex" "opencode-zen" "vercel-ai-gateway" "zenmux" {
263
+ "google-vertex" "opencode-zen" "snowflake" "vercel-ai-gateway" "zenmux" {
199
264
  family "opus" {
200
265
  revision ">=4.5 <4.6" {
201
266
  thinking-mode "anthropic-budget-effort"
@@ -203,7 +268,7 @@ class "anthropic" {
203
268
  }
204
269
  }
205
270
  on "amazon-bedrock" "anthropic" "cloudflare-ai-gateway" "github-copilot" "google-vertex" \
206
- "opencode-zen" "openrouter" "vercel-ai-gateway" "zenmux" {
271
+ "opencode-zen" "openrouter" "snowflake" "vercel-ai-gateway" "zenmux" {
207
272
  family "opus" {
208
273
  revision ">=4.6 <4.7" {
209
274
  thinking-efforts "low" "medium" "high" "max"
@@ -211,7 +276,7 @@ class "anthropic" {
211
276
  }
212
277
  }
213
278
  on "amazon-bedrock" "anthropic" "cloudflare-ai-gateway" "github-copilot" "google-vertex" \
214
- "opencode-zen" "vercel-ai-gateway" "zenmux" {
279
+ "opencode-zen" "snowflake" "vercel-ai-gateway" "zenmux" {
215
280
  revision ">=4.7 <45.1" {
216
281
  thinking-mode "anthropic-adaptive"
217
282
  }
@@ -242,13 +307,13 @@ class "anthropic" {
242
307
  }
243
308
  }
244
309
  on "anthropic" "cloudflare-ai-gateway" "github-copilot" "google-vertex" "opencode-zen" \
245
- "openrouter" "vercel-ai-gateway" "zenmux" {
310
+ "openrouter" "snowflake" "vercel-ai-gateway" "zenmux" {
246
311
  revision ">=4.7 <45.1" {
247
312
  thinking-efforts "low" "medium" "high" "xhigh" "max"
248
313
  }
249
314
  }
250
315
  on "anthropic" "cloudflare-ai-gateway" "github-copilot" "google-vertex" "opencode-zen" \
251
- "vercel-ai-gateway" "zenmux" {
316
+ "snowflake" "vercel-ai-gateway" "zenmux" {
252
317
  family "sonnet" {
253
318
  revision ">=4.6 <5" {
254
319
  thinking-efforts "low" "medium" "high"
@@ -118,3 +118,15 @@ class "gemini" {
118
118
  }
119
119
  }
120
120
  }
121
+
122
+ // Gemini 3+ bills a fixed `media_resolution` budget per image regardless of
123
+ // pixels; the default for images is HIGH, 1,120 tokens.
124
+ // https://ai.google.dev/gemini-api/docs/media-resolution
125
+ class "gemini" {
126
+ revision ">=3" {
127
+ image-tokenization {
128
+ regime "fixed"
129
+ tokens 1120
130
+ }
131
+ }
132
+ }