@oh-my-pi/pi-catalog 18.8.4 → 18.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.8.6] - 2026-10-08
6
+
7
+ ### Added
8
+
9
+ - Added prompt-cache lookback support for Claude models across all hosts, including the public `prompt-cache-lookback` catalog axis and `resolvePromptCacheLookback` API.
10
+
11
+ ### Fixed
12
+
13
+ - Fixed model discovery when providers publish models before they are recognized by the catalog; unsupported models are now skipped with a warning so other available models remain discoverable.
14
+
15
+ ## [18.8.5] - 2026-10-08
16
+
17
+ ### Breaking Changes
18
+
19
+ - `googleAntigravityModelManagerOptions` takes `resolveAccounts` instead of `oauthToken`, and `fetchAntigravityDiscoveryModels` returns a roster or credential-rejection result instead of a bare list ([#14924](https://github.com/can1357/oh-my-pi/issues/14924)).
20
+
21
+ ### Fixed
22
+
23
+ - Fixed Claude Haiku 5.5 on Cursor showing as unpriced; it now uses Cursor's $0.10/$0.50 rate card and 5x long-context tier above 100K input tokens ([#14890](https://github.com/can1357/oh-my-pi/pull/14890) by [@eggpeat](https://github.com/eggpeat)).
24
+ - Fixed Claude Haiku 5.5 on GitHub Copilot pricing cache reads and writes at $0 on both the standard and `-1m` long-context rows, and the `-1m` row billing the 5x long-context band a second time above 100K input tokens ([#14890](https://github.com/can1357/oh-my-pi/pull/14890) by [@eggpeat](https://github.com/eggpeat)).
25
+
5
26
  ## [18.8.4] - 2026-10-08
6
27
 
7
28
  ### Fixed
@@ -0,0 +1,7 @@
1
+ import type { Model } from "../types.js";
2
+ /**
3
+ * Block positions the provider's prompt cache checks back from a breakpoint for
4
+ * an earlier request's entry (`prompt-cache-lookback` axis), or `undefined` when
5
+ * the model has no known lookback bound.
6
+ */
7
+ export declare function resolvePromptCacheLookback(model: Model): number | undefined;
@@ -63,13 +63,23 @@ export interface FetchAntigravityDiscoveryModelsOptions {
63
63
  */
64
64
  collapseTable?: VariantCollapseTable;
65
65
  }
66
+ /**
67
+ * A complete account roster, or a definitive 401/403 credential rejection.
68
+ * `null` from {@link fetchAntigravityDiscoveryModels} instead means a transient
69
+ * failure or invalid response, which must not replace an authoritative catalog.
70
+ */
71
+ export interface AntigravityModelDiscoveryResult {
72
+ models: ModelSpec<"google-gemini-cli">[];
73
+ rejectedStatus?: 401 | 403;
74
+ }
66
75
  /**
67
76
  * Fetches discoverable Antigravity models and normalizes them into canonical model entries.
68
77
  *
69
- * Returns `null` on network/payload/auth failures.
70
- * Returns `[]` only when the endpoint responds successfully with no usable models.
78
+ * Returns `null` on network, server, or payload failures; a 401/403 is reported
79
+ * separately so multi-account discovery can skip a rejected credential.
80
+ * A successful empty roster has `models: []` without `rejectedStatus`.
71
81
  */
72
- export declare function fetchAntigravityDiscoveryModels(options: FetchAntigravityDiscoveryModelsOptions): Promise<ModelSpec<"google-gemini-cli">[] | null>;
82
+ export declare function fetchAntigravityDiscoveryModels(options: FetchAntigravityDiscoveryModelsOptions): Promise<AntigravityModelDiscoveryResult | null>;
73
83
  /** Advertised image model and serving endpoint for one Antigravity account. */
74
84
  export interface AntigravityImageModel {
75
85
  id: string;
@@ -0,0 +1,11 @@
1
+ import type { Api, ModelSpec } from "../types.js";
2
+ /**
3
+ * Merge per-account discovery catalogs into one list, deduped by model id.
4
+ *
5
+ * The first catalog to list an id supplies its spec; {@link ModelSpec.accountAccess}
6
+ * is merged from every catalog listing that id, so credential selection can
7
+ * route account-gated models to the accounts that serve them. Used by
8
+ * multi-account discovery for Codex and Antigravity, whose rosters differ per
9
+ * account.
10
+ */
11
+ export declare function unionAccountCatalogs<TApi extends Api>(catalogs: readonly (readonly ModelSpec<TApi>[])[]): ModelSpec<TApi>[];
@@ -11,8 +11,30 @@ export interface GoogleVertexModelManagerConfig {
11
11
  signal?: AbortSignal;
12
12
  fetch?: FetchImpl;
13
13
  }
14
+ /** One Antigravity OAuth account whose `fetchAvailableModels` roster discovery reads. */
15
+ export interface GoogleAntigravityAccount {
16
+ /** OAuth access token used for `Authorization: Bearer ...`. */
17
+ accessToken: string;
18
+ /**
19
+ * Credential identity recorded in {@link ModelSpec.accountAccess} for every
20
+ * model this account serves (see `oauthAccountKey` in `@oh-my-pi/pi-ai`).
21
+ * When any account lacks one, no model records per-account access.
22
+ */
23
+ accountKey?: string;
24
+ }
14
25
  export interface GoogleAntigravityModelManagerConfig {
15
- oauthToken?: string;
26
+ /**
27
+ * Resolves every configured Antigravity account at discovery time. Rosters
28
+ * are plan-scoped (Claude 5.5 is served to some accounts only) and
29
+ * authoritative, so each account's roster is fetched and the results are
30
+ * unioned; reading one account's roster would prune models only its
31
+ * siblings serve (#14924).
32
+ *
33
+ * Returns `null` to abort discovery (e.g. an account's credential failed to
34
+ * refresh), keeping the previous/bundled catalog instead of caching a
35
+ * partial one.
36
+ */
37
+ resolveAccounts?: () => Promise<readonly GoogleAntigravityAccount[] | null>;
16
38
  endpoint?: string;
17
39
  fetch?: FetchImpl;
18
40
  }
@@ -1209,11 +1209,11 @@ export interface Model<TApi extends Api = Api> {
1209
1209
  cursorMaxModeRoutes?: Readonly<Record<string, boolean>>;
1210
1210
  /**
1211
1211
  * Per-account availability recorded by multi-account discovery: provider
1212
- * account id (Codex: ChatGPT `chatgpt_account_id`) → that account's
1213
- * entitlements on this model. An account appears only when its own catalog
1214
- * lists the model, so credential selection can route account-gated models
1215
- * (e.g. `gpt-daybreak-blue-latest`) straight to eligible accounts. Absent on
1216
- * bundled/config rows and on single-account discovery.
1212
+ * account key (Codex: ChatGPT `chatgpt_account_id`; Antigravity: login
1213
+ * email) → that account's entitlements on this model. An account appears
1214
+ * only when its own catalog lists the model, so credential selection can
1215
+ * route account-gated models (e.g. `gpt-daybreak-blue-latest`, Antigravity
1216
+ * Claude 5.5) straight to eligible accounts. Absent on bundled/config rows.
1217
1217
  */
1218
1218
  accountAccess?: Readonly<Record<string, ModelAccountAccess>>;
1219
1219
  /** Cursor `RequestedModel.parameters` for this model's default variant. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-catalog",
3
- "version": "18.8.4",
3
+ "version": "18.8.6",
4
4
  "description": "Model catalog for omp: bundled model database, provider discovery descriptors, model identity, classification, and equivalence",
5
5
  "keywords": [
6
6
  "ai",
@@ -93,12 +93,12 @@
93
93
  "gen:proto": "bun scripts/generate-protocols.ts"
94
94
  },
95
95
  "dependencies": {
96
- "@oh-my-pi/omptype": "18.8.4",
97
- "@oh-my-pi/pi-utils": "18.8.4"
96
+ "@oh-my-pi/omptype": "18.8.6",
97
+ "@oh-my-pi/pi-utils": "18.8.6"
98
98
  },
99
99
  "devDependencies": {
100
100
  "@bgotink/kdl": "0.4.0",
101
- "@oh-my-pi/pi-ai": "18.8.4",
101
+ "@oh-my-pi/pi-ai": "18.8.6",
102
102
  "@types/bun": "^1.3.14"
103
103
  },
104
104
  "engines": {
package/src/build.ts CHANGED
@@ -176,40 +176,6 @@ export function applyCatalogCorrections(
176
176
  model: Pick<ModelSpec<Api>, "cost" | "contextWindow" | "maxTokens" | "input">,
177
177
  catalog: Record<string, unknown>,
178
178
  ): void {
179
- const longContext = objectPayload(catalog.longContext);
180
- if (longContext !== undefined) {
181
- const inputThreshold = numberField(longContext, "inputThreshold");
182
- const inclusive = Reflect.get(longContext, "inputThresholdInclusive") === true;
183
- const multiplier = numberField(longContext, "multiplier");
184
- const input = numberField(longContext, "input");
185
- const output = numberField(longContext, "output");
186
- const cacheRead = numberField(longContext, "cacheRead");
187
- const cacheWrite = numberField(longContext, "cacheWrite");
188
- const base = model.cost;
189
- const hasTokenPrice = base.input !== 0 || base.output !== 0 || base.cacheRead !== 0 || base.cacheWrite !== 0;
190
- if (inputThreshold !== undefined && multiplier !== undefined && hasTokenPrice) {
191
- // Multiplier form: tier rates derive from the row's live list price.
192
- model.cost = {
193
- ...base,
194
- longContext: {
195
- inputThreshold,
196
- ...(inclusive && { inputThresholdInclusive: true }),
197
- input: base.input * multiplier,
198
- output: base.output * multiplier,
199
- cacheRead: base.cacheRead * multiplier,
200
- cacheWrite: base.cacheWrite * multiplier,
201
- },
202
- };
203
- } else if (
204
- inputThreshold !== undefined &&
205
- input !== undefined &&
206
- output !== undefined &&
207
- cacheRead !== undefined &&
208
- cacheWrite !== undefined
209
- ) {
210
- model.cost = { ...model.cost, longContext: { inputThreshold, input, output, cacheRead, cacheWrite } };
211
- }
212
- }
213
179
  const patch = objectPayload(catalog.costPatch);
214
180
  if (patch !== undefined) {
215
181
  model.cost = { ...model.cost };
@@ -246,6 +212,41 @@ export function applyCatalogCorrections(
246
212
  applyEffectiveFallbackRates(model.cost, Reflect.get(fallback, "effectiveRates"));
247
213
  }
248
214
  }
215
+ const longContext = objectPayload(catalog.longContext);
216
+ if (longContext !== undefined) {
217
+ const inputThreshold = numberField(longContext, "inputThreshold");
218
+ const inclusive = Reflect.get(longContext, "inputThresholdInclusive") === true;
219
+ const multiplier = numberField(longContext, "multiplier");
220
+ const input = numberField(longContext, "input");
221
+ const output = numberField(longContext, "output");
222
+ const cacheRead = numberField(longContext, "cacheRead");
223
+ 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;
226
+ if (inputThreshold !== undefined && multiplier !== undefined && hasTokenPrice) {
227
+ // Multiplier form: tier rates derive from the row's final base card,
228
+ // after `cost-patch`/`cost-fallback` corrected the live list price.
229
+ model.cost = {
230
+ ...base,
231
+ longContext: {
232
+ inputThreshold,
233
+ ...(inclusive && { inputThresholdInclusive: true }),
234
+ input: base.input * multiplier,
235
+ output: base.output * multiplier,
236
+ cacheRead: base.cacheRead * multiplier,
237
+ cacheWrite: base.cacheWrite * multiplier,
238
+ },
239
+ };
240
+ } else if (
241
+ inputThreshold !== undefined &&
242
+ input !== undefined &&
243
+ output !== undefined &&
244
+ cacheRead !== undefined &&
245
+ cacheWrite !== undefined
246
+ ) {
247
+ model.cost = { ...model.cost, longContext: { inputThreshold, input, output, cacheRead, cacheWrite } };
248
+ }
249
+ }
249
250
  if (catalog.cacheReadAtInputRate === true) {
250
251
  const { longContext } = model.cost;
251
252
  model.cost = {
@@ -399,6 +399,12 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
399
399
  "limits-patch": { key: "limitsPatch", set: "catalog", shape: "object" },
400
400
  "long-context-cost": { key: "longContext", set: "catalog", shape: "object" },
401
401
  "prompt-cache": { key: "promptCache", set: "catalog", shape: "object" },
402
+ /**
403
+ * Prompt-cache lookback in block positions: how far back from a cache
404
+ * breakpoint the provider looks for an earlier request's cache entry.
405
+ * Unassigned: no known lookback bound.
406
+ */
407
+ "prompt-cache-lookback": { key: "promptCacheLookback", set: "catalog", shape: "scalar" },
402
408
  "long-usage-limit-fallback": { key: "longUsageLimitFallback", set: "catalog", shape: "scalar" },
403
409
  "max-context-window": { key: "maxContextWindow", set: "catalog", shape: "scalar" },
404
410
  "pricing-status": {
@@ -0,0 +1,23 @@
1
+ import { toModelSpec } from "../provider-models/bundled-references";
2
+ import type { Model } from "../types";
3
+ import { resolveCatalogAxes } from "./resolve";
4
+
5
+ const lookbackCache = new Map<string, number | null>();
6
+ const LOOKBACK_CACHE_MAX = 8192;
7
+
8
+ /**
9
+ * Block positions the provider's prompt cache checks back from a breakpoint for
10
+ * an earlier request's entry (`prompt-cache-lookback` axis), or `undefined` when
11
+ * the model has no known lookback bound.
12
+ */
13
+ export function resolvePromptCacheLookback(model: Model): number | undefined {
14
+ const key = `${model.provider} ${model.id} ${model.api}`;
15
+ let lookback = lookbackCache.get(key);
16
+ if (lookback === undefined) {
17
+ const value = resolveCatalogAxes(toModelSpec(model)).promptCacheLookback;
18
+ lookback = typeof value === "number" && Number.isInteger(value) && value > 0 ? value : null;
19
+ if (lookbackCache.size >= LOOKBACK_CACHE_MAX) lookbackCache.clear();
20
+ lookbackCache.set(key, lookback);
21
+ }
22
+ return lookback ?? undefined;
23
+ }
@@ -213,12 +213,14 @@ The three value shapes are:
213
213
  - **Object**: no arguments and a child block, including an empty block. Child names are kebab-case: an axis-directive spelling compiles to its resolved axis key (`template-reasoning-effort` → `qwenTemplateReasoningEffort`), anything else converts mechanically (`input-threshold` → `inputThreshold`); camelCase names are a compile error. `extra-body` payloads (top-level or nested) are the exception — their child names are literal wire JSON keys copied verbatim (`enable_thinking`). Each child is either one scalar or another object; arrays are not representable inside an object payload.
214
214
 
215
215
  A rule cannot assign the same resolved axis twice in one block.
216
- One object axis carries a computed form: `long-context-cost` accepts either the absolute rates (`input-threshold` + `input`/`output`/`cache-read`/`cache-write`) or `input-threshold` + `multiplier` (with optional `input-threshold-inclusive`), which derives the tier from the row's live base price at build time so the rule tracks upstream list-price updates (xAI's SuperGrok 200K tier). Rows without a token price carry no tier.
216
+ One object axis carries a computed form: `long-context-cost` accepts either the absolute rates (`input-threshold` + `input`/`output`/`cache-read`/`cache-write`) or `input-threshold` + `multiplier` (with optional `input-threshold-inclusive`), which derives the tier from the row's base price at build time, after `cost-patch` and `cost-fallback`, so the rule tracks upstream list-price updates (xAI's SuperGrok 200K tier). Rows without a token price carry no tier. An empty `long-context-cost {}` adds no tier; a more specific rule uses it to opt a host out of a broader rule's band (GitHub Copilot's Haiku 5.5 rows, whose long tier is the `-1m` sibling).
217
217
 
218
218
  `context-window-authoritative #true` preserves a host's supplied context window through runtime model selection instead of applying inferred expansion or reference-price-tier caps. Explicit user context overrides still apply afterward. It applies to rows materialized through `buildModel` (discovery and regenerated bundles). See [`providers/factory-droid.kdl`](providers/factory-droid.kdl).
219
219
 
220
220
  `inline-image-byte-budget` caps retained base64 image characters for a deployment's request body. It leaves room for tool schemas and text beneath the provider's body-size limit; the coding agent reads it through `resolveInlineImageByteBudget` before sending live history. The budget applies only while `compat.officialEndpoint` holds: a custom `baseUrl` imposes its own body limit.
221
221
 
222
+ `prompt-cache-lookback` is how many block positions back from a cache breakpoint the model's prompt cache looks for an earlier request's entry (Claude: 20, counting the breakpoint; a run of consecutive `tool_use` or `tool_result` blocks is one position). The coding agent reads it through `resolvePromptCacheLookback` and keeps warm-cache tool-result pruning inside the window; unassigned means no known bound.
223
+
222
224
  The routed-subscription registry axes (`upstream-rotation`, `region-upstreams-global|us|eu`, `region-limits-eu`, `credit-rates`, `list-price-from`, `routing-family`, `policy-aliases`, `entitlement`, `default-reasoning-off`) describe a gateway whose proxy fans one model out to several upstreams. They are read through `src/compat/factory-droid.ts` by Factory Droid discovery and its request provider, not materialized by `buildModel`. A provider-wide `region-upstreams-*` rule is the upstream serving table and a model rule replaces it for that region. `list-price-from "<provider>" ["<id>"]` shows a bundled row's list price beside the subscription's own billing; unlike seed values it is resolved at runtime and degrades to the seed's zero cost when the row is gone. See [`providers/factory-droid.kdl`](providers/factory-droid.kdl), whose wire and billing pool per model are `api-routes` and `quota-tiers` rules in `runtime/behavior.kdl`.
223
225
 
224
226
  ### Time-based pricing
@@ -9,7 +9,13 @@
9
9
  // are never undercounted; only Opus/Sonnet/Haiku revisions known to predate
10
10
  // 4.7 take the standard tier. Separator-collapsed revisions parse as two
11
11
  // digits (`claude-opus-45` is Opus 4.5 → 45), so 10–46 is standard too.
12
+ // Claude's prompt cache finds an earlier request's entry only within 20 block
13
+ // positions back from a breakpoint, counting the breakpoint; a run of
14
+ // consecutive tool_use or tool_result blocks is one position. Warm-cache
15
+ // tool-result pruning keeps its rewrites inside this window.
16
+ // https://platform.claude.com/docs/en/build-with-claude/prompt-caching
12
17
  class "anthropic" {
18
+ prompt-cache-lookback 20
13
19
  image-tokenization {
14
20
  regime "anthropic-patch"
15
21
  max-edge 2576
@@ -102,7 +108,8 @@ class "anthropic" {
102
108
  }
103
109
  // Haiku 5.5 bills every rate 5x for prompts over 100K. Derived from each
104
110
  // host's list price so non-Anthropic hosts get the band too; the Claude
105
- // API pins absolute rates further down.
111
+ // API and Cursor pin absolute rates, and GitHub Copilot opts out because
112
+ // its long tier is the `-1m` sibling row.
106
113
  // https://platform.claude.com/docs/en/models/haiku-5-5/overview#pricing
107
114
  revision ">=5.5 <6" {
108
115
  long-context-cost {
@@ -167,6 +167,24 @@ provider "cursor" {
167
167
  cache-write 12.5
168
168
  }
169
169
  }
170
+ // Claude Haiku 5.5: $0.10/$0.50, cache $0.01/$0.125. Requests above
171
+ // 100k input tokens bill every leg at 5x ($0.50/$2.50, cache
172
+ // $0.05/$0.625), on the 1M lane too.
173
+ models "claude-haiku-5-5" "claude-haiku-5-5-1m" {
174
+ cost-patch {
175
+ input 0.1
176
+ output 0.5
177
+ cache-read 0.01
178
+ cache-write 0.125
179
+ }
180
+ long-context-cost {
181
+ input-threshold 100000
182
+ input 0.5
183
+ output 2.5
184
+ cache-read 0.05
185
+ cache-write 0.625
186
+ }
187
+ }
170
188
  // Claude 4.7 Opus: $5/$25, cache $0.50/$6.25. Up to 1M same rates.
171
189
  models "claude-opus-4-7-high" "claude-opus-4-7-low" "claude-opus-4-7-max" "claude-opus-4-7-medium" "claude-opus-4-7-xhigh" {
172
190
  cost-patch {
@@ -76,4 +76,29 @@ provider "github-copilot" {
76
76
  }
77
77
  max-context-window 1050000
78
78
  }
79
+ // Claude Haiku 5.5: `billing.token_prices` reports its input/output rates
80
+ // but no cache legs, so discovered rows priced cache reads and writes at
81
+ // $0. Copilot's rate card bills $0.01 read / $0.125 write up to 100K and
82
+ // $0.05 / $0.625 on the long tier (the `-1m` sibling).
83
+ // https://docs.github.com/en/copilot/reference/copilot-billing/models-and-pricing
84
+ // Copilot serves that tier as the `-1m` row (promotion target of the base
85
+ // row), so both rows opt out of the class-wide 100K 5x band: the base row
86
+ // would otherwise bill the band on top of the promotion, and the `-1m`
87
+ // row would charge 5x twice.
88
+ models "claude-haiku-5.5" {
89
+ cost-patch {
90
+ cache-read 0.01
91
+ cache-write 0.125
92
+ }
93
+ long-context-cost {
94
+ }
95
+ }
96
+ models "claude-haiku-5.5-1m" {
97
+ cost-patch {
98
+ cache-read 0.05
99
+ cache-write 0.625
100
+ }
101
+ long-context-cost {
102
+ }
103
+ }
79
104
  }