@linxiraos/pi-catalog 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +4 -2
  3. package/THIRD-PARTY-NOTICES.txt +55 -5
  4. package/dist/types/build.d.ts +3 -2
  5. package/dist/types/compat/auth-ids.d.ts +2 -2
  6. package/dist/types/compat/axes.d.ts +1 -1
  7. package/dist/types/compat/behavior.d.ts +5 -0
  8. package/dist/types/compat/catalog-endpoint.d.ts +8 -0
  9. package/dist/types/compat/image-tokenization.d.ts +9 -1
  10. package/dist/types/compat/judgment-images.d.ts +25 -0
  11. package/dist/types/compat/provider-ids.d.ts +1 -1
  12. package/dist/types/compat/request-compression.d.ts +23 -0
  13. package/dist/types/compat/request-size.d.ts +12 -3
  14. package/dist/types/compat/thinking-replay.d.ts +9 -0
  15. package/dist/types/compat/types.d.ts +4 -0
  16. package/dist/types/discovery/antigravity.d.ts +7 -0
  17. package/dist/types/model-cache.d.ts +7 -0
  18. package/dist/types/model-manager.d.ts +6 -0
  19. package/dist/types/model-thinking.d.ts +11 -1
  20. package/dist/types/models.d.ts +16 -1
  21. package/dist/types/pricing.d.ts +37 -1
  22. package/dist/types/provider-models/openai-compat.d.ts +18 -46
  23. package/dist/types/types.d.ts +63 -12
  24. package/dist/types/wire/codex.d.ts +2 -0
  25. package/package.json +6 -6
  26. package/src/build.d.ts +3 -2
  27. package/src/build.ts +159 -80
  28. package/src/compat/auth-ids.ts +2 -0
  29. package/src/compat/axes.d.ts +1 -1
  30. package/src/compat/axes.ts +129 -4
  31. package/src/compat/behavior.d.ts +5 -0
  32. package/src/compat/behavior.ts +9 -1
  33. package/src/compat/cascade.ts +4 -1
  34. package/src/compat/catalog-endpoint.ts +43 -0
  35. package/src/compat/image-tokenization.ts +28 -10
  36. package/src/compat/judgment-images.ts +83 -0
  37. package/src/compat/provider-ids.d.ts +1 -1
  38. package/src/compat/provider-ids.ts +1 -0
  39. package/src/compat/request-compression.ts +43 -0
  40. package/src/compat/request-size.ts +20 -5
  41. package/src/compat/resolve.ts +90 -53
  42. package/src/compat/rules/README.md +32 -3
  43. package/src/compat/rules/auth/_order.kdl +1 -1
  44. package/src/compat/rules/auth/google-antigravity.kdl +1 -1
  45. package/src/compat/rules/auth/sambanova.kdl +9 -0
  46. package/src/compat/rules/auth/stencil.kdl +1 -1
  47. package/src/compat/rules/classes/anthropic.kdl +42 -2
  48. package/src/compat/rules/classes/deepseek.kdl +4 -0
  49. package/src/compat/rules/classes/gemini.kdl +46 -2
  50. package/src/compat/rules/classes/meta.kdl +7 -0
  51. package/src/compat/rules/classes/minimax.kdl +11 -1
  52. package/src/compat/rules/classes/openai.kdl +34 -3
  53. package/src/compat/rules/classes/xai.kdl +26 -0
  54. package/src/compat/rules/providers/amazon-bedrock.kdl +1 -0
  55. package/src/compat/rules/providers/anthropic.kdl +23 -0
  56. package/src/compat/rules/providers/azure.kdl +2 -0
  57. package/src/compat/rules/providers/cerebras.kdl +3 -0
  58. package/src/compat/rules/providers/cline-pass.kdl +16 -0
  59. package/src/compat/rules/providers/commandcode.kdl +10 -0
  60. package/src/compat/rules/providers/cursor.kdl +50 -1
  61. package/src/compat/rules/providers/deepinfra.kdl +3 -1
  62. package/src/compat/rules/providers/devin.kdl +117 -6
  63. package/src/compat/rules/providers/factory-droid.kdl +6 -0
  64. package/src/compat/rules/providers/firepass.kdl +15 -0
  65. package/src/compat/rules/providers/fireworks.kdl +26 -0
  66. package/src/compat/rules/providers/github-copilot.kdl +24 -0
  67. package/src/compat/rules/providers/google-antigravity.kdl +37 -0
  68. package/src/compat/rules/providers/google-gemini-cli.kdl +1 -0
  69. package/src/compat/rules/providers/google-vertex.kdl +5 -0
  70. package/src/compat/rules/providers/google.kdl +1 -0
  71. package/src/compat/rules/providers/image-tokenization.kdl +2 -1
  72. package/src/compat/rules/providers/local.kdl +8 -1
  73. package/src/compat/rules/providers/meta.kdl +16 -0
  74. package/src/compat/rules/providers/moonshot.kdl +2 -0
  75. package/src/compat/rules/providers/muse-code.kdl +49 -0
  76. package/src/compat/rules/providers/native-tools.kdl +3 -0
  77. package/src/compat/rules/providers/ollama-cloud.kdl +10 -0
  78. package/src/compat/rules/providers/openai-codex.kdl +29 -5
  79. package/src/compat/rules/providers/openai.kdl +1 -0
  80. package/src/compat/rules/providers/opencode-go.kdl +35 -0
  81. package/src/compat/rules/providers/opencode-zen.kdl +4 -0
  82. package/src/compat/rules/providers/openrouter.kdl +19 -0
  83. package/src/compat/rules/providers/sambanova.kdl +60 -0
  84. package/src/compat/rules/providers/tool-call-names.kdl +37 -0
  85. package/src/compat/rules/providers/typesafe.kdl +11 -0
  86. package/src/compat/rules/providers/umans.kdl +7 -0
  87. package/src/compat/rules/providers/venice.kdl +7 -0
  88. package/src/compat/rules/providers/xai-oauth.kdl +16 -6
  89. package/src/compat/rules/providers/xai.kdl +3 -0
  90. package/src/compat/rules/providers/zai.kdl +5 -0
  91. package/src/compat/rules/runtime/behavior.kdl +128 -8
  92. package/src/compat/rules/taxonomy/_collapse.kdl +11 -0
  93. package/src/compat/rules/taxonomy/gemma.kdl +1 -0
  94. package/src/compat/rules/taxonomy/meta.kdl +7 -0
  95. package/src/compat/rules/taxonomy/openai.kdl +2 -0
  96. package/src/compat/rules.json +1 -1
  97. package/src/compat/thinking-replay.ts +33 -0
  98. package/src/compat/types.d.ts +4 -0
  99. package/src/compat/types.ts +4 -0
  100. package/src/discovery/antigravity.d.ts +7 -0
  101. package/src/discovery/antigravity.ts +13 -0
  102. package/src/discovery/codex.ts +0 -4
  103. package/src/discovery/cursor.ts +28 -20
  104. package/src/discovery/devin.ts +7 -8
  105. package/src/model-cache.d.ts +7 -0
  106. package/src/model-cache.ts +8 -0
  107. package/src/model-manager.d.ts +6 -0
  108. package/src/model-manager.ts +41 -21
  109. package/src/model-thinking.d.ts +80 -0
  110. package/src/model-thinking.ts +13 -1
  111. package/src/model-tokenizer.ts +7 -1
  112. package/src/models.d.ts +16 -1
  113. package/src/models.json +1 -1
  114. package/src/models.ts +26 -12
  115. package/src/pricing.d.ts +37 -1
  116. package/src/pricing.ts +123 -12
  117. package/src/provider-models/bundled-references.ts +5 -2
  118. package/src/provider-models/cache-provider-id.ts +1 -1
  119. package/src/provider-models/descriptors.ts +2 -0
  120. package/src/provider-models/google.ts +7 -1
  121. package/src/provider-models/openai-compat.d.ts +18 -46
  122. package/src/provider-models/openai-compat.ts +188 -104
  123. package/src/provider-models/special.ts +1 -0
  124. package/src/types.d.ts +63 -12
  125. package/src/types.ts +71 -9
  126. package/src/wire/codex.d.ts +2 -0
  127. 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");
@@ -81,6 +54,11 @@ function applyCatalogAssignments<TApi extends Api>(model: Model<TApi>, catalog:
81
54
  } else {
82
55
  delete model.contextWindowAuthoritative;
83
56
  }
57
+ if (catalog.longContextWindowUncapped === true) {
58
+ model.longContextWindowUncapped = true;
59
+ } else {
60
+ delete model.longContextWindowUncapped;
61
+ }
84
62
  const webSearch = catalog.webSearch;
85
63
  if (
86
64
  webSearch === "gemini" ||
@@ -142,6 +120,11 @@ function applyCatalogAssignments<TApi extends Api>(model: Model<TApi>, catalog:
142
120
  } else {
143
121
  delete model.requiresCursorToolSchemaProjection;
144
122
  }
123
+ if (catalog.toolSchemasInContext === false) {
124
+ model.toolSchemasInContext = false;
125
+ } else {
126
+ delete model.toolSchemasInContext;
127
+ }
145
128
  const requiresToolResultImageHoisting = catalog.requiresToolResultImageHoisting;
146
129
  if (requiresToolResultImageHoisting === true) {
147
130
  model.requiresToolResultImageHoisting = true;
@@ -164,52 +147,65 @@ function applyCatalogAssignments<TApi extends Api>(model: Model<TApi>, catalog:
164
147
  }
165
148
  }
166
149
 
150
+ /** The parsed dated cards of a row's `cost-patch` and `cost-fallback`. */
151
+ interface DatedCostCards {
152
+ patch: readonly DatedCostCard[];
153
+ fallback: readonly DatedCostCard[];
154
+ }
155
+
167
156
  /**
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.
157
+ * The row's corrected price card at `now`: `cost-patch`, then `cost-fallback`
158
+ * (each with its dated card due at `now`), then the long-context tier and
159
+ * cache-rate derivations, all from the upstream card.
174
160
  */
175
- export function applyCatalogCorrections(
176
- model: Pick<ModelSpec<Api>, "cost" | "contextWindow" | "maxTokens" | "input">,
161
+ function correctedCost(
162
+ upstream: ModelCost,
177
163
  catalog: Record<string, unknown>,
178
- ): void {
164
+ cards: DatedCostCards,
165
+ now: number,
166
+ ): ModelCost {
167
+ let cost: ModelCost = { ...upstream };
168
+ const upstreamCard =
169
+ upstream.timeBased?.effectiveRates?.[0]?.effectiveFrom === EARLIEST_EFFECTIVE_FROM
170
+ ? dueEffectiveRate(upstream.timeBased, now)
171
+ : undefined;
172
+ if (upstreamCard !== undefined) {
173
+ // A row rebuilt from an already built one (dynamic merges, overrides)
174
+ // carries the dated schedule compiled below. Its leading card covers all
175
+ // history, so the scalar legs only ever held the card due at the first
176
+ // build; start from the card due now instead.
177
+ const { effectiveFrom: _effectiveFrom, ...card } = upstreamCard;
178
+ const { longContext: _longContext, ...base } = cost;
179
+ cost = { ...base, ...card };
180
+ }
179
181
  const patch = objectPayload(catalog.costPatch);
180
182
  if (patch !== undefined) {
181
- model.cost = { ...model.cost };
182
183
  const input = numberField(patch, "input");
183
- if (input !== undefined) model.cost.input = input;
184
+ if (input !== undefined) cost.input = input;
184
185
  const output = numberField(patch, "output");
185
- if (output !== undefined) model.cost.output = output;
186
+ if (output !== undefined) cost.output = output;
186
187
  const cacheRead = numberField(patch, "cacheRead");
187
- if (cacheRead !== undefined) model.cost.cacheRead = cacheRead;
188
+ if (cacheRead !== undefined) cost.cacheRead = cacheRead;
188
189
  const cacheWrite = numberField(patch, "cacheWrite");
189
- if (cacheWrite !== undefined) model.cost.cacheWrite = cacheWrite;
190
+ if (cacheWrite !== undefined) cost.cacheWrite = cacheWrite;
191
+ applyDueDatedCostCard(cost, cards.patch, now);
190
192
  }
191
193
  const fallback = objectPayload(catalog.costFallback);
192
194
  if (fallback !== undefined) {
193
- const base = model.cost;
194
- const hasTokenPrice = base.input !== 0 || base.output !== 0 || base.cacheRead !== 0 || base.cacheWrite !== 0;
195
+ const hasTokenPrice = cost.input !== 0 || cost.output !== 0 || cost.cacheRead !== 0 || cost.cacheWrite !== 0;
195
196
  if (!hasTokenPrice) {
196
197
  // Upstream reported no token price (plan-included or promo-free
197
198
  // rows): seed the reviewed list price instead of overwriting real
198
199
  // discovery data the way `cost-patch` would.
199
- model.cost = { ...model.cost };
200
200
  const input = numberField(fallback, "input");
201
- if (input !== undefined) model.cost.input = input;
201
+ if (input !== undefined) cost.input = input;
202
202
  const output = numberField(fallback, "output");
203
- if (output !== undefined) model.cost.output = output;
203
+ if (output !== undefined) cost.output = output;
204
204
  const cacheRead = numberField(fallback, "cacheRead");
205
- if (cacheRead !== undefined) model.cost.cacheRead = cacheRead;
205
+ if (cacheRead !== undefined) cost.cacheRead = cacheRead;
206
206
  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"));
207
+ if (cacheWrite !== undefined) cost.cacheWrite = cacheWrite;
208
+ applyDueDatedCostCard(cost, cards.fallback, now);
213
209
  }
214
210
  }
215
211
  const longContext = objectPayload(catalog.longContext);
@@ -221,20 +217,19 @@ export function applyCatalogCorrections(
221
217
  const output = numberField(longContext, "output");
222
218
  const cacheRead = numberField(longContext, "cacheRead");
223
219
  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;
220
+ const hasTokenPrice = cost.input !== 0 || cost.output !== 0 || cost.cacheRead !== 0 || cost.cacheWrite !== 0;
226
221
  if (inputThreshold !== undefined && multiplier !== undefined && hasTokenPrice) {
227
222
  // Multiplier form: tier rates derive from the row's final base card,
228
223
  // after `cost-patch`/`cost-fallback` corrected the live list price.
229
- model.cost = {
230
- ...base,
224
+ cost = {
225
+ ...cost,
231
226
  longContext: {
232
227
  inputThreshold,
233
228
  ...(inclusive && { inputThresholdInclusive: true }),
234
- input: base.input * multiplier,
235
- output: base.output * multiplier,
236
- cacheRead: base.cacheRead * multiplier,
237
- cacheWrite: base.cacheWrite * multiplier,
229
+ input: cost.input * multiplier,
230
+ output: cost.output * (numberField(longContext, "outputMultiplier") ?? multiplier),
231
+ cacheRead: cost.cacheRead * multiplier,
232
+ cacheWrite: cost.cacheWrite * multiplier,
238
233
  },
239
234
  };
240
235
  } else if (
@@ -244,19 +239,94 @@ export function applyCatalogCorrections(
244
239
  cacheRead !== undefined &&
245
240
  cacheWrite !== undefined
246
241
  ) {
247
- model.cost = { ...model.cost, longContext: { inputThreshold, input, output, cacheRead, cacheWrite } };
242
+ cost = { ...cost, longContext: { inputThreshold, input, output, cacheRead, cacheWrite } };
248
243
  }
249
244
  }
250
245
  if (catalog.cacheReadAtInputRate === true) {
251
- const { longContext } = model.cost;
252
- model.cost = {
253
- ...model.cost,
254
- cacheRead: model.cost.input,
246
+ const { longContext } = cost;
247
+ cost = {
248
+ ...cost,
249
+ cacheRead: cost.input,
255
250
  ...(longContext && { longContext: { ...longContext, cacheRead: longContext.input } }),
256
251
  };
257
252
  }
258
- if (catalog.timeBased !== undefined) {
259
- model.cost = { ...model.cost, timeBased: materializeTimeBasedCost(catalog.timeBased) };
253
+ const cacheWriteRate = catalog.cacheWriteRate;
254
+ if (typeof cacheWriteRate === "number") {
255
+ const { longContext } = cost;
256
+ cost = {
257
+ ...cost,
258
+ cacheWrite: cost.input * cacheWriteRate,
259
+ ...(longContext && { longContext: { ...longContext, cacheWrite: longContext.input * cacheWriteRate } }),
260
+ };
261
+ }
262
+ return cost;
263
+ }
264
+
265
+ /**
266
+ * Every price period the dated `cost-patch`/`cost-fallback` cards define, for
267
+ * request-time selection (`calculateUsageCost`): a row built before a change
268
+ * still bills requests after it at the new price, and a leading card keeps
269
+ * earlier requests at the old one on rows built after it. Undefined when no
270
+ * card changes the price (no cards, or a fallback the upstream price outranks).
271
+ */
272
+ function scheduledRates(
273
+ upstream: ModelCost,
274
+ catalog: Record<string, unknown>,
275
+ cards: DatedCostCards,
276
+ ): EffectiveTokenCost[] | undefined {
277
+ const dates = [...cards.patch, ...cards.fallback].map(card => card.effectiveFrom).sort((a, b) => a - b);
278
+ if (dates.length === 0) return undefined;
279
+ const periods: EffectiveTokenCost[] = [];
280
+ let previousPrice: string | undefined;
281
+ for (const effectiveFrom of [EARLIEST_EFFECTIVE_FROM, ...dates]) {
282
+ const { input, output, cacheRead, cacheWrite, longContext } = correctedCost(
283
+ upstream,
284
+ catalog,
285
+ cards,
286
+ effectiveFrom,
287
+ );
288
+ const price = { input, output, cacheRead, cacheWrite, ...(longContext && { longContext }) };
289
+ const key = JSON.stringify(price);
290
+ if (key === previousPrice) continue;
291
+ previousPrice = key;
292
+ periods.push({ effectiveFrom, ...price });
293
+ }
294
+ return periods.length > 1 ? periods : undefined;
295
+ }
296
+
297
+ /**
298
+ * Applies reviewed catalog-data value corrections (`cost-patch`,
299
+ * `cache-read-at-input-rate`, `cache-write-rate`, `limits-patch`,
300
+ * `long-context-cost`, `context-window-floor`, `input-modalities`) onto an
301
+ * upstream-sourced spec. Applied by
302
+ * `buildModel` to every upstream-sourced spec; user-authored overrides are
303
+ * recomposed after building by the override applicators, so explicit user
304
+ * limits and pricing still win.
305
+ */
306
+ export function applyCatalogCorrections(
307
+ model: Pick<ModelSpec<Api>, "cost" | "contextWindow" | "maxTokens" | "input">,
308
+ catalog: Record<string, unknown>,
309
+ ): void {
310
+ const upstream = model.cost;
311
+ const cards: DatedCostCards = {
312
+ patch: parseDatedCostCards(catalog.costPatch, "cost-patch"),
313
+ fallback: parseDatedCostCards(catalog.costFallback, "cost-fallback"),
314
+ };
315
+ // The card due now is the row's displayed price; the dated schedule rides
316
+ // along so rows reused past a change (bundles, caches, long sessions) still
317
+ // bill each request at the card in force when it was made.
318
+ model.cost = correctedCost(upstream, catalog, cards, Date.now());
319
+ const recurring = catalog.timeBased === undefined ? undefined : materializeTimeBasedCost(catalog.timeBased);
320
+ const dated = scheduledRates(upstream, catalog, cards);
321
+ if (dated !== undefined) {
322
+ if (recurring?.effectiveRates !== undefined) {
323
+ throw new Error(
324
+ "Dated cost-patch/cost-fallback effective-rates cannot combine with time-based-cost effective-rates",
325
+ );
326
+ }
327
+ model.cost = { ...model.cost, timeBased: { ...(recurring ?? NO_RECURRING_TARIFF), effectiveRates: dated } };
328
+ } else if (recurring !== undefined) {
329
+ model.cost = { ...model.cost, timeBased: recurring };
260
330
  }
261
331
  const limitsPatch = objectPayload(catalog.limitsPatch);
262
332
  if (limitsPatch !== undefined) {
@@ -265,6 +335,14 @@ export function applyCatalogCorrections(
265
335
  const maxTokens = numberField(limitsPatch, "maxTokens");
266
336
  if (maxTokens !== undefined) model.maxTokens = maxTokens;
267
337
  }
338
+ const maxTokensFallback = catalog.maxTokensFallback;
339
+ if (typeof maxTokensFallback === "number" && model.maxTokens === null) {
340
+ model.maxTokens = maxTokensFallback;
341
+ }
342
+ const maxTokensCeiling = catalog.maxTokensCeiling;
343
+ if (typeof maxTokensCeiling === "number" && model.maxTokens !== null && model.maxTokens > maxTokensCeiling) {
344
+ model.maxTokens = maxTokensCeiling;
345
+ }
268
346
  const contextWindowFloor = catalog.contextWindowFloor;
269
347
  if (typeof contextWindowFloor === "number") {
270
348
  model.contextWindow = Math.max(model.contextWindow ?? 0, contextWindowFloor);
@@ -351,8 +429,9 @@ export function buildModel<TApi extends Api>(spec: ModelSpec<TApi>): Model<TApi>
351
429
  const model: Model<TApi> = {
352
430
  ...spec,
353
431
  // 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.
432
+ // `reasoning: false` discovery default, and an unclassified neutral spec
433
+ // gets an unverified ladder (see `resolveThinkingPolicy`); materialize
434
+ // either for transports and the picker.
356
435
  reasoning: spec.reasoning || policy.thinking !== undefined,
357
436
  name: cleanModelName(spec.name),
358
437
  identity: policy.identity,
@@ -70,6 +70,7 @@ export type AuthProviderId =
70
70
  | "qianfan"
71
71
  | "qwen-portal"
72
72
  | "sakana"
73
+ | "sambanova"
73
74
  | "siliconflow"
74
75
  | "siliconflow-cn"
75
76
  | "singularityapi-dev"
@@ -157,6 +158,7 @@ export type LoginProviderId =
157
158
  | "qianfan"
158
159
  | "qwen-portal"
159
160
  | "sakana"
161
+ | "sambanova"
160
162
  | "siliconflow"
161
163
  | "siliconflow-cn"
162
164
  | "singularityapi-dev"
@@ -16,7 +16,7 @@ export type AxisShape = "scalar" | "array" | "object";
16
16
  /** Axis namespace: request-wire compat, thinking control surface, or catalog metadata. */
17
17
  export type AxisSet = "wire" | "thinking" | "catalog";
18
18
  /** Resolved compat record families a wire axis may be assigned onto. */
19
- export type CompatRecordName = "openai" | "openai-responses" | "anthropic" | "bedrock" | "devin" | "google" | "request";
19
+ export type CompatRecordName = "openai" | "openai-responses" | "anthropic" | "bedrock" | "devin" | "google" | "request" | "ollama";
20
20
  /** One axis definition: resolved key, namespace, shape, and applicability. */
21
21
  export interface AxisDef {
22
22
  /** Resolved camelCase field the directive assigns. */
@@ -20,7 +20,15 @@ export type AxisShape = "scalar" | "array" | "object";
20
20
  export type AxisSet = "wire" | "thinking" | "catalog";
21
21
 
22
22
  /** Resolved compat record families a wire axis may be assigned onto. */
23
- export type CompatRecordName = "openai" | "openai-responses" | "anthropic" | "bedrock" | "devin" | "google" | "request";
23
+ export type CompatRecordName =
24
+ | "openai"
25
+ | "openai-responses"
26
+ | "anthropic"
27
+ | "bedrock"
28
+ | "devin"
29
+ | "google"
30
+ | "request"
31
+ | "ollama";
24
32
 
25
33
  /** One axis definition: resolved key, namespace, shape, and applicability. */
26
34
  export interface AxisDef {
@@ -242,15 +250,16 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
242
250
  "escape-builtin-tool-names": wire("escapeBuiltinToolNames", ["anthropic"]),
243
251
  "fast-mode": wire("fastMode", ["anthropic"]),
244
252
  "first-party-provider": wire("firstPartyProvider", ["anthropic"]),
253
+ "honors-claude-code-beta-kill-switch": wire("honorsClaudeCodeBetaKillSwitch", ["anthropic"]),
245
254
  "inject-claude-code-instruction": wire("injectClaudeCodeInstruction", ["anthropic"]),
246
- "max-image-dimension": wire("maxImageDimension", ["anthropic"]),
247
- "max-image-payload-bytes": wire("maxImagePayloadBytes", ["anthropic"]),
255
+ "max-image-dimension": wire("maxImageDimension", ["anthropic", "google"]),
256
+ "max-image-payload-bytes": wire("maxImagePayloadBytes", ["anthropic", "google"]),
248
257
  "official-endpoint": wire("officialEndpoint", ["anthropic", "openai-responses"]),
249
258
  "replay-unsigned-thinking": wire("replayUnsignedThinking", ["anthropic"]),
250
259
  "requires-thinking-enabled": wire("requiresThinkingEnabled", ["anthropic"]),
251
260
  "requires-tool-result-id": wire("requiresToolResultId", ["anthropic"]),
252
261
  "signing-endpoint": wire("signingEndpoint", ["anthropic"]),
253
- "strip-thinking-history": wire("stripThinkingHistory", ["anthropic"]),
262
+ "strip-thinking-history": wire("stripThinkingHistory", ["openai", "anthropic", "ollama"]),
254
263
  "supports-context-management": wire("supportsContextManagement", ["anthropic"]),
255
264
  "supports-output-effort": wire("supportsOutputEffort", ["anthropic"]),
256
265
  "supports-eager-tool-input-streaming": wire("supportsEagerToolInputStreaming", ["anthropic"]),
@@ -328,6 +337,8 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
328
337
  "thinking-suppress-when-off": { key: "suppressWhenOff", set: "thinking", shape: "scalar" },
329
338
  "thinking-supports-display": { key: "supportsDisplay", set: "thinking", shape: "scalar" },
330
339
  "thinking-upgrade-neutral": { key: "upgradeNeutral", set: "thinking", shape: "scalar" },
340
+ /** Unclassified neutral ids here are capability-unknown: offer the ladder as opt-in. */
341
+ "thinking-unverified": { key: "unverified", set: "thinking", shape: "scalar" },
331
342
 
332
343
  // ── catalog metadata ──
333
344
  "apply-patch-tool-type": {
@@ -337,12 +348,21 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
337
348
  values: ["freeform", "function"],
338
349
  },
339
350
  "requires-native-tools": { key: "requiresNativeTools", set: "catalog", shape: "scalar", values: [true, false] },
351
+ "tool-schemas-in-context": { key: "toolSchemasInContext", set: "catalog", shape: "scalar", values: [true, false] },
340
352
  "requires-tool-free-history-for-tool-opt-out": {
341
353
  key: "requiresToolFreeHistoryForToolOptOut",
342
354
  set: "catalog",
343
355
  shape: "scalar",
344
356
  values: [true, false],
345
357
  },
358
+ /** Replayed tool calls must satisfy the target wire's declaration charset and length. */
359
+ "tool-call-name-charset": {
360
+ key: "toolCallNameCharset",
361
+ set: "catalog",
362
+ shape: "scalar",
363
+ values: ["ascii", "google"],
364
+ },
365
+ "tool-call-name-max-length": { key: "toolCallNameMaxLength", set: "catalog", shape: "scalar" },
346
366
  "preserves-max-output-tokens": {
347
367
  key: "preservesMaxOutputTokens",
348
368
  set: "catalog",
@@ -367,6 +387,19 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
367
387
  shape: "scalar",
368
388
  values: [true, false],
369
389
  },
390
+ /**
391
+ * The model's input classifier refuses requests that reproduce assistant
392
+ * reasoning as quoted text (`reasoning_extraction`), so reasoning must not
393
+ * be replayed as text into its input: compaction archives, summarizer
394
+ * transcripts and demoted interrupted turns leave it out. Declared per class
395
+ * in `classes/*.kdl`.
396
+ */
397
+ "classifier-cot-extraction": {
398
+ key: "classifierCotExtraction",
399
+ set: "catalog",
400
+ shape: "scalar",
401
+ values: [true, false],
402
+ },
370
403
  "clamp-context-override": { key: "clampContextOverride", set: "catalog", shape: "scalar" },
371
404
  "context-promotion-target": { key: "contextPromotionTarget", set: "catalog", shape: "scalar" },
372
405
  "context-window-floor": { key: "contextWindowFloor", set: "catalog", shape: "scalar" },
@@ -376,6 +409,19 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
376
409
  shape: "scalar",
377
410
  values: [true, false],
378
411
  },
412
+ /**
413
+ * The model's long-context price tier does not bound its runtime context
414
+ * window: with extended context off, the window is not capped at the
415
+ * tier's input threshold (subscription hosts carrying API-equivalent tier
416
+ * prices, or a model served at its full window). Explicit user context
417
+ * overrides still apply.
418
+ */
419
+ "long-context-window-uncapped": {
420
+ key: "longContextWindowUncapped",
421
+ set: "catalog",
422
+ shape: "scalar",
423
+ values: [true, false],
424
+ },
379
425
  "cost-patch": { key: "costPatch", set: "catalog", shape: "object" },
380
426
  "cost-fallback": { key: "costFallback", set: "catalog", shape: "object" },
381
427
  /**
@@ -388,6 +434,26 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
388
434
  shape: "scalar",
389
435
  values: [true, false],
390
436
  },
437
+ /**
438
+ * The host bills cache writes at this multiple of the row's live `input`
439
+ * price, for hosts whose discovery reports cache-write tokens but no
440
+ * cache-write rate. It replaces any `cacheWrite` the row carries, including
441
+ * a `cost-patch`/`cost-fallback` value and an absolute long-context tier's
442
+ * `cacheWrite` (rebased on the tier's `input`).
443
+ */
444
+ "cache-write-rate": { key: "cacheWriteRate", set: "catalog", shape: "scalar" },
445
+ /**
446
+ * How the model line bills a replayed signed thinking block as input:
447
+ * `output` bills the reasoning its signature carries, at the size the turn
448
+ * billed as output, and not the signature's bytes. Unassigned: estimated
449
+ * from the signature's bytes. Read through `./thinking-replay`.
450
+ */
451
+ "thinking-replay-tokens": {
452
+ key: "thinkingReplayTokens",
453
+ set: "catalog",
454
+ shape: "scalar",
455
+ values: ["output", "signature"],
456
+ },
391
457
  "delegation-bias": { key: "delegationBias", set: "catalog", shape: "scalar", values: DELEGATION_BIASES },
392
458
  "discovery-api": { key: "discoveryApi", set: "catalog", shape: "scalar" },
393
459
  "edit-prompt-variant": { key: "editPromptVariant", set: "catalog", shape: "scalar", values: ["full", "compact"] },
@@ -404,16 +470,74 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
404
470
  "hosted-image": { key: "hostedImage", set: "catalog", shape: "scalar", values: [true, false] },
405
471
  /** How the model line bills an input image; shape and formulas in `./image-tokenization`. */
406
472
  "image-tokenization": { key: "imageTokenization", set: "catalog", shape: "object" },
473
+ /**
474
+ * Most images one request may carry, documents included where the host
475
+ * counts them. Unassigned: unknown host, held to a conservative floor.
476
+ */
477
+ "image-count-budget": { key: "imageCountBudget", set: "catalog", shape: "scalar" },
478
+ /**
479
+ * Replaces `image-count-budget` while the request reaches the official
480
+ * endpoint (pi-ai `reachesOfficialEndpoint`): a limit measured or documented
481
+ * for the host's own endpoint, which a custom `baseUrl` or an
482
+ * `ANTHROPIC_BASE_URL`/`FOUNDRY_BASE_URL` reroute (proxy, gateway) need not share.
483
+ */
484
+ "official-image-count-budget": { key: "officialImageCountBudget", set: "catalog", shape: "scalar" },
485
+ /**
486
+ * Opt-in: the reader may get a fresh `read` result as snapcompact frames
487
+ * (`snapcompact.toolResults`), because it edits as exactly from frames as
488
+ * from text. Unassigned: `read` results stay text, since a reader that
489
+ * misreads indentation in frames makes inexact edits.
490
+ */
491
+ "snapcompact-image-reads": { key: "snapcompactImageReads", set: "catalog", shape: "scalar", values: [true, false] },
407
492
  "image-model": { key: "imageModel", set: "catalog", shape: "scalar" },
493
+ /** Longest image edge the host passes to the model: it downscales larger images to fit first. */
494
+ "image-max-edge": { key: "imageMaxEdge", set: "catalog", shape: "scalar" },
408
495
  "inline-image-byte-budget": { key: "inlineImageByteBudget", set: "catalog", shape: "scalar" },
496
+ /**
497
+ * Supported formats and size bounds for judgment-request images; `mime-types`
498
+ * is comma-separated because object payloads have no array shape.
499
+ */
500
+ "judgment-image-input": { key: "judgmentImageInput", set: "catalog", shape: "object" },
501
+ /**
502
+ * `Content-Encoding`s the host decodes on request bodies, most preferred
503
+ * first. Applied only while the request reaches the provider's own endpoint
504
+ * (see `endpoint-hosts`), so a proxy or rerouted endpoint never receives
505
+ * bytes it cannot read. Unassigned: bodies are sent uncompressed.
506
+ */
507
+ "request-encodings": { key: "requestEncodings", set: "catalog", shape: "array", values: ["zstd", "gzip"] },
508
+ /**
509
+ * HTTPS hostnames the provider itself serves besides its bundled base URL's
510
+ * host — per-account endpoints such as Copilot's plan hosts. Together with
511
+ * that host they define the provider's own endpoint, which tells first-party
512
+ * traffic from a custom gateway that reuses the provider id.
513
+ */
514
+ "endpoint-hosts": { key: "endpointHosts", set: "catalog", shape: "array" },
515
+ /**
516
+ * Pattern of the JSON 400/415 error body the host returns when it cannot
517
+ * decode a compressed request body. Unassigned: every JSON 400/415 is a
518
+ * possible rejection and is re-checked with an uncompressed resend. Either
519
+ * way, a 400/403/415 whose body is not JSON (presumed a proxy's answer, not
520
+ * the host's) is re-checked too.
521
+ */
522
+ "request-encoding-rejection": { key: "requestEncodingRejection", set: "catalog", shape: "scalar" },
409
523
  /**
410
524
  * Most questions one System One judgment request may carry; the judge
411
525
  * splits larger requests. Unassigned: the endpoint takes any batch size.
412
526
  */
413
527
  "judgment-questions-max": { key: "judgmentQuestionsMax", set: "catalog", shape: "scalar" },
414
528
  "limits-patch": { key: "limitsPatch", set: "catalog", shape: "object" },
529
+ /** Seeds `maxTokens` only when the row reports none; applied after `limits-patch`. */
530
+ "max-tokens-fallback": { key: "maxTokensFallback", set: "catalog", shape: "scalar" },
531
+ /** Most output tokens the host serves: a larger reported `maxTokens` is lowered to it; an unknown one stays unknown. */
532
+ "max-tokens-ceiling": { key: "maxTokensCeiling", set: "catalog", shape: "scalar" },
415
533
  "long-context-cost": { key: "longContext", set: "catalog", shape: "object" },
416
534
  "prompt-cache": { key: "promptCache", set: "catalog", shape: "object" },
535
+ "task-sibling-prompt-cache": {
536
+ key: "taskSiblingPromptCache",
537
+ set: "catalog",
538
+ shape: "scalar",
539
+ values: [true, false],
540
+ },
417
541
  /**
418
542
  * Prompt-cache lookback in block positions: how far back from a cache
419
543
  * breakpoint the provider looks for an earlier request's cache entry.
@@ -532,6 +656,7 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
532
656
 
533
657
  /** Records applicable to each API family; used by `resolve.ts` when applying wire axes. */
534
658
  export const API_COMPAT_RECORDS: Readonly<Record<string, readonly CompatRecordName[]>> = {
659
+ "ollama-chat": ["ollama"],
535
660
  "openai-completions": ["openai"],
536
661
  openrouter: ["openai", "openai-responses"],
537
662
  "openai-responses": ["openai-responses"],
@@ -78,3 +78,8 @@ export declare function pricingPeerFor(provider: string, model: string): {
78
78
  peers: readonly string[];
79
79
  peerId: string;
80
80
  } | undefined;
81
+ /**
82
+ * Built-in default model selector chains per agent role (`smol`, `slow`,
83
+ * `image`, …), most preferred first, in declaration order.
84
+ */
85
+ export declare function roleDefaultModels(): Readonly<Record<string, readonly string[]>>;
@@ -3,7 +3,7 @@
3
3
  * (`rules/runtime/behavior.kdl`): provider/model heuristics that run before
4
4
  * or outside exact bundled-model lookup — responses routing, API routing,
5
5
  * quota tiers, plan requirements, model limits, roster and discovery-mode
6
- * exclusions, hosted defaults, and pricing peers.
6
+ * exclusions, hosted defaults, pricing peers, and per-role default models.
7
7
  */
8
8
  import { globMatch } from "./cascade";
9
9
  import rules from "./rules.json";
@@ -225,3 +225,11 @@ export function pricingPeerFor(
225
225
  const alias = rule.aliases.find(candidate => candidate.model === model);
226
226
  return { peers: rule.peers, peerId: alias?.peerId ?? model };
227
227
  }
228
+
229
+ /**
230
+ * Built-in default model selector chains per agent role (`smol`, `slow`,
231
+ * `image`, …), most preferred first, in declaration order.
232
+ */
233
+ export function roleDefaultModels(): Readonly<Record<string, readonly string[]>> {
234
+ return behavior.roleDefaults;
235
+ }
@@ -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),