@oh-my-pi/pi-catalog 18.10.1 → 18.10.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/THIRD-PARTY-NOTICES.txt +3 -3
  3. package/dist/types/compat/image-tokenization.d.ts +9 -1
  4. package/dist/types/compat/request-size.d.ts +11 -3
  5. package/dist/types/compat/thinking-replay.d.ts +9 -0
  6. package/dist/types/model-thinking.d.ts +11 -1
  7. package/dist/types/types.d.ts +37 -5
  8. package/dist/types/wire/codex.d.ts +2 -0
  9. package/package.json +4 -4
  10. package/src/build.ts +3 -2
  11. package/src/compat/axes.ts +44 -2
  12. package/src/compat/cascade.ts +4 -1
  13. package/src/compat/image-tokenization.ts +28 -10
  14. package/src/compat/request-size.ts +18 -5
  15. package/src/compat/resolve.ts +32 -5
  16. package/src/compat/rules/README.md +10 -2
  17. package/src/compat/rules/classes/anthropic.kdl +40 -2
  18. package/src/compat/rules/classes/meta.kdl +5 -0
  19. package/src/compat/rules/classes/openai.kdl +26 -3
  20. package/src/compat/rules/classes/xai.kdl +26 -0
  21. package/src/compat/rules/providers/amazon-bedrock.kdl +1 -0
  22. package/src/compat/rules/providers/anthropic.kdl +17 -0
  23. package/src/compat/rules/providers/cursor.kdl +30 -1
  24. package/src/compat/rules/providers/google-antigravity.kdl +26 -0
  25. package/src/compat/rules/providers/google-gemini-cli.kdl +1 -0
  26. package/src/compat/rules/providers/google-vertex.kdl +1 -0
  27. package/src/compat/rules/providers/google.kdl +1 -0
  28. package/src/compat/rules/providers/image-tokenization.kdl +2 -1
  29. package/src/compat/rules/providers/meta.kdl +16 -0
  30. package/src/compat/rules/providers/muse-code.kdl +9 -0
  31. package/src/compat/rules/providers/openai-codex.kdl +8 -0
  32. package/src/compat/rules/providers/openai.kdl +1 -0
  33. package/src/compat/rules/providers/openrouter.kdl +1 -0
  34. package/src/compat/rules/providers/umans.kdl +1 -0
  35. package/src/compat/rules/providers/xai-oauth.kdl +3 -0
  36. package/src/compat/rules/providers/xai.kdl +3 -0
  37. package/src/compat/rules/runtime/behavior.kdl +3 -3
  38. package/src/compat/rules/taxonomy/meta.kdl +3 -0
  39. package/src/compat/rules.json +1 -1
  40. package/src/compat/thinking-replay.ts +33 -0
  41. package/src/model-thinking.ts +13 -1
  42. package/src/model-tokenizer.ts +7 -1
  43. package/src/models.json +1 -1
  44. package/src/provider-models/bundled-references.ts +5 -2
  45. package/src/provider-models/openai-compat.ts +3 -1
  46. package/src/types.ts +39 -4
  47. package/src/wire/codex.ts +17 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,44 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.10.3] - 2026-10-12
6
+
7
+ ### Breaking Changes
8
+
9
+ - `resolveInlineImageByteBudget(model, officialEndpoint)` and `resolveImageCountBudget(model, officialEndpoint)` take whether the request reaches the official endpoint instead of reading `compat.officialEndpoint`, which misses request-time reroutes such as `ANTHROPIC_BASE_URL`; pass pi-ai's `reachesOfficialEndpoint(model)` ([#15434](https://github.com/can1357/oh-my-pi/pull/15434) by [@will-bogusz](https://github.com/will-bogusz))
10
+
11
+ ### Added
12
+
13
+ - Added Muse Wonder (`muse-wonder-1.0`) to Meta and Muse Code with its `minimal`–`max` effort ladder and pricing
14
+ - Added the `thinking-unverified` rule axis: on hosts whose model list carries no capability metadata (Meta, Muse Code), unrecognized models now offer an opt-in thinking ladder (`ThinkingConfig.unverified`) instead of none
15
+ - Added `thinking-unverified` axis for offering reasoning ladders on unclassified model IDs
16
+ - Added `Muse Wonder 1.0` model definition to Meta provider
17
+ - Added the opt-in `snapcompact-image-reads` axis: snapcompact images a fresh `read` result only for readers whose rule sets it; Claude Opus 5.5 and later opt in ([#14249](https://github.com/can1357/oh-my-pi/pull/14249) by [@will-bogusz](https://github.com/will-bogusz))
18
+ - Added the `image-count-budget` rule for how many images one request may carry (Anthropic 90, Codex 200, other hosts unchanged) and the `official-image-count-budget` override that applies only on the official endpoint: Anthropic 600 (100 on 200k-window Claude models), Codex 2,000 ([#15434](https://github.com/can1357/oh-my-pi/pull/15434) by [@will-bogusz](https://github.com/will-bogusz))
19
+ - Added a 24 MB inline-image byte budget for the Codex backend, which now counts as its official endpoint at its default URL and at paths under it such as `/codex/responses` ([#15434](https://github.com/can1357/oh-my-pi/pull/15434) by [@will-bogusz](https://github.com/will-bogusz))
20
+ - Added `isOfficialCodexApiUrl` to `wire/codex`, moved from pi-ai ([#15434](https://github.com/can1357/oh-my-pi/pull/15434) by [@will-bogusz](https://github.com/will-bogusz))
21
+ - Added `verified` to `image-tokenization` rules, marking the Claude 4.7+ high-res tier and the GPT-5.5 to 6.1 and gpt-6-astra rules as matching measured provider billing ([#15442](https://github.com/can1357/oh-my-pi/pull/15442) by [@will-bogusz](https://github.com/will-bogusz))
22
+ - Added the `image-max-edge` axis and `resolveImageMaxEdge()` for hosts that shrink images before the model reads them; Cursor's Claude and GPT models and its `default` (Auto) route fit 1024×1024 ([#15448](https://github.com/can1357/oh-my-pi/pull/15448) by [@will-bogusz](https://github.com/will-bogusz))
23
+ - Added the `thinking-replay-tokens` axis; Claude lines set it to `output` on every host, since a replayed signed thinking block bills the turn's full reasoning, not the signature's bytes ([#15446](https://github.com/can1357/oh-my-pi/pull/15446) by [@will-bogusz](https://github.com/will-bogusz))
24
+ - Added a KDL opt-in for sibling subagent prompt-cache sharing, enabled for Muse Code's Spark 1.3, Wonder 1.0, and Wonder Full 1.0 variants over the Responses API ([#15467](https://github.com/can1357/oh-my-pi/pull/15467) by [@iliaal](https://github.com/iliaal))
25
+
26
+ ### Changed
27
+
28
+ - Updated model definitions and compatibility rules for provider metadata
29
+ - Google Antigravity, xAI and Cursor requests keep up to 60 images, 20 for Claude on Antigravity and Cursor, up from 5; this counts snapcompact frames and live screenshots together ([#15449](https://github.com/can1357/oh-my-pi/pull/15449) by [@will-bogusz](https://github.com/will-bogusz))
30
+ - Snapcompact sizes archives at the frame prices measured for Antigravity Gemini (a fixed 1,089 tokens), Grok 4.6 to 4.20, and Claude Sonnet 4.6's standard tier ([#15449](https://github.com/can1357/oh-my-pi/pull/15449) by [@will-bogusz](https://github.com/will-bogusz))
31
+
32
+ ### Fixed
33
+
34
+ - Applied `max-image-dimension` and `max-image-payload-bytes` to Google API models, and declared the 5 MiB per-image limit of Claude on Google Antigravity ([#15444](https://github.com/can1357/oh-my-pi/pull/15444) by [@will-bogusz](https://github.com/will-bogusz))
35
+ - Fixed local token counts for OpenAI GPT-5 and later models (Codex, OpenAI, Azure, OpenRouter, Copilot and other hosts) reading about 12% low: their text is now counted with o200k_base instead of a bytes/4 estimate ([#15447](https://github.com/can1357/oh-my-pi/pull/15447) by [@will-bogusz](https://github.com/will-bogusz))
36
+
37
+ ## [18.10.2] - 2026-10-12
38
+
39
+ ### Added
40
+
41
+ - Added the `honors-claude-code-beta-kill-switch` axis (`compat.honorsClaudeCodeBetaKillSwitch`), on for the `anthropic` provider, so a rule or `models.yml` override decides which providers honor `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS` ([#15458](https://github.com/can1357/oh-my-pi/pull/15458) by [@H4vC](https://github.com/H4vC))
42
+
5
43
  ## [18.10.0] - 2026-10-12
6
44
 
7
45
  ### Changed
@@ -79,9 +79,9 @@ uutils coreutils (https://github.com/uutils/coreutils), MIT
79
79
  --------------------------------------------------------------------------------
80
80
  Covers: base32, base64, basename, cat, cksum (shared checksum machinery),
81
81
  b2sum, md5sum, sha1sum, sha224sum, sha256sum, sha384sum, sha512sum, comm, cp,
82
- cut, date, dirname, head, hostname, ln, ls, mkdir, mktemp, mv, nproc, paste,
83
- printenv, readlink, realpath, rm, seq, sort, stat, tac, tail, tee, touch, tr,
84
- truncate, uname, uniq, wc, whoami, yes.
82
+ cut, date, dirname, head, hostname, ln, ls, mkdir, mktemp, mv, nl, nproc,
83
+ paste, printenv, readlink, realpath, rm, seq, sort, stat, tac, tail, tee,
84
+ touch, tr, truncate, uname, uniq, wc, whoami, yes.
85
85
 
86
86
  Copyright (c) uutils developers
87
87
 
@@ -23,7 +23,7 @@ export interface OpenAiPatchTokenization {
23
23
  auto: "high" | "original";
24
24
  }
25
25
  /** One model line's image billing rule: the vendor formula and its per-model numbers. */
26
- export type ImageTokenization = OpenAiPatchTokenization
26
+ export type ImageTokenization = (OpenAiPatchTokenization
27
27
  /** Anthropic 28px patches, resized to fit a padded-edge limit and a visual-token budget. */
28
28
  | {
29
29
  regime: "anthropic-patch";
@@ -34,6 +34,9 @@ export type ImageTokenization = OpenAiPatchTokenization
34
34
  | {
35
35
  regime: "fixed";
36
36
  tokens: number;
37
+ }) & {
38
+ /** Provider billing was measured to match this rule; unset for docs-only rules and wire fallbacks. */
39
+ verified?: boolean;
37
40
  };
38
41
  /** What reads the image: a built model, any `{ id }` with optional host and identity, or just a wire `{ api }`. */
39
42
  export interface ImageTokenizationTarget {
@@ -48,5 +51,10 @@ export interface ImageTokenizationTarget {
48
51
  * unregistered API).
49
52
  */
50
53
  export declare function resolveImageTokenization(target: ImageTokenizationTarget): ImageTokenization | undefined;
54
+ /**
55
+ * Longest image edge the reader's host passes to the model (`image-max-edge`),
56
+ * or undefined when the host keeps the image's size.
57
+ */
58
+ export declare function resolveImageMaxEdge(target: ImageTokenizationTarget): number | undefined;
51
59
  /** Billed input tokens for one image of `size` under `rule`; only `openai-patch` reads `detail`. */
52
60
  export declare function imageTokens(rule: ImageTokenization, size: ImageSize, detail?: ImageDetail): number;
@@ -1,7 +1,15 @@
1
1
  import type { Model } from "../types.js";
2
2
  /**
3
3
  * Maximum encoded inline-image bytes retained per request for a deployment with a documented body-size limit.
4
- * The axis is a host contract: a rerouted `baseUrl` (proxy, gateway) imposes its own limit, so the budget
5
- * applies only while the model targets its official endpoint.
4
+ * The axis is a host contract: a rerouted endpoint (proxy, gateway) imposes its own limit, so the budget applies
5
+ * only when `officialEndpoint` says the request reaches the model's official endpoint. `compat.officialEndpoint`
6
+ * classifies only the configured `baseUrl`; pi-ai's `reachesOfficialEndpoint` also follows request-time reroutes.
6
7
  */
7
- export declare function resolveInlineImageByteBudget(model: Model): number | undefined;
8
+ export declare function resolveInlineImageByteBudget(model: Model, officialEndpoint: boolean): number | undefined;
9
+ /**
10
+ * Most images one request to this deployment may carry, or `undefined` when no rule names the host. The
11
+ * `official-image-count-budget` axis replaces `image-count-budget` only when `officialEndpoint` says the request
12
+ * reaches the model's official endpoint (see {@link resolveInlineImageByteBudget}); a rerouted endpoint keeps the
13
+ * provider's generic budget.
14
+ */
15
+ export declare function resolveImageCountBudget(model: Model, officialEndpoint: boolean): number | undefined;
@@ -0,0 +1,9 @@
1
+ import type { ImageTokenizationTarget } from "./image-tokenization.js";
2
+ /**
3
+ * `output`: the reasoning the signature carries, at the size its turn billed
4
+ * as output; the signature's own bytes are free. `signature`: the signature's
5
+ * bytes stand in for that reasoning.
6
+ */
7
+ export type ThinkingReplayTokens = "output" | "signature";
8
+ /** How `reader` bills replayed thinking; `signature` unless its lineage declares otherwise. */
9
+ export declare function resolveThinkingReplayTokens(reader: ImageTokenizationTarget): ThinkingReplayTokens;
@@ -7,7 +7,7 @@
7
7
  * id parsing, no host matching, no compat detection per request.
8
8
  */
9
9
  import { Effort } from "./effort.js";
10
- import type { Api, Model, ModelSpec } from "./types.js";
10
+ import type { Api, Model, ModelSpec, ThinkingConfig } from "./types.js";
11
11
  /**
12
12
  * Runtime helpers read baked metadata only, so they accept both pre-build
13
13
  * specs and built models.
@@ -19,6 +19,16 @@ type ApiModel<TApi extends Api = Api> = ModelSpec<TApi> | Model<TApi>;
19
19
  * controllable effort surface (`thinking: undefined`).
20
20
  */
21
21
  export declare function getSupportedEfforts<TApi extends Api>(model: ApiModel<TApi>): readonly Effort[];
22
+ /**
23
+ * Neutral view of a model whose thinking ladder is unverified: `reasoning: false`
24
+ * and no ladder. Re-entering policy resolution starts from it (the ladder is
25
+ * derived output, not authored input), and requests without an effort send it so
26
+ * no reasoning or disable fields reach an upstream that may reject them.
27
+ */
28
+ export declare function withoutUnverifiedThinking<T extends {
29
+ reasoning: boolean;
30
+ thinking?: ThinkingConfig;
31
+ }>(model: T): T;
22
32
  /**
23
33
  * Clamps a requested thinking level against explicit model metadata.
24
34
  *
@@ -93,6 +93,14 @@ export interface ThinkingConfig {
93
93
  * provides an explicit wire off-path.
94
94
  */
95
95
  requiresEffort?: boolean;
96
+ /**
97
+ * The catalog has no evidence this model reasons (unclassified id, no
98
+ * capability metadata). The ladder is offered for the user to opt into:
99
+ * sessions leave thinking unset until the user picks an effort, and a
100
+ * request without an effort goes out as non-reasoning (see
101
+ * `withoutUnverifiedThinking`).
102
+ */
103
+ unverified?: boolean;
96
104
  }
97
105
  export type Provider = string;
98
106
  /** Token budgets for each thinking level (token-based providers only) */
@@ -492,6 +500,14 @@ export interface AnthropicCompat {
492
500
  * contract instead. Default: false.
493
501
  */
494
502
  firstPartyProvider?: boolean;
503
+ /**
504
+ * Whether the provider honors Claude Code's `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS`
505
+ * kill switch, which withholds pre-release fields (`context_management`, beta
506
+ * tool fields, task budgets, structured-output format, `thinking.block_binding`,
507
+ * server compaction) with their betas. Rule-owned on the
508
+ * provider whose `ANTHROPIC_*` env routing Claude Code shares. Default: false.
509
+ */
510
+ honorsClaudeCodeBetaKillSwitch?: boolean;
495
511
  /**
496
512
  * Whether requests may carry `output_config.effort` (and its effort beta
497
513
  * header). Vertex AI rejects the field/header. Default: true.
@@ -928,9 +944,11 @@ export interface ResolvedOpenAIResponsesCompat extends ResolvedOpenAISharedCompa
928
944
  isVercelGatewayHost: boolean;
929
945
  /**
930
946
  * The configured endpoint is first-party OpenAI (`provider === "openai"` on
931
- * an `api.openai.com` or unset baseUrl). Gates official-only Responses
932
- * behavior: default-on stateful `previous_response_id` chaining and the
933
- * `text.verbosity` field.
947
+ * an `api.openai.com` or unset baseUrl), or the Codex backend
948
+ * (`openai-codex-responses` on the default Codex baseUrl). Gates platform
949
+ * OpenAI's official-only Responses behavior (default-on stateful
950
+ * `previous_response_id` chaining and the `text.verbosity` field; the Codex
951
+ * adapter is not governed by either gate) and host request-size budgets.
934
952
  */
935
953
  officialEndpoint: boolean;
936
954
  /** Run Harmony-protocol leak detection/mitigation on streamed output. */
@@ -1055,18 +1073,32 @@ export interface GoogleCompat {
1055
1073
  antigravityUsageLabel?: string;
1056
1074
  /** Strip image inputs before encoding (text-only serving of a multimodal id). */
1057
1075
  stripImageInput?: boolean;
1076
+ /**
1077
+ * Largest width or height a single image may carry for a Claude reader on
1078
+ * this host (see {@link AnthropicCompat.maxImageDimension}). Setting this or
1079
+ * {@link maxImagePayloadBytes} turns on Claude's image clamp, many-image
1080
+ * cap included; unset leaves images as sent.
1081
+ */
1082
+ maxImageDimension?: number;
1083
+ /**
1084
+ * Largest base64 payload a single image may carry, in bytes (see
1085
+ * {@link AnthropicCompat.maxImagePayloadBytes}).
1086
+ */
1087
+ maxImagePayloadBytes?: number;
1058
1088
  /** Thinking-loop watchdog guard family applied to streamed reasoning. */
1059
1089
  thinkingLoopGuard?: "gemini" | "deepseek" | "xai";
1060
1090
  /** See {@link BedrockCompat.supportsSamplingParams}. */
1061
1091
  supportsSamplingParams?: boolean;
1062
1092
  }
1063
1093
  /** Fully-resolved google-API compat view, materialized once by `buildModel`. */
1064
- export type ResolvedGoogleCompat = Required<Omit<GoogleCompat, "streamFirstEventTimeoutMs" | "streamIdleTimeoutMs" | "thinkingLoopGuard" | "antigravityUsageLabel" | "supportsSamplingParams">> & {
1094
+ export type ResolvedGoogleCompat = Required<Omit<GoogleCompat, "streamFirstEventTimeoutMs" | "streamIdleTimeoutMs" | "thinkingLoopGuard" | "antigravityUsageLabel" | "supportsSamplingParams" | "maxImageDimension" | "maxImagePayloadBytes">> & {
1065
1095
  streamFirstEventTimeoutMs?: number;
1066
1096
  streamIdleTimeoutMs?: number;
1067
1097
  thinkingLoopGuard?: GoogleCompat["thinkingLoopGuard"];
1068
1098
  antigravityUsageLabel?: string;
1069
1099
  supportsSamplingParams?: boolean;
1100
+ maxImageDimension?: number;
1101
+ maxImagePayloadBytes?: number;
1070
1102
  };
1071
1103
  /** Sparse, user-authored compat overrides for a given API (models.json / config vocabulary). */
1072
1104
  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;
@@ -1145,7 +1177,7 @@ export interface ModelCost extends TokenCost {
1145
1177
  * generations rather than providers: DeepSeek V3 through V4 share
1146
1178
  * `"deepseek-v3"`; Kimi K2 through K3 share `"kimi-k2"`.
1147
1179
  */
1148
- export type ModelTokenizer = "claude-v3" | "claude-v47" | "claude-v5" | "claude-v5-sonnet" | "qwen3" | "deepseek-v3" | "kimi-k2" | "glm5";
1180
+ export type ModelTokenizer = "claude-v3" | "claude-v47" | "claude-v5" | "claude-v5-sonnet" | "qwen3" | "deepseek-v3" | "kimi-k2" | "glm5" | "o200k";
1149
1181
  /** One account's discovered entitlements on a model; see {@link Model.accountAccess}. */
1150
1182
  export interface ModelAccountAccess {
1151
1183
  /**
@@ -2,6 +2,8 @@
2
2
  * Constants for OpenAI Codex (ChatGPT OAuth) backend
3
3
  */
4
4
  export declare const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
5
+ /** Strict official-Codex endpoint check; exact origin or a path boundary after {@link CODEX_BASE_URL}. */
6
+ export declare function isOfficialCodexApiUrl(baseUrl: string | undefined): boolean;
5
7
  /**
6
8
  * Pinned OpenAI Codex client version (corresponds to @openai/codex package version).
7
9
  * Updated from npm by `bun run check-spoofed-versions --update`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-catalog",
3
- "version": "18.10.1",
3
+ "version": "18.10.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.10.1",
97
- "@oh-my-pi/pi-utils": "18.10.1"
96
+ "@oh-my-pi/omptype": "18.10.3",
97
+ "@oh-my-pi/pi-utils": "18.10.3"
98
98
  },
99
99
  "devDependencies": {
100
100
  "@bgotink/kdl": "0.4.0",
101
- "@oh-my-pi/pi-ai": "18.10.1",
101
+ "@oh-my-pi/pi-ai": "18.10.3",
102
102
  "@types/bun": "^1.3.14"
103
103
  },
104
104
  "engines": {
package/src/build.ts CHANGED
@@ -351,8 +351,9 @@ export function buildModel<TApi extends Api>(spec: ModelSpec<TApi>): Model<TApi>
351
351
  const model: Model<TApi> = {
352
352
  ...spec,
353
353
  // A reviewed `thinking-upgrade-neutral` policy can repair a stale
354
- // `reasoning: false` discovery default (see `resolveThinkingPolicy`);
355
- // materialize the correction for transports and the picker.
354
+ // `reasoning: false` discovery default, and an unclassified neutral spec
355
+ // gets an unverified ladder (see `resolveThinkingPolicy`); materialize
356
+ // either for transports and the picker.
356
357
  reasoning: spec.reasoning || policy.thinking !== undefined,
357
358
  name: cleanModelName(spec.name),
358
359
  identity: policy.identity,
@@ -242,9 +242,10 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
242
242
  "escape-builtin-tool-names": wire("escapeBuiltinToolNames", ["anthropic"]),
243
243
  "fast-mode": wire("fastMode", ["anthropic"]),
244
244
  "first-party-provider": wire("firstPartyProvider", ["anthropic"]),
245
+ "honors-claude-code-beta-kill-switch": wire("honorsClaudeCodeBetaKillSwitch", ["anthropic"]),
245
246
  "inject-claude-code-instruction": wire("injectClaudeCodeInstruction", ["anthropic"]),
246
- "max-image-dimension": wire("maxImageDimension", ["anthropic"]),
247
- "max-image-payload-bytes": wire("maxImagePayloadBytes", ["anthropic"]),
247
+ "max-image-dimension": wire("maxImageDimension", ["anthropic", "google"]),
248
+ "max-image-payload-bytes": wire("maxImagePayloadBytes", ["anthropic", "google"]),
248
249
  "official-endpoint": wire("officialEndpoint", ["anthropic", "openai-responses"]),
249
250
  "replay-unsigned-thinking": wire("replayUnsignedThinking", ["anthropic"]),
250
251
  "requires-thinking-enabled": wire("requiresThinkingEnabled", ["anthropic"]),
@@ -328,6 +329,8 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
328
329
  "thinking-suppress-when-off": { key: "suppressWhenOff", set: "thinking", shape: "scalar" },
329
330
  "thinking-supports-display": { key: "supportsDisplay", set: "thinking", shape: "scalar" },
330
331
  "thinking-upgrade-neutral": { key: "upgradeNeutral", set: "thinking", shape: "scalar" },
332
+ /** Unclassified neutral ids here are capability-unknown: offer the ladder as opt-in. */
333
+ "thinking-unverified": { key: "unverified", set: "thinking", shape: "scalar" },
331
334
 
332
335
  // ── catalog metadata ──
333
336
  "apply-patch-tool-type": {
@@ -388,6 +391,18 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
388
391
  shape: "scalar",
389
392
  values: [true, false],
390
393
  },
394
+ /**
395
+ * How the model line bills a replayed signed thinking block as input:
396
+ * `output` bills the reasoning its signature carries, at the size the turn
397
+ * billed as output, and not the signature's bytes. Unassigned: estimated
398
+ * from the signature's bytes. Read through `./thinking-replay`.
399
+ */
400
+ "thinking-replay-tokens": {
401
+ key: "thinkingReplayTokens",
402
+ set: "catalog",
403
+ shape: "scalar",
404
+ values: ["output", "signature"],
405
+ },
391
406
  "delegation-bias": { key: "delegationBias", set: "catalog", shape: "scalar", values: DELEGATION_BIASES },
392
407
  "discovery-api": { key: "discoveryApi", set: "catalog", shape: "scalar" },
393
408
  "edit-prompt-variant": { key: "editPromptVariant", set: "catalog", shape: "scalar", values: ["full", "compact"] },
@@ -404,7 +419,28 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
404
419
  "hosted-image": { key: "hostedImage", set: "catalog", shape: "scalar", values: [true, false] },
405
420
  /** How the model line bills an input image; shape and formulas in `./image-tokenization`. */
406
421
  "image-tokenization": { key: "imageTokenization", set: "catalog", shape: "object" },
422
+ /**
423
+ * Most images one request may carry, documents included where the host
424
+ * counts them. Unassigned: unknown host, held to a conservative floor.
425
+ */
426
+ "image-count-budget": { key: "imageCountBudget", set: "catalog", shape: "scalar" },
427
+ /**
428
+ * Replaces `image-count-budget` while the request reaches the official
429
+ * endpoint (pi-ai `reachesOfficialEndpoint`): a limit measured or documented
430
+ * for the host's own endpoint, which a custom `baseUrl` or an
431
+ * `ANTHROPIC_BASE_URL`/`FOUNDRY_BASE_URL` reroute (proxy, gateway) need not share.
432
+ */
433
+ "official-image-count-budget": { key: "officialImageCountBudget", set: "catalog", shape: "scalar" },
434
+ /**
435
+ * Opt-in: the reader may get a fresh `read` result as snapcompact frames
436
+ * (`snapcompact.toolResults`), because it edits as exactly from frames as
437
+ * from text. Unassigned: `read` results stay text, since a reader that
438
+ * misreads indentation in frames makes inexact edits.
439
+ */
440
+ "snapcompact-image-reads": { key: "snapcompactImageReads", set: "catalog", shape: "scalar", values: [true, false] },
407
441
  "image-model": { key: "imageModel", set: "catalog", shape: "scalar" },
442
+ /** Longest image edge the host passes to the model: it downscales larger images to fit first. */
443
+ "image-max-edge": { key: "imageMaxEdge", set: "catalog", shape: "scalar" },
408
444
  "inline-image-byte-budget": { key: "inlineImageByteBudget", set: "catalog", shape: "scalar" },
409
445
  /**
410
446
  * Most questions one System One judgment request may carry; the judge
@@ -414,6 +450,12 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
414
450
  "limits-patch": { key: "limitsPatch", set: "catalog", shape: "object" },
415
451
  "long-context-cost": { key: "longContext", set: "catalog", shape: "object" },
416
452
  "prompt-cache": { key: "promptCache", set: "catalog", shape: "object" },
453
+ "task-sibling-prompt-cache": {
454
+ key: "taskSiblingPromptCache",
455
+ set: "catalog",
456
+ shape: "scalar",
457
+ values: [true, false],
458
+ },
417
459
  /**
418
460
  * Prompt-cache lookback in block positions: how far back from a cache
419
461
  * breakpoint the provider looks for an earlier request's cache entry.
@@ -409,13 +409,16 @@ function resolveOverIndex(index: RuleIndex, target: ResolveTarget): ResolvedAxes
409
409
  reasoning = true;
410
410
  }
411
411
  }
412
+ // Unclassified targets get their provider's ladder too: it is offered as
413
+ // unverified thinking (`resolveThinkingPolicy`), never as reasoning capability.
414
+ const offersThinking = reasoning || target.class === "unknown";
412
415
  const wire: WinnerTable = {};
413
416
  const thinking: WinnerTable = {};
414
417
  const catalog: WinnerTable = {};
415
418
  for (const { rule, rank } of ranked) {
416
419
  contest(wire, rule.compiled.wire, rank, rule, target);
417
420
  contest(catalog, rule.compiled.catalog, rank, rule, target);
418
- if (reasoning) contest(thinking, rule.compiled.thinking, rank, rule, target);
421
+ if (offersThinking) contest(thinking, rule.compiled.thinking, rank, rule, target);
419
422
  }
420
423
  return {
421
424
  wire: collect(wire, rule => rule.wire),
@@ -37,12 +37,16 @@ export interface OpenAiPatchTokenization {
37
37
  }
38
38
 
39
39
  /** One model line's image billing rule: the vendor formula and its per-model numbers. */
40
- export type ImageTokenization =
40
+ export type ImageTokenization = (
41
41
  | OpenAiPatchTokenization
42
42
  /** Anthropic 28px patches, resized to fit a padded-edge limit and a visual-token budget. */
43
43
  | { regime: "anthropic-patch"; maxEdge: number; maxTokens: number }
44
44
  /** A fixed per-image budget regardless of pixels (Gemini 3 `media_resolution`). */
45
- | { regime: "fixed"; tokens: number };
45
+ | { regime: "fixed"; tokens: number }
46
+ ) & {
47
+ /** Provider billing was measured to match this rule; unset for docs-only rules and wire fallbacks. */
48
+ verified?: boolean;
49
+ };
46
50
 
47
51
  /** A `class: "unknown"` identity for targets resolved by wire API alone. */
48
52
  const UNCLASSIFIED: Pick<ModelIdentity, "class"> = { class: "unknown" };
@@ -62,17 +66,13 @@ export interface ImageTokenizationTarget {
62
66
  identity?: Pick<ModelIdentity, "class" | "family" | "revision">;
63
67
  }
64
68
 
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 {
69
+ /** Catalog axes for `target`'s identity on its host and wire API. */
70
+ function resolveImageAxes(target: ImageTokenizationTarget): Record<string, unknown> {
71
71
  const provider = target.provider ?? "";
72
72
  const model = target.id ?? "";
73
73
  const identity: Pick<ModelIdentity, "class" | "family" | "revision"> =
74
74
  target.identity ?? (model ? classifyModel(provider, model, { lenient: true }) : UNCLASSIFIED);
75
- const rule = resolveCascade({
75
+ return resolveCascade({
76
76
  provider,
77
77
  api: target.api ?? "",
78
78
  class: identity.class,
@@ -80,10 +80,28 @@ export function resolveImageTokenization(target: ImageTokenizationTarget): Image
80
80
  reasoning: false,
81
81
  ...(identity.family !== undefined && { family: identity.family }),
82
82
  ...(identity.revision !== undefined && { revision: identity.revision }),
83
- }).catalog.imageTokenization;
83
+ }).catalog;
84
+ }
85
+
86
+ /**
87
+ * The reader's image billing rule: its lineage rule, else its wire API's
88
+ * fallback; undefined when neither covers it (an unclassified model on an
89
+ * unregistered API).
90
+ */
91
+ export function resolveImageTokenization(target: ImageTokenizationTarget): ImageTokenization | undefined {
92
+ const rule = resolveImageAxes(target).imageTokenization;
84
93
  return isImageTokenization(rule) ? rule : undefined;
85
94
  }
86
95
 
96
+ /**
97
+ * Longest image edge the reader's host passes to the model (`image-max-edge`),
98
+ * or undefined when the host keeps the image's size.
99
+ */
100
+ export function resolveImageMaxEdge(target: ImageTokenizationTarget): number | undefined {
101
+ const edge = resolveImageAxes(target).imageMaxEdge;
102
+ return typeof edge === "number" && edge > 0 ? edge : undefined;
103
+ }
104
+
87
105
  /** Scale `size` down (never up) so its longest side is at most `maxEdge`. */
88
106
  function fitLongEdge(size: ImageSize, maxEdge: number): ImageSize {
89
107
  const longest = Math.max(size.width, size.height);
@@ -1,5 +1,6 @@
1
1
  import { toModelSpec } from "../provider-models/bundled-references";
2
2
  import type { Model } from "../types";
3
+ import { resolveCatalogPolicy } from "./catalog-policy";
3
4
  import { resolveCatalogAxes } from "./resolve";
4
5
 
5
6
  const imageBudgetCache = new Map<string, number | null>();
@@ -7,12 +8,12 @@ const IMAGE_BUDGET_CACHE_MAX = 8192;
7
8
 
8
9
  /**
9
10
  * Maximum encoded inline-image bytes retained per request for a deployment with a documented body-size limit.
10
- * The axis is a host contract: a rerouted `baseUrl` (proxy, gateway) imposes its own limit, so the budget
11
- * applies only while the model targets its official endpoint.
11
+ * The axis is a host contract: a rerouted endpoint (proxy, gateway) imposes its own limit, so the budget applies
12
+ * only when `officialEndpoint` says the request reaches the model's official endpoint. `compat.officialEndpoint`
13
+ * classifies only the configured `baseUrl`; pi-ai's `reachesOfficialEndpoint` also follows request-time reroutes.
12
14
  */
13
- export function resolveInlineImageByteBudget(model: Model): number | undefined {
14
- const compat = model.compat;
15
- if (!compat || !("officialEndpoint" in compat) || !compat.officialEndpoint) return undefined;
15
+ export function resolveInlineImageByteBudget(model: Model, officialEndpoint: boolean): number | undefined {
16
+ if (!officialEndpoint) return undefined;
16
17
  const key = `${model.provider} ${model.id} ${model.api}`;
17
18
  let budget = imageBudgetCache.get(key);
18
19
  if (budget === undefined) {
@@ -23,3 +24,15 @@ export function resolveInlineImageByteBudget(model: Model): number | undefined {
23
24
  }
24
25
  return budget ?? undefined;
25
26
  }
27
+
28
+ /**
29
+ * Most images one request to this deployment may carry, or `undefined` when no rule names the host. The
30
+ * `official-image-count-budget` axis replaces `image-count-budget` only when `officialEndpoint` says the request
31
+ * reaches the model's official endpoint (see {@link resolveInlineImageByteBudget}); a rerouted endpoint keeps the
32
+ * provider's generic budget.
33
+ */
34
+ export function resolveImageCountBudget(model: Model, officialEndpoint: boolean): number | undefined {
35
+ const policy = resolveCatalogPolicy(model);
36
+ const value = (officialEndpoint && policy.officialImageCountBudget) || policy.imageCountBudget;
37
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
38
+ }
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import { Effort, THINKING_EFFORTS } from "../effort";
19
19
  import { hostMatchesUrl, modelMatchesHost } from "../hosts";
20
+ import { withoutUnverifiedThinking } from "../model-thinking";
20
21
  import type {
21
22
  Api,
22
23
  CompatOf,
@@ -31,6 +32,7 @@ import type {
31
32
  ResolvedOpenAISharedCompat,
32
33
  ThinkingConfig,
33
34
  } from "../types";
35
+ import { isOfficialCodexApiUrl } from "../wire/codex";
34
36
  import {
35
37
  isAnthropicSigningProxyUrl,
36
38
  isAzureAnthropicRoute,
@@ -796,7 +798,11 @@ function resolveOpenAIResponsesPolicy(
796
798
  supportsObfuscationOptOut: isOpenAIUrl || provider === "openai",
797
799
  storeResponses: false,
798
800
  connectionBoundNativeHistory: false,
799
- officialEndpoint: isOfficialOpenAIEndpoint(provider, baseUrl),
801
+ // The Codex backend is official at the URL its requests default to when `baseUrl` is unset, and at paths
802
+ // under it such as `/codex` and `/codex/responses`, which its requests resolve to the same endpoint.
803
+ officialEndpoint:
804
+ isOfficialOpenAIEndpoint(provider, baseUrl) ||
805
+ (api === "openai-codex-responses" && isOfficialCodexApiUrl(baseUrl)),
800
806
  harmonyLeakMitigation: false,
801
807
  rejectRootObjectUnion: false,
802
808
  retryWithoutStrictOnGrammarError: false,
@@ -892,6 +898,7 @@ function resolveAnthropicPolicy(
892
898
  supportsContextManagement: true,
893
899
  supportsServerCompaction: false,
894
900
  firstPartyProvider: false,
901
+ honorsClaudeCodeBetaKillSwitch: false,
895
902
  supportsOutputEffort: true,
896
903
  disableStrictTools: rejectsStrictTools,
897
904
  // Present as a key so models.yml `compat` can set it; unset unless detected.
@@ -979,6 +986,10 @@ function resolveGooglePolicy(
979
986
  claudeThinkingBetaHeader: false,
980
987
  antigravityClaudeToolMode: false,
981
988
  stripImageInput: false,
989
+ // Unset means "no image contract": images go out as sent. Keys exist so
990
+ // a KDL rule or a user `compat` block can declare one.
991
+ maxImageDimension: undefined,
992
+ maxImagePayloadBytes: undefined,
982
993
  };
983
994
  applyWireAxes(compat, axes.wire, spec.api);
984
995
  applyCompatOverrides(compat, spec.compat);
@@ -1088,6 +1099,7 @@ interface RuleThinking {
1088
1099
  supportsDisplay?: boolean;
1089
1100
  prefixBinding?: boolean;
1090
1101
  upgradeNeutral?: boolean;
1102
+ unverified?: boolean;
1091
1103
  }
1092
1104
 
1093
1105
  function readRuleThinking(axes: ResolvedAxes): RuleThinking {
@@ -1108,6 +1120,7 @@ function readRuleThinking(axes: ResolvedAxes): RuleThinking {
1108
1120
  if (typeof raw.supportsDisplay === "boolean") out.supportsDisplay = raw.supportsDisplay;
1109
1121
  if (typeof raw.prefixBinding === "boolean") out.prefixBinding = raw.prefixBinding;
1110
1122
  if (typeof raw.upgradeNeutral === "boolean") out.upgradeNeutral = raw.upgradeNeutral;
1123
+ if (typeof raw.unverified === "boolean") out.unverified = raw.unverified;
1111
1124
  return out;
1112
1125
  }
1113
1126
 
@@ -1155,8 +1168,16 @@ function resolveThinkingPolicy<TApi extends Api>(
1155
1168
  // not re-expand it into an unadvertised ladder. Absent metadata is
1156
1169
  // repaired only where KDL opts in with `thinking-upgrade-neutral`
1157
1170
  // alongside a reviewed `thinking-efforts` ladder (the cascade upgrade for
1158
- // stale source capability data); otherwise the neutral default holds.
1159
- if (!spec.reasoning && (explicitThinking !== undefined || rule.upgradeNeutral !== true)) return undefined;
1171
+ // stale source capability data). On deployments whose discovery reports no
1172
+ // capabilities (`thinking-unverified`), an unclassified neutral spec — nothing
1173
+ // says whether it reasons — gets an unverified ladder the user may opt into.
1174
+ const unverified = !compatReasoning(spec, axes);
1175
+ if (
1176
+ unverified &&
1177
+ (explicitThinking !== undefined || facts.identity.class !== "unknown" || rule.unverified !== true)
1178
+ ) {
1179
+ return undefined;
1180
+ }
1160
1181
  if (
1161
1182
  spec.provider === "cline-pass" &&
1162
1183
  compat !== undefined &&
@@ -1180,9 +1201,14 @@ function resolveThinkingPolicy<TApi extends Api>(
1180
1201
  if (config.efforts.length === 0) {
1181
1202
  throw new Error(`Model ${spec.provider}/${spec.id} resolved to an empty thinking range`);
1182
1203
  }
1183
- if (rule.defaultLevel !== undefined) config.defaultLevel = rule.defaultLevel;
1184
1204
  const effortMap = mergeEffortMap(spec, rule.effortMap, compat, config.efforts);
1185
1205
  if (effortMap !== undefined) config.effortMap = effortMap;
1206
+ if (unverified) {
1207
+ // Off-by-default opt-in: no default level, mandatory effort, or wire off-switch.
1208
+ config.unverified = true;
1209
+ return config;
1210
+ }
1211
+ if (rule.defaultLevel !== undefined) config.defaultLevel = rule.defaultLevel;
1186
1212
  if (rule.effortBudgets !== undefined) config.effortBudgets = rule.effortBudgets;
1187
1213
  const supportsDisplay = rule.supportsDisplay ?? defaultSupportsDisplay(spec, facts);
1188
1214
  if (supportsDisplay) config.supportsDisplay = true;
@@ -1315,7 +1341,8 @@ export function resolveModelPolicy<TApi extends Api>(
1315
1341
  spec: ModelSpec<TApi>,
1316
1342
  route?: ResolveRoute,
1317
1343
  ): ResolvedModelPolicy<TApi>;
1318
- export function resolveModelPolicy(spec: ModelSpec<Api>, route?: ResolveRoute): ResolvedModelPolicy<Api> {
1344
+ export function resolveModelPolicy(input: ModelSpec<Api>, route?: ResolveRoute): ResolvedModelPolicy<Api> {
1345
+ const spec = withoutUnverifiedThinking(input);
1319
1346
  const identity = resolveIdentity(spec);
1320
1347
  const facts = new IdentityFacts(identity);
1321
1348
  const target = buildResolveTarget(spec, identity);