@oh-my-pi/pi-catalog 18.10.2 → 18.10.4

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 (58) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +4 -2
  3. package/dist/types/build.d.ts +3 -2
  4. package/dist/types/compat/image-tokenization.d.ts +9 -1
  5. package/dist/types/compat/request-size.d.ts +11 -3
  6. package/dist/types/compat/thinking-replay.d.ts +9 -0
  7. package/dist/types/model-thinking.d.ts +11 -1
  8. package/dist/types/models.d.ts +16 -1
  9. package/dist/types/pricing.d.ts +37 -1
  10. package/dist/types/types.d.ts +29 -7
  11. package/dist/types/wire/codex.d.ts +2 -0
  12. package/package.json +4 -4
  13. package/src/build.ts +141 -80
  14. package/src/compat/axes.ts +64 -2
  15. package/src/compat/cascade.ts +4 -1
  16. package/src/compat/image-tokenization.ts +28 -10
  17. package/src/compat/request-size.ts +18 -5
  18. package/src/compat/resolve.ts +46 -7
  19. package/src/compat/rules/README.md +15 -3
  20. package/src/compat/rules/classes/anthropic.kdl +43 -2
  21. package/src/compat/rules/classes/gemini.kdl +28 -0
  22. package/src/compat/rules/classes/meta.kdl +7 -0
  23. package/src/compat/rules/classes/openai.kdl +26 -3
  24. package/src/compat/rules/classes/xai.kdl +26 -0
  25. package/src/compat/rules/providers/amazon-bedrock.kdl +1 -0
  26. package/src/compat/rules/providers/anthropic.kdl +13 -0
  27. package/src/compat/rules/providers/cursor.kdl +50 -1
  28. package/src/compat/rules/providers/devin.kdl +110 -5
  29. package/src/compat/rules/providers/google-antigravity.kdl +26 -0
  30. package/src/compat/rules/providers/google-gemini-cli.kdl +1 -0
  31. package/src/compat/rules/providers/google-vertex.kdl +1 -0
  32. package/src/compat/rules/providers/google.kdl +1 -0
  33. package/src/compat/rules/providers/image-tokenization.kdl +2 -1
  34. package/src/compat/rules/providers/meta.kdl +16 -0
  35. package/src/compat/rules/providers/muse-code.kdl +49 -0
  36. package/src/compat/rules/providers/openai-codex.kdl +8 -0
  37. package/src/compat/rules/providers/openai.kdl +1 -0
  38. package/src/compat/rules/providers/openrouter.kdl +1 -0
  39. package/src/compat/rules/providers/umans.kdl +1 -0
  40. package/src/compat/rules/providers/xai-oauth.kdl +3 -0
  41. package/src/compat/rules/providers/xai.kdl +3 -0
  42. package/src/compat/rules/runtime/behavior.kdl +3 -3
  43. package/src/compat/rules/taxonomy/_collapse.kdl +11 -0
  44. package/src/compat/rules/taxonomy/meta.kdl +3 -0
  45. package/src/compat/rules.json +1 -1
  46. package/src/compat/thinking-replay.ts +33 -0
  47. package/src/discovery/codex.ts +0 -4
  48. package/src/discovery/cursor.ts +25 -11
  49. package/src/discovery/devin.ts +5 -3
  50. package/src/model-thinking.ts +13 -1
  51. package/src/model-tokenizer.ts +7 -1
  52. package/src/models.json +1 -1
  53. package/src/models.ts +26 -12
  54. package/src/pricing.ts +123 -12
  55. package/src/provider-models/bundled-references.ts +5 -2
  56. package/src/provider-models/openai-compat.ts +69 -4
  57. package/src/types.ts +31 -6
  58. package/src/wire/codex.ts +17 -0
package/src/build.ts CHANGED
@@ -10,8 +10,16 @@
10
10
  import { resolveDiscoveryApi, resolveModelPolicy } from "./compat/resolve";
11
11
  import type { ModelIdentity } from "./compat/types";
12
12
  import { resolveModelTokenizer } from "./model-tokenizer";
13
- import { materializeTimeBasedCost } from "./pricing";
14
- import { type Api, MODEL_KINDS, type Model, type ModelSpec } from "./types";
13
+ import {
14
+ applyDueDatedCostCard,
15
+ type DatedCostCard,
16
+ dueEffectiveRate,
17
+ EARLIEST_EFFECTIVE_FROM,
18
+ materializeTimeBasedCost,
19
+ NO_RECURRING_TARIFF,
20
+ parseDatedCostCards,
21
+ } from "./pricing";
22
+ import { type Api, type EffectiveTokenCost, MODEL_KINDS, type Model, type ModelCost, type ModelSpec } from "./types";
15
23
  import { cleanModelName } from "./utils";
16
24
 
17
25
  function numberField(source: object, key: string): number | undefined {
@@ -24,41 +32,6 @@ function objectPayload(value: unknown): object | undefined {
24
32
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
25
33
  }
26
34
 
27
- /**
28
- * Overwrite seeded fallback rates with the latest dated card whose
29
- * `effectiveFrom` is already due. Leaves the seed unchanged when no card
30
- * has started, and never attaches a `timeBased` tariff.
31
- */
32
- function applyEffectiveFallbackRates(
33
- cost: { input: number; output: number; cacheRead: number; cacheWrite: number },
34
- effectiveRates: unknown,
35
- now = Date.now(),
36
- ): void {
37
- const rates = objectPayload(effectiveRates);
38
- if (rates === undefined) return;
39
- let latestFrom = Number.NEGATIVE_INFINITY;
40
- let latest: object | undefined;
41
- for (const entry of Object.values(rates)) {
42
- const payload = objectPayload(entry);
43
- if (payload === undefined) continue;
44
- const date = Reflect.get(payload, "effectiveFrom");
45
- if (typeof date !== "string") continue;
46
- const from = Date.parse(date);
47
- if (!Number.isFinite(from) || from > now || from < latestFrom) continue;
48
- latestFrom = from;
49
- latest = payload;
50
- }
51
- if (latest === undefined) return;
52
- const input = numberField(latest, "input");
53
- if (input !== undefined) cost.input = input;
54
- const output = numberField(latest, "output");
55
- if (output !== undefined) cost.output = output;
56
- const cacheRead = numberField(latest, "cacheRead");
57
- if (cacheRead !== undefined) cost.cacheRead = cacheRead;
58
- const cacheWrite = numberField(latest, "cacheWrite");
59
- if (cacheWrite !== undefined) cost.cacheWrite = cacheWrite;
60
- }
61
-
62
35
  /** Narrow a compiled `input-modalities` axis value to the model input union. */
63
36
  function isInputModalities(value: unknown): value is ("text" | "image")[] {
64
37
  return Array.isArray(value) && value.every(entry => entry === "text" || entry === "image");
@@ -164,52 +137,65 @@ function applyCatalogAssignments<TApi extends Api>(model: Model<TApi>, catalog:
164
137
  }
165
138
  }
166
139
 
140
+ /** The parsed dated cards of a row's `cost-patch` and `cost-fallback`. */
141
+ interface DatedCostCards {
142
+ patch: readonly DatedCostCard[];
143
+ fallback: readonly DatedCostCard[];
144
+ }
145
+
167
146
  /**
168
- * Applies reviewed catalog-data value corrections (`cost-patch`,
169
- * `cache-read-at-input-rate`, `limits-patch`, `long-context-cost`,
170
- * `context-window-floor`, `input-modalities`) onto an upstream-sourced spec. Applied by
171
- * `buildModel` to every upstream-sourced spec; user-authored overrides are
172
- * recomposed after building by the override applicators, so explicit user
173
- * limits and pricing still win.
147
+ * The row's corrected price card at `now`: `cost-patch`, then `cost-fallback`
148
+ * (each with its dated card due at `now`), then the long-context tier and
149
+ * cache-rate derivations, all from the upstream card.
174
150
  */
175
- export function applyCatalogCorrections(
176
- model: Pick<ModelSpec<Api>, "cost" | "contextWindow" | "maxTokens" | "input">,
151
+ function correctedCost(
152
+ upstream: ModelCost,
177
153
  catalog: Record<string, unknown>,
178
- ): void {
154
+ cards: DatedCostCards,
155
+ now: number,
156
+ ): ModelCost {
157
+ let cost: ModelCost = { ...upstream };
158
+ const upstreamCard =
159
+ upstream.timeBased?.effectiveRates?.[0]?.effectiveFrom === EARLIEST_EFFECTIVE_FROM
160
+ ? dueEffectiveRate(upstream.timeBased, now)
161
+ : undefined;
162
+ if (upstreamCard !== undefined) {
163
+ // A row rebuilt from an already built one (dynamic merges, overrides)
164
+ // carries the dated schedule compiled below. Its leading card covers all
165
+ // history, so the scalar legs only ever held the card due at the first
166
+ // build; start from the card due now instead.
167
+ const { effectiveFrom: _effectiveFrom, ...card } = upstreamCard;
168
+ const { longContext: _longContext, ...base } = cost;
169
+ cost = { ...base, ...card };
170
+ }
179
171
  const patch = objectPayload(catalog.costPatch);
180
172
  if (patch !== undefined) {
181
- model.cost = { ...model.cost };
182
173
  const input = numberField(patch, "input");
183
- if (input !== undefined) model.cost.input = input;
174
+ if (input !== undefined) cost.input = input;
184
175
  const output = numberField(patch, "output");
185
- if (output !== undefined) model.cost.output = output;
176
+ if (output !== undefined) cost.output = output;
186
177
  const cacheRead = numberField(patch, "cacheRead");
187
- if (cacheRead !== undefined) model.cost.cacheRead = cacheRead;
178
+ if (cacheRead !== undefined) cost.cacheRead = cacheRead;
188
179
  const cacheWrite = numberField(patch, "cacheWrite");
189
- if (cacheWrite !== undefined) model.cost.cacheWrite = cacheWrite;
180
+ if (cacheWrite !== undefined) cost.cacheWrite = cacheWrite;
181
+ applyDueDatedCostCard(cost, cards.patch, now);
190
182
  }
191
183
  const fallback = objectPayload(catalog.costFallback);
192
184
  if (fallback !== undefined) {
193
- const base = model.cost;
194
- const hasTokenPrice = base.input !== 0 || base.output !== 0 || base.cacheRead !== 0 || base.cacheWrite !== 0;
185
+ const hasTokenPrice = cost.input !== 0 || cost.output !== 0 || cost.cacheRead !== 0 || cost.cacheWrite !== 0;
195
186
  if (!hasTokenPrice) {
196
187
  // Upstream reported no token price (plan-included or promo-free
197
188
  // rows): seed the reviewed list price instead of overwriting real
198
189
  // discovery data the way `cost-patch` would.
199
- model.cost = { ...model.cost };
200
190
  const input = numberField(fallback, "input");
201
- if (input !== undefined) model.cost.input = input;
191
+ if (input !== undefined) cost.input = input;
202
192
  const output = numberField(fallback, "output");
203
- if (output !== undefined) model.cost.output = output;
193
+ if (output !== undefined) cost.output = output;
204
194
  const cacheRead = numberField(fallback, "cacheRead");
205
- if (cacheRead !== undefined) model.cost.cacheRead = cacheRead;
195
+ if (cacheRead !== undefined) cost.cacheRead = cacheRead;
206
196
  const cacheWrite = numberField(fallback, "cacheWrite");
207
- if (cacheWrite !== undefined) model.cost.cacheWrite = cacheWrite;
208
- // Dated fallback rates overwrite the seeded numbers when they have
209
- // already taken effect. They are not a recurring tariff: wrapping
210
- // them in `timeBased` with empty peak windows would report
211
- // permanent off-peak and never wake at the dated boundary.
212
- applyEffectiveFallbackRates(model.cost, Reflect.get(fallback, "effectiveRates"));
197
+ if (cacheWrite !== undefined) cost.cacheWrite = cacheWrite;
198
+ applyDueDatedCostCard(cost, cards.fallback, now);
213
199
  }
214
200
  }
215
201
  const longContext = objectPayload(catalog.longContext);
@@ -221,20 +207,19 @@ export function applyCatalogCorrections(
221
207
  const output = numberField(longContext, "output");
222
208
  const cacheRead = numberField(longContext, "cacheRead");
223
209
  const cacheWrite = numberField(longContext, "cacheWrite");
224
- const base = model.cost;
225
- const hasTokenPrice = base.input !== 0 || base.output !== 0 || base.cacheRead !== 0 || base.cacheWrite !== 0;
210
+ const hasTokenPrice = cost.input !== 0 || cost.output !== 0 || cost.cacheRead !== 0 || cost.cacheWrite !== 0;
226
211
  if (inputThreshold !== undefined && multiplier !== undefined && hasTokenPrice) {
227
212
  // Multiplier form: tier rates derive from the row's final base card,
228
213
  // after `cost-patch`/`cost-fallback` corrected the live list price.
229
- model.cost = {
230
- ...base,
214
+ cost = {
215
+ ...cost,
231
216
  longContext: {
232
217
  inputThreshold,
233
218
  ...(inclusive && { inputThresholdInclusive: true }),
234
- input: base.input * multiplier,
235
- output: base.output * multiplier,
236
- cacheRead: base.cacheRead * multiplier,
237
- cacheWrite: base.cacheWrite * multiplier,
219
+ input: cost.input * multiplier,
220
+ output: cost.output * (numberField(longContext, "outputMultiplier") ?? multiplier),
221
+ cacheRead: cost.cacheRead * multiplier,
222
+ cacheWrite: cost.cacheWrite * multiplier,
238
223
  },
239
224
  };
240
225
  } else if (
@@ -244,19 +229,94 @@ export function applyCatalogCorrections(
244
229
  cacheRead !== undefined &&
245
230
  cacheWrite !== undefined
246
231
  ) {
247
- model.cost = { ...model.cost, longContext: { inputThreshold, input, output, cacheRead, cacheWrite } };
232
+ cost = { ...cost, longContext: { inputThreshold, input, output, cacheRead, cacheWrite } };
248
233
  }
249
234
  }
250
235
  if (catalog.cacheReadAtInputRate === true) {
251
- const { longContext } = model.cost;
252
- model.cost = {
253
- ...model.cost,
254
- cacheRead: model.cost.input,
236
+ const { longContext } = cost;
237
+ cost = {
238
+ ...cost,
239
+ cacheRead: cost.input,
255
240
  ...(longContext && { longContext: { ...longContext, cacheRead: longContext.input } }),
256
241
  };
257
242
  }
258
- if (catalog.timeBased !== undefined) {
259
- model.cost = { ...model.cost, timeBased: materializeTimeBasedCost(catalog.timeBased) };
243
+ const cacheWriteRate = catalog.cacheWriteRate;
244
+ if (typeof cacheWriteRate === "number") {
245
+ const { longContext } = cost;
246
+ cost = {
247
+ ...cost,
248
+ cacheWrite: cost.input * cacheWriteRate,
249
+ ...(longContext && { longContext: { ...longContext, cacheWrite: longContext.input * cacheWriteRate } }),
250
+ };
251
+ }
252
+ return cost;
253
+ }
254
+
255
+ /**
256
+ * Every price period the dated `cost-patch`/`cost-fallback` cards define, for
257
+ * request-time selection (`calculateUsageCost`): a row built before a change
258
+ * still bills requests after it at the new price, and a leading card keeps
259
+ * earlier requests at the old one on rows built after it. Undefined when no
260
+ * card changes the price (no cards, or a fallback the upstream price outranks).
261
+ */
262
+ function scheduledRates(
263
+ upstream: ModelCost,
264
+ catalog: Record<string, unknown>,
265
+ cards: DatedCostCards,
266
+ ): EffectiveTokenCost[] | undefined {
267
+ const dates = [...cards.patch, ...cards.fallback].map(card => card.effectiveFrom).sort((a, b) => a - b);
268
+ if (dates.length === 0) return undefined;
269
+ const periods: EffectiveTokenCost[] = [];
270
+ let previousPrice: string | undefined;
271
+ for (const effectiveFrom of [EARLIEST_EFFECTIVE_FROM, ...dates]) {
272
+ const { input, output, cacheRead, cacheWrite, longContext } = correctedCost(
273
+ upstream,
274
+ catalog,
275
+ cards,
276
+ effectiveFrom,
277
+ );
278
+ const price = { input, output, cacheRead, cacheWrite, ...(longContext && { longContext }) };
279
+ const key = JSON.stringify(price);
280
+ if (key === previousPrice) continue;
281
+ previousPrice = key;
282
+ periods.push({ effectiveFrom, ...price });
283
+ }
284
+ return periods.length > 1 ? periods : undefined;
285
+ }
286
+
287
+ /**
288
+ * Applies reviewed catalog-data value corrections (`cost-patch`,
289
+ * `cache-read-at-input-rate`, `cache-write-rate`, `limits-patch`,
290
+ * `long-context-cost`, `context-window-floor`, `input-modalities`) onto an
291
+ * upstream-sourced spec. Applied by
292
+ * `buildModel` to every upstream-sourced spec; user-authored overrides are
293
+ * recomposed after building by the override applicators, so explicit user
294
+ * limits and pricing still win.
295
+ */
296
+ export function applyCatalogCorrections(
297
+ model: Pick<ModelSpec<Api>, "cost" | "contextWindow" | "maxTokens" | "input">,
298
+ catalog: Record<string, unknown>,
299
+ ): void {
300
+ const upstream = model.cost;
301
+ const cards: DatedCostCards = {
302
+ patch: parseDatedCostCards(catalog.costPatch, "cost-patch"),
303
+ fallback: parseDatedCostCards(catalog.costFallback, "cost-fallback"),
304
+ };
305
+ // The card due now is the row's displayed price; the dated schedule rides
306
+ // along so rows reused past a change (bundles, caches, long sessions) still
307
+ // bill each request at the card in force when it was made.
308
+ model.cost = correctedCost(upstream, catalog, cards, Date.now());
309
+ const recurring = catalog.timeBased === undefined ? undefined : materializeTimeBasedCost(catalog.timeBased);
310
+ const dated = scheduledRates(upstream, catalog, cards);
311
+ if (dated !== undefined) {
312
+ if (recurring?.effectiveRates !== undefined) {
313
+ throw new Error(
314
+ "Dated cost-patch/cost-fallback effective-rates cannot combine with time-based-cost effective-rates",
315
+ );
316
+ }
317
+ model.cost = { ...model.cost, timeBased: { ...(recurring ?? NO_RECURRING_TARIFF), effectiveRates: dated } };
318
+ } else if (recurring !== undefined) {
319
+ model.cost = { ...model.cost, timeBased: recurring };
260
320
  }
261
321
  const limitsPatch = objectPayload(catalog.limitsPatch);
262
322
  if (limitsPatch !== undefined) {
@@ -351,8 +411,9 @@ export function buildModel<TApi extends Api>(spec: ModelSpec<TApi>): Model<TApi>
351
411
  const model: Model<TApi> = {
352
412
  ...spec,
353
413
  // 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.
414
+ // `reasoning: false` discovery default, and an unclassified neutral spec
415
+ // gets an unverified ladder (see `resolveThinkingPolicy`); materialize
416
+ // either for transports and the picker.
356
417
  reasoning: spec.reasoning || policy.thinking !== undefined,
357
418
  name: cleanModelName(spec.name),
358
419
  identity: policy.identity,
@@ -244,8 +244,8 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
244
244
  "first-party-provider": wire("firstPartyProvider", ["anthropic"]),
245
245
  "honors-claude-code-beta-kill-switch": wire("honorsClaudeCodeBetaKillSwitch", ["anthropic"]),
246
246
  "inject-claude-code-instruction": wire("injectClaudeCodeInstruction", ["anthropic"]),
247
- "max-image-dimension": wire("maxImageDimension", ["anthropic"]),
248
- "max-image-payload-bytes": wire("maxImagePayloadBytes", ["anthropic"]),
247
+ "max-image-dimension": wire("maxImageDimension", ["anthropic", "google"]),
248
+ "max-image-payload-bytes": wire("maxImagePayloadBytes", ["anthropic", "google"]),
249
249
  "official-endpoint": wire("officialEndpoint", ["anthropic", "openai-responses"]),
250
250
  "replay-unsigned-thinking": wire("replayUnsignedThinking", ["anthropic"]),
251
251
  "requires-thinking-enabled": wire("requiresThinkingEnabled", ["anthropic"]),
@@ -329,6 +329,8 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
329
329
  "thinking-suppress-when-off": { key: "suppressWhenOff", set: "thinking", shape: "scalar" },
330
330
  "thinking-supports-display": { key: "supportsDisplay", set: "thinking", shape: "scalar" },
331
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" },
332
334
 
333
335
  // ── catalog metadata ──
334
336
  "apply-patch-tool-type": {
@@ -368,6 +370,19 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
368
370
  shape: "scalar",
369
371
  values: [true, false],
370
372
  },
373
+ /**
374
+ * The model's input classifier refuses requests that reproduce assistant
375
+ * reasoning as quoted text (`reasoning_extraction`), so reasoning must not
376
+ * be replayed as text into its input: compaction archives, summarizer
377
+ * transcripts and demoted interrupted turns leave it out. Declared per class
378
+ * in `classes/*.kdl`.
379
+ */
380
+ "classifier-cot-extraction": {
381
+ key: "classifierCotExtraction",
382
+ set: "catalog",
383
+ shape: "scalar",
384
+ values: [true, false],
385
+ },
371
386
  "clamp-context-override": { key: "clampContextOverride", set: "catalog", shape: "scalar" },
372
387
  "context-promotion-target": { key: "contextPromotionTarget", set: "catalog", shape: "scalar" },
373
388
  "context-window-floor": { key: "contextWindowFloor", set: "catalog", shape: "scalar" },
@@ -389,6 +404,26 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
389
404
  shape: "scalar",
390
405
  values: [true, false],
391
406
  },
407
+ /**
408
+ * The host bills cache writes at this multiple of the row's live `input`
409
+ * price, for hosts whose discovery reports cache-write tokens but no
410
+ * cache-write rate. It replaces any `cacheWrite` the row carries, including
411
+ * a `cost-patch`/`cost-fallback` value and an absolute long-context tier's
412
+ * `cacheWrite` (rebased on the tier's `input`).
413
+ */
414
+ "cache-write-rate": { key: "cacheWriteRate", set: "catalog", shape: "scalar" },
415
+ /**
416
+ * How the model line bills a replayed signed thinking block as input:
417
+ * `output` bills the reasoning its signature carries, at the size the turn
418
+ * billed as output, and not the signature's bytes. Unassigned: estimated
419
+ * from the signature's bytes. Read through `./thinking-replay`.
420
+ */
421
+ "thinking-replay-tokens": {
422
+ key: "thinkingReplayTokens",
423
+ set: "catalog",
424
+ shape: "scalar",
425
+ values: ["output", "signature"],
426
+ },
392
427
  "delegation-bias": { key: "delegationBias", set: "catalog", shape: "scalar", values: DELEGATION_BIASES },
393
428
  "discovery-api": { key: "discoveryApi", set: "catalog", shape: "scalar" },
394
429
  "edit-prompt-variant": { key: "editPromptVariant", set: "catalog", shape: "scalar", values: ["full", "compact"] },
@@ -405,7 +440,28 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
405
440
  "hosted-image": { key: "hostedImage", set: "catalog", shape: "scalar", values: [true, false] },
406
441
  /** How the model line bills an input image; shape and formulas in `./image-tokenization`. */
407
442
  "image-tokenization": { key: "imageTokenization", set: "catalog", shape: "object" },
443
+ /**
444
+ * Most images one request may carry, documents included where the host
445
+ * counts them. Unassigned: unknown host, held to a conservative floor.
446
+ */
447
+ "image-count-budget": { key: "imageCountBudget", set: "catalog", shape: "scalar" },
448
+ /**
449
+ * Replaces `image-count-budget` while the request reaches the official
450
+ * endpoint (pi-ai `reachesOfficialEndpoint`): a limit measured or documented
451
+ * for the host's own endpoint, which a custom `baseUrl` or an
452
+ * `ANTHROPIC_BASE_URL`/`FOUNDRY_BASE_URL` reroute (proxy, gateway) need not share.
453
+ */
454
+ "official-image-count-budget": { key: "officialImageCountBudget", set: "catalog", shape: "scalar" },
455
+ /**
456
+ * Opt-in: the reader may get a fresh `read` result as snapcompact frames
457
+ * (`snapcompact.toolResults`), because it edits as exactly from frames as
458
+ * from text. Unassigned: `read` results stay text, since a reader that
459
+ * misreads indentation in frames makes inexact edits.
460
+ */
461
+ "snapcompact-image-reads": { key: "snapcompactImageReads", set: "catalog", shape: "scalar", values: [true, false] },
408
462
  "image-model": { key: "imageModel", set: "catalog", shape: "scalar" },
463
+ /** Longest image edge the host passes to the model: it downscales larger images to fit first. */
464
+ "image-max-edge": { key: "imageMaxEdge", set: "catalog", shape: "scalar" },
409
465
  "inline-image-byte-budget": { key: "inlineImageByteBudget", set: "catalog", shape: "scalar" },
410
466
  /**
411
467
  * Most questions one System One judgment request may carry; the judge
@@ -415,6 +471,12 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
415
471
  "limits-patch": { key: "limitsPatch", set: "catalog", shape: "object" },
416
472
  "long-context-cost": { key: "longContext", set: "catalog", shape: "object" },
417
473
  "prompt-cache": { key: "promptCache", set: "catalog", shape: "object" },
474
+ "task-sibling-prompt-cache": {
475
+ key: "taskSiblingPromptCache",
476
+ set: "catalog",
477
+ shape: "scalar",
478
+ values: [true, false],
479
+ },
418
480
  /**
419
481
  * Prompt-cache lookback in block positions: how far back from a cache
420
482
  * 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,
@@ -117,11 +119,24 @@ class IdentityFacts {
117
119
  }
118
120
  }
119
121
 
122
+ /**
123
+ * The id a spec resolves identity and rules under: its own, unless that names
124
+ * no model and the row sends a model that does on the wire. A Devin Fusion
125
+ * pairing runs only its lead, so the lead's reviewed rules (cache-write rate,
126
+ * long-context tier) apply to it at build time.
127
+ */
128
+ function policyModelId<TApi extends Api>(spec: ModelSpec<TApi>): string {
129
+ const wireId = spec.requestModelId;
130
+ if (wireId === undefined || wireId === spec.id) return spec.id;
131
+ if (classifyModel(spec.provider, spec.id).class !== "unknown") return spec.id;
132
+ return classifyModel(spec.provider, wireId).class === "unknown" ? spec.id : wireId;
133
+ }
134
+
120
135
  function resolveIdentity<TApi extends Api>(spec: ModelSpec<TApi>): ModelIdentity {
121
136
  // Strict on purpose: ambiguous identity is a rule-authoring defect surfaced
122
137
  // at build/CI time, never silently degraded to `unknown`. Discovery
123
138
  // normalization opts into leniency through `classifyModel` directly.
124
- return classifyModel(spec.provider, spec.id);
139
+ return classifyModel(spec.provider, policyModelId(spec));
125
140
  }
126
141
 
127
142
  // ---------------------------------------------------------------------------
@@ -796,7 +811,11 @@ function resolveOpenAIResponsesPolicy(
796
811
  supportsObfuscationOptOut: isOpenAIUrl || provider === "openai",
797
812
  storeResponses: false,
798
813
  connectionBoundNativeHistory: false,
799
- officialEndpoint: isOfficialOpenAIEndpoint(provider, baseUrl),
814
+ // The Codex backend is official at the URL its requests default to when `baseUrl` is unset, and at paths
815
+ // under it such as `/codex` and `/codex/responses`, which its requests resolve to the same endpoint.
816
+ officialEndpoint:
817
+ isOfficialOpenAIEndpoint(provider, baseUrl) ||
818
+ (api === "openai-codex-responses" && isOfficialCodexApiUrl(baseUrl)),
800
819
  harmonyLeakMitigation: false,
801
820
  rejectRootObjectUnion: false,
802
821
  retryWithoutStrictOnGrammarError: false,
@@ -980,6 +999,10 @@ function resolveGooglePolicy(
980
999
  claudeThinkingBetaHeader: false,
981
1000
  antigravityClaudeToolMode: false,
982
1001
  stripImageInput: false,
1002
+ // Unset means "no image contract": images go out as sent. Keys exist so
1003
+ // a KDL rule or a user `compat` block can declare one.
1004
+ maxImageDimension: undefined,
1005
+ maxImagePayloadBytes: undefined,
983
1006
  };
984
1007
  applyWireAxes(compat, axes.wire, spec.api);
985
1008
  applyCompatOverrides(compat, spec.compat);
@@ -1089,6 +1112,7 @@ interface RuleThinking {
1089
1112
  supportsDisplay?: boolean;
1090
1113
  prefixBinding?: boolean;
1091
1114
  upgradeNeutral?: boolean;
1115
+ unverified?: boolean;
1092
1116
  }
1093
1117
 
1094
1118
  function readRuleThinking(axes: ResolvedAxes): RuleThinking {
@@ -1109,6 +1133,7 @@ function readRuleThinking(axes: ResolvedAxes): RuleThinking {
1109
1133
  if (typeof raw.supportsDisplay === "boolean") out.supportsDisplay = raw.supportsDisplay;
1110
1134
  if (typeof raw.prefixBinding === "boolean") out.prefixBinding = raw.prefixBinding;
1111
1135
  if (typeof raw.upgradeNeutral === "boolean") out.upgradeNeutral = raw.upgradeNeutral;
1136
+ if (typeof raw.unverified === "boolean") out.unverified = raw.unverified;
1112
1137
  return out;
1113
1138
  }
1114
1139
 
@@ -1156,8 +1181,16 @@ function resolveThinkingPolicy<TApi extends Api>(
1156
1181
  // not re-expand it into an unadvertised ladder. Absent metadata is
1157
1182
  // repaired only where KDL opts in with `thinking-upgrade-neutral`
1158
1183
  // alongside a reviewed `thinking-efforts` ladder (the cascade upgrade for
1159
- // stale source capability data); otherwise the neutral default holds.
1160
- if (!spec.reasoning && (explicitThinking !== undefined || rule.upgradeNeutral !== true)) return undefined;
1184
+ // stale source capability data). On deployments whose discovery reports no
1185
+ // capabilities (`thinking-unverified`), an unclassified neutral spec — nothing
1186
+ // says whether it reasons — gets an unverified ladder the user may opt into.
1187
+ const unverified = !compatReasoning(spec, axes);
1188
+ if (
1189
+ unverified &&
1190
+ (explicitThinking !== undefined || facts.identity.class !== "unknown" || rule.unverified !== true)
1191
+ ) {
1192
+ return undefined;
1193
+ }
1161
1194
  if (
1162
1195
  spec.provider === "cline-pass" &&
1163
1196
  compat !== undefined &&
@@ -1181,9 +1214,14 @@ function resolveThinkingPolicy<TApi extends Api>(
1181
1214
  if (config.efforts.length === 0) {
1182
1215
  throw new Error(`Model ${spec.provider}/${spec.id} resolved to an empty thinking range`);
1183
1216
  }
1184
- if (rule.defaultLevel !== undefined) config.defaultLevel = rule.defaultLevel;
1185
1217
  const effortMap = mergeEffortMap(spec, rule.effortMap, compat, config.efforts);
1186
1218
  if (effortMap !== undefined) config.effortMap = effortMap;
1219
+ if (unverified) {
1220
+ // Off-by-default opt-in: no default level, mandatory effort, or wire off-switch.
1221
+ config.unverified = true;
1222
+ return config;
1223
+ }
1224
+ if (rule.defaultLevel !== undefined) config.defaultLevel = rule.defaultLevel;
1187
1225
  if (rule.effortBudgets !== undefined) config.effortBudgets = rule.effortBudgets;
1188
1226
  const supportsDisplay = rule.supportsDisplay ?? defaultSupportsDisplay(spec, facts);
1189
1227
  if (supportsDisplay) config.supportsDisplay = true;
@@ -1280,7 +1318,7 @@ function buildResolveTarget<TApi extends Api>(
1280
1318
  provider: providerType,
1281
1319
  api: spec.api,
1282
1320
  class: identity.class,
1283
- model: spec.id,
1321
+ model: policyModelId(spec),
1284
1322
  reasoning: Boolean(spec.reasoning),
1285
1323
  };
1286
1324
  if (identity.family !== undefined) target.family = identity.family;
@@ -1316,7 +1354,8 @@ export function resolveModelPolicy<TApi extends Api>(
1316
1354
  spec: ModelSpec<TApi>,
1317
1355
  route?: ResolveRoute,
1318
1356
  ): ResolvedModelPolicy<TApi>;
1319
- export function resolveModelPolicy(spec: ModelSpec<Api>, route?: ResolveRoute): ResolvedModelPolicy<Api> {
1357
+ export function resolveModelPolicy(input: ModelSpec<Api>, route?: ResolveRoute): ResolvedModelPolicy<Api> {
1358
+ const spec = withoutUnverifiedThinking(input);
1320
1359
  const identity = resolveIdentity(spec);
1321
1360
  const facts = new IdentityFacts(identity);
1322
1361
  const target = buildResolveTarget(spec, identity);