@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.
- package/CHANGELOG.md +61 -0
- package/README.md +4 -2
- package/dist/types/build.d.ts +3 -2
- package/dist/types/compat/image-tokenization.d.ts +9 -1
- package/dist/types/compat/request-size.d.ts +11 -3
- package/dist/types/compat/thinking-replay.d.ts +9 -0
- package/dist/types/model-thinking.d.ts +11 -1
- package/dist/types/models.d.ts +16 -1
- package/dist/types/pricing.d.ts +37 -1
- package/dist/types/types.d.ts +29 -7
- package/dist/types/wire/codex.d.ts +2 -0
- package/package.json +4 -4
- package/src/build.ts +141 -80
- package/src/compat/axes.ts +64 -2
- package/src/compat/cascade.ts +4 -1
- package/src/compat/image-tokenization.ts +28 -10
- package/src/compat/request-size.ts +18 -5
- package/src/compat/resolve.ts +46 -7
- package/src/compat/rules/README.md +15 -3
- package/src/compat/rules/classes/anthropic.kdl +43 -2
- package/src/compat/rules/classes/gemini.kdl +28 -0
- package/src/compat/rules/classes/meta.kdl +7 -0
- package/src/compat/rules/classes/openai.kdl +26 -3
- package/src/compat/rules/classes/xai.kdl +26 -0
- package/src/compat/rules/providers/amazon-bedrock.kdl +1 -0
- package/src/compat/rules/providers/anthropic.kdl +13 -0
- package/src/compat/rules/providers/cursor.kdl +50 -1
- package/src/compat/rules/providers/devin.kdl +110 -5
- package/src/compat/rules/providers/google-antigravity.kdl +26 -0
- package/src/compat/rules/providers/google-gemini-cli.kdl +1 -0
- package/src/compat/rules/providers/google-vertex.kdl +1 -0
- package/src/compat/rules/providers/google.kdl +1 -0
- package/src/compat/rules/providers/image-tokenization.kdl +2 -1
- package/src/compat/rules/providers/meta.kdl +16 -0
- package/src/compat/rules/providers/muse-code.kdl +49 -0
- package/src/compat/rules/providers/openai-codex.kdl +8 -0
- package/src/compat/rules/providers/openai.kdl +1 -0
- package/src/compat/rules/providers/openrouter.kdl +1 -0
- package/src/compat/rules/providers/umans.kdl +1 -0
- package/src/compat/rules/providers/xai-oauth.kdl +3 -0
- package/src/compat/rules/providers/xai.kdl +3 -0
- package/src/compat/rules/runtime/behavior.kdl +3 -3
- package/src/compat/rules/taxonomy/_collapse.kdl +11 -0
- package/src/compat/rules/taxonomy/meta.kdl +3 -0
- package/src/compat/rules.json +1 -1
- package/src/compat/thinking-replay.ts +33 -0
- package/src/discovery/codex.ts +0 -4
- package/src/discovery/cursor.ts +25 -11
- package/src/discovery/devin.ts +5 -3
- package/src/model-thinking.ts +13 -1
- package/src/model-tokenizer.ts +7 -1
- package/src/models.json +1 -1
- package/src/models.ts +26 -12
- package/src/pricing.ts +123 -12
- package/src/provider-models/bundled-references.ts +5 -2
- package/src/provider-models/openai-compat.ts +69 -4
- package/src/types.ts +31 -6
- 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 {
|
|
14
|
-
|
|
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
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
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
|
-
|
|
176
|
-
|
|
151
|
+
function correctedCost(
|
|
152
|
+
upstream: ModelCost,
|
|
177
153
|
catalog: Record<string, unknown>,
|
|
178
|
-
|
|
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)
|
|
174
|
+
if (input !== undefined) cost.input = input;
|
|
184
175
|
const output = numberField(patch, "output");
|
|
185
|
-
if (output !== undefined)
|
|
176
|
+
if (output !== undefined) cost.output = output;
|
|
186
177
|
const cacheRead = numberField(patch, "cacheRead");
|
|
187
|
-
if (cacheRead !== undefined)
|
|
178
|
+
if (cacheRead !== undefined) cost.cacheRead = cacheRead;
|
|
188
179
|
const cacheWrite = numberField(patch, "cacheWrite");
|
|
189
|
-
if (cacheWrite !== undefined)
|
|
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
|
|
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)
|
|
191
|
+
if (input !== undefined) cost.input = input;
|
|
202
192
|
const output = numberField(fallback, "output");
|
|
203
|
-
if (output !== undefined)
|
|
193
|
+
if (output !== undefined) cost.output = output;
|
|
204
194
|
const cacheRead = numberField(fallback, "cacheRead");
|
|
205
|
-
if (cacheRead !== undefined)
|
|
195
|
+
if (cacheRead !== undefined) cost.cacheRead = cacheRead;
|
|
206
196
|
const cacheWrite = numberField(fallback, "cacheWrite");
|
|
207
|
-
if (cacheWrite !== undefined)
|
|
208
|
-
|
|
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
|
|
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
|
-
|
|
230
|
-
...
|
|
214
|
+
cost = {
|
|
215
|
+
...cost,
|
|
231
216
|
longContext: {
|
|
232
217
|
inputThreshold,
|
|
233
218
|
...(inclusive && { inputThresholdInclusive: true }),
|
|
234
|
-
input:
|
|
235
|
-
output:
|
|
236
|
-
cacheRead:
|
|
237
|
-
cacheWrite:
|
|
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
|
-
|
|
232
|
+
cost = { ...cost, longContext: { inputThreshold, input, output, cacheRead, cacheWrite } };
|
|
248
233
|
}
|
|
249
234
|
}
|
|
250
235
|
if (catalog.cacheReadAtInputRate === true) {
|
|
251
|
-
const { longContext } =
|
|
252
|
-
|
|
253
|
-
...
|
|
254
|
-
cacheRead:
|
|
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
|
-
|
|
259
|
-
|
|
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
|
|
355
|
-
//
|
|
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,
|
package/src/compat/axes.ts
CHANGED
|
@@ -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.
|
package/src/compat/cascade.ts
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
11
|
-
*
|
|
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
|
-
|
|
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
|
+
}
|
package/src/compat/resolve.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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)
|
|
1160
|
-
|
|
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
|
|
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(
|
|
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);
|