@oh-my-pi/pi-catalog 18.13.3 → 18.14.1

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,41 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.14.1] - 2026-10-31
6
+
7
+ ### Changed
8
+
9
+ - Clamping a requested thinking effort now resolves through the model's `effortMap` alias first, so the request lands on the tier the model serves instead of the nearest lower one ([#17395](https://github.com/can1357/oh-my-pi/pull/17395) by [@Maziar123](https://github.com/Maziar123))
10
+ - Long image-heavy conversations on the official Anthropic API now keep up to 120 MB of inline images before evicting older ones, following the Messages API request limit increase from 32 MB to 128 MB
11
+ - The official Anthropic API now records its 128 MB request limit (`request-body-byte-limit`), so the newest message's images can use the 8 MB reserve and long text cannot push images past the limit ([#17492](https://github.com/can1357/oh-my-pi/pull/17492) by [@will-bogusz](https://github.com/will-bogusz))
12
+
13
+ ### Fixed
14
+
15
+ - Fixed Cursor's DeepSeek V4.1 Flash 1017 reporting zero cost; every effort level now bills at Cursor's API rate of $0.30/$1.20 per million tokens, with cache reads at $0.006 ([#17390](https://github.com/can1357/oh-my-pi/pull/17390) by [@eggpeat](https://github.com/eggpeat))
16
+
17
+ ## [18.14.0] - 2026-10-29
18
+
19
+ ### Breaking Changes
20
+
21
+ - Removed the raw `@oh-my-pi/pi-catalog/models.json` export; use the bundled model registry APIs instead.
22
+
23
+ ### Added
24
+
25
+ - Added `nextBuildPriceChange(cost, now)` to identify when a model's dated pricing card may change, allowing callers to safely reuse earlier builds until that time.
26
+ - Added the `selector-revision-suffix` catalog axis for Google Vertex, so `google-vertex/claude-opus-4-8` resolves exactly to `claude-opus-4-8@default`.
27
+
28
+ ### Changed
29
+
30
+ - Reduced the bundled model catalog size and loading overhead.
31
+ - Updated role defaults so Factory Droid image models are preferred for the `image` role, allowing `generate_image` to work with only a Factory login.
32
+ - Updated `smol` and `slow` role defaults to use the currently bundled Cerebras and OpenAI Codex model identifiers.
33
+
34
+ ### Fixed
35
+
36
+ - Fixed Mistral reasoning models so streamed thinking appears in the transcript and is preserved for subsequent turns.
37
+ - Fixed later-turn HTTP 422 errors in Mistral-hosted DeepSeek conversations when assistant reasoning history is included.
38
+ - Fixed DeepSeek V4.1 Flash image attachments being dropped for publisher-namespaced model IDs such as `ds/deepseek-flash`.
39
+
5
40
  ## [18.13.3] - 2026-10-28
6
41
 
7
42
  ### Added
package/README.md CHANGED
@@ -21,7 +21,7 @@ Import from subpaths (`@oh-my-pi/pi-catalog/<module>`) or the root barrel.
21
21
 
22
22
  ## models.json and rules.json are generated
23
23
 
24
- Never edit `src/models.json` or `src/compat/rules.json` by hand. `models.json` is produced from upstream sources (stencil.so, provider catalog discovery, OpenCode docs) by `scripts/generate-models.ts`; `rules.json` is compiled from the KDL tree in `src/compat/rules/`. Regenerate with:
24
+ Never edit `src/models.json` or `src/compat/rules.json` by hand. `models.json` is a compact generated catalog; use `getBundledProviders()` and `getBundledModels()` instead of reading its internal format. It is produced from upstream sources (stencil.so, provider catalog discovery, OpenCode docs) by `scripts/generate-models.ts`; `rules.json` is compiled from the KDL tree in `src/compat/rules/`. Regenerate with:
25
25
 
26
26
  ```sh
27
27
  bun run gen:compat # src/compat/rules/**/*.kdl -> src/compat/rules.json
@@ -47,7 +47,7 @@ The `models` subpath (also exported from the root) provides timestamp-aware pric
47
47
  | `calculateCost(model, usage, timestamp?)` | Updates and returns `usage.cost` using `model.cost`. |
48
48
  | `calculateUsageCost(cost, usage, timestamp?)` | Updates and returns `usage.cost` using a `ModelCost`. |
49
49
  | `calculateUncachedInputCost(cost, promptInputTokens, timestamp?)` | Returns the cost of a fully uncached prompt. |
50
- | `getEffectiveTokenCost(cost, timestamp?)` | Returns the base card in force: the latest due effective rate, else the row's own legs. |
50
+ | `getEffectiveTokenCost(cost, timestamp?)` | Returns the base card in force: the latest due effective rate, else the row's own legs. |
51
51
  | `getTimeBasedPricingPeriod(cost, timestamp?)` | Returns `"peak"`, `"off-peak"`, or `undefined` without a recurring tariff. |
52
52
  | `hasRecurringTariff(schedule)` | Whether a `TimeBasedCost` carries a peak/off-peak tariff rather than only dated rate cards. |
53
53
  | `getNextTimeBasedPricingTransition(cost, timestamp?)` | Returns the next actual peak/off-peak change strictly after the timestamp, or `undefined` if none exists. |
@@ -6,7 +6,7 @@
6
6
  * Request handlers read fields — they never detect, parse ids, or allocate
7
7
  * compat per request.
8
8
  */
9
- import { type Api, type Model, type ModelSpec } from "./types.js";
9
+ import { type Api, type Model, type ModelCost, type ModelSpec } from "./types.js";
10
10
  /**
11
11
  * Applies reviewed catalog-data value corrections (`cost-patch`,
12
12
  * `cache-read-at-input-rate`, `cache-write-rate`, `limits-patch`,
@@ -24,8 +24,16 @@ export declare function applyCatalogCorrections(model: Pick<ModelSpec<Api>, "cos
24
24
  */
25
25
  export declare function buildDiscoveredModel(spec: ModelSpec<Api>, providerType: string): Model<Api>;
26
26
  /**
27
- * Build one model from an authored spec. Bundled models.json rows are fully
28
- * materialized by the generator and consumed directly (see `models.ts`), so
29
- * this only runs for discovered/custom/override specs.
27
+ * The first instant after `now` at which {@link buildModel} could select a
28
+ * different dated price card for a spec carrying `cost`: the next
29
+ * `effectiveFrom` of the spec's own rate schedule or of any dated card in the
30
+ * compiled rules. A build of an unchanged spec made at or after `now` is
31
+ * identical to a fresh one until then. `Infinity` when no change is pending.
32
+ */
33
+ export declare function nextBuildPriceChange(cost: ModelCost, now: number): number;
34
+ /**
35
+ * Build one model from an authored spec. Bundled models are prebuilt by the
36
+ * generator and decoded by `models.ts`, so this only runs for discovered,
37
+ * custom, and override specs.
30
38
  */
31
39
  export declare function buildModel<TApi extends Api>(spec: ModelSpec<TApi>): Model<TApi>;
@@ -369,8 +369,10 @@ export type CompiledAuthValidation = {
369
369
  kind: "models-endpoint";
370
370
  label?: string;
371
371
  url: string;
372
- /** Env var holding an alternate base URL; `/models` is appended to it. */
372
+ /** Env var holding an alternate base URL; `baseUrlPath` is appended to it. */
373
373
  baseUrlEnv?: string;
374
+ /** Path appended to the `baseUrlEnv` value (default `/models`), for bases that exclude the API version. */
375
+ baseUrlPath?: string;
374
376
  /** Hook returning extra request headers (may throw a configuration error). */
375
377
  headersHook?: string;
376
378
  optional?: boolean;
@@ -2514,6 +2514,9 @@ export interface InteractionUpdate extends ProtoMessage {
2514
2514
  } | {
2515
2515
  case: "stepCompleted";
2516
2516
  value: StepCompletedUpdate;
2517
+ } | {
2518
+ case: "toolRequestsListed";
2519
+ value: ToolRequestsListedUpdate;
2517
2520
  };
2518
2521
  }
2519
2522
  export declare const InteractionUpdateSchema: MessageCodec<InteractionUpdate>;
@@ -5318,6 +5321,11 @@ export interface ToolCallStartedUpdate extends ProtoMessage {
5318
5321
  modelCallId: string;
5319
5322
  }
5320
5323
  export declare const ToolCallStartedUpdateSchema: MessageCodec<ToolCallStartedUpdate>;
5324
+ /** Cursor agent message agent.v1.ToolRequestsListedUpdate. */
5325
+ export interface ToolRequestsListedUpdate extends ProtoMessage {
5326
+ callCount: number;
5327
+ }
5328
+ export declare const ToolRequestsListedUpdateSchema: MessageCodec<ToolRequestsListedUpdate>;
5321
5329
  /** Cursor agent message agent.v1.TruncatedToolCall. */
5322
5330
  export interface TruncatedToolCall extends ProtoMessage {
5323
5331
  originalStepBlobId: Uint8Array;
@@ -48,6 +48,14 @@ export interface CacheEntry<TApi extends Api = Api> {
48
48
  * cache was opened under — Windows cannot remove a database that is still open.
49
49
  */
50
50
  export declare function closeModelCache(): void;
51
+ /**
52
+ * Keeps one connection to a non-default cache path open until the returned
53
+ * handle is disposed. The default `models.db` already shares a long-lived
54
+ * connection; other paths otherwise open, initialize and close the database on
55
+ * every read and write, which a refresh pass repeats once per provider.
56
+ * Retains nest: the connection closes when the last one is disposed.
57
+ */
58
+ export declare function retainModelCacheConnection(dbPath: string | undefined): Disposable;
51
59
  export declare function readModelCache<TApi extends Api>(providerId: string, ttlMs: number, now: () => number, dbPath?: string): CacheEntry<TApi> | null;
52
60
  /**
53
61
  * Persist a provider snapshot.
@@ -0,0 +1,37 @@
1
+ import type { Api, Model } from "./types.js";
2
+ type EncodedValue = string | number | boolean | null | [number];
3
+ type ObjectNode = [0, EncodedValue[], number, number[]];
4
+ type ArrayNode = [1, EncodedValue[]];
5
+ type Node = ObjectNode | ArrayNode;
6
+ type CompatDelta = [number[], EncodedValue];
7
+ type ModelRow = [string, EncodedValue, CompatDelta?];
8
+ type ProviderRow = [string, EncodedValue, ModelRow[]];
9
+ interface CatalogData {
10
+ version: 1;
11
+ keys: string[];
12
+ booleanShapes: number[][];
13
+ nodes: Node[];
14
+ compat: [string, EncodedValue][];
15
+ providers: ProviderRow[];
16
+ }
17
+ /**
18
+ * Parsed compact catalog. Decoded nested objects and arrays can be shared by
19
+ * multiple models; callers must treat those values as immutable.
20
+ */
21
+ export interface ModelCatalog {
22
+ readonly data: CatalogData;
23
+ readonly values: unknown[];
24
+ readonly decoded: boolean[];
25
+ readonly providers: Map<string, Record<string, Model<Api>>>;
26
+ readonly compatBases: Map<string, EncodedValue>;
27
+ readonly compatValues: Map<string, Map<number, Map<string, Record<string, unknown>>>>;
28
+ }
29
+ /** Encode a model catalog into a deterministic, compact JSON representation. */
30
+ export declare function encodeModelCatalog(models: Record<string, Record<string, Model<Api>>>): string;
31
+ /** Open a parsed catalog without materializing its provider models. */
32
+ export declare function openModelCatalog(value: unknown): ModelCatalog;
33
+ /** List catalog provider keys in deterministic order. */
34
+ export declare function getCatalogProviders(catalog: ModelCatalog): string[];
35
+ /** Materialize and cache one provider, or return undefined if it is absent. */
36
+ export declare function getCatalogProvider(catalog: ModelCatalog, provider: string): Record<string, Model<Api>> | undefined;
37
+ export {};
@@ -32,6 +32,10 @@ export declare function withoutUnverifiedThinking<T extends {
32
32
  /**
33
33
  * Clamps a requested thinking level against explicit model metadata.
34
34
  *
35
+ * An unserved level first follows `thinking.effortMap` when the map sends it to
36
+ * a served tier that reaches the wire unchanged (a gateway answering `medium`
37
+ * as `high`); otherwise it climbs down to the nearest lower tier.
38
+ *
35
39
  * Non-reasoning models always resolve to `undefined`.
36
40
  */
37
41
  export declare function clampThinkingLevelForModel<TApi extends Api>(model: ApiModel<TApi> | undefined, requested: Effort | undefined): Effort | undefined;
@@ -1,10 +1,13 @@
1
- import MODELS from "./models.json";
2
1
  import type { Api, KnownProvider, Model, ModelCost, ModelPricingStatus, TimeBasedCost, TokenCost, Usage } from "./types.js";
3
- export type GeneratedProvider = keyof typeof MODELS;
2
+ /** Provider identifier accepted by the bundled model registry. */
3
+ export type GeneratedProvider = string;
4
4
  /** Whether `provider` has bundled rows in models.json (e.g. a provider id authored in KDL). */
5
5
  export declare function isGeneratedProvider(provider: string): provider is GeneratedProvider;
6
+ /** Look up a bundled model; nested catalog values are shared and must not be mutated. */
6
7
  export declare function getBundledModel<TApi extends Api = Api>(provider: GeneratedProvider, modelId: string): Model<TApi>;
7
- export declare function getBundledProviders(): KnownProvider[];
8
+ /** List providers with bundled rows; the array is shared and frozen. */
9
+ export declare function getBundledProviders(): readonly KnownProvider[];
10
+ /** List a provider's bundled models; nested catalog values are shared and must not be mutated. */
8
11
  export declare function getBundledModels(provider: GeneratedProvider): Model<Api>[];
9
12
  /** Resolve display semantics without mistaking an absent rate card for a free model. */
10
13
  export declare function getModelPricingStatus(model: Pick<Model<Api>, "cost" | "pricingStatus">): ModelPricingStatus;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-catalog",
3
- "version": "18.13.3",
3
+ "version": "18.14.1",
4
4
  "description": "Model catalog for omp: bundled model database, provider discovery descriptors, model identity, classification, and equivalence",
5
5
  "keywords": [
6
6
  "ai",
@@ -28,7 +28,6 @@
28
28
  "README.md",
29
29
  "CHANGELOG.md",
30
30
  "LICENSE",
31
- "THIRD-PARTY-NOTICES.txt",
32
31
  "dist/types"
33
32
  ],
34
33
  "type": "module",
@@ -39,10 +38,6 @@
39
38
  "types": "./dist/types/index.d.ts",
40
39
  "import": "./src/index.ts"
41
40
  },
42
- "./models.json": {
43
- "types": "./dist/types/models.json.d.d.ts",
44
- "import": "./src/models.json"
45
- },
46
41
  "./provider-models": {
47
42
  "types": "./dist/types/provider-models/index.d.ts",
48
43
  "import": "./src/provider-models/index.ts"
@@ -93,12 +88,12 @@
93
88
  "gen:proto": "bun scripts/generate-protocols.ts"
94
89
  },
95
90
  "dependencies": {
96
- "@oh-my-pi/omptype": "18.13.3",
97
- "@oh-my-pi/pi-utils": "18.13.3"
91
+ "@oh-my-pi/omptype": "18.14.1",
92
+ "@oh-my-pi/pi-utils": "18.14.1"
98
93
  },
99
94
  "devDependencies": {
100
95
  "@bgotink/kdl": "0.4.0",
101
- "@oh-my-pi/pi-ai": "18.13.3",
96
+ "@oh-my-pi/pi-ai": "18.14.1",
102
97
  "@types/bun": "^1.3.14"
103
98
  },
104
99
  "engines": {
package/src/build.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  * compat per request.
8
8
  */
9
9
 
10
+ import RULES from "./compat/rules.json" with { type: "json" };
10
11
  import { resolveDiscoveryApi, resolveModelPolicy } from "./compat/resolve";
11
12
  import type { ModelIdentity } from "./compat/types";
12
13
  import { resolveModelTokenizer } from "./model-tokenizer";
@@ -463,10 +464,56 @@ export function buildDiscoveredModel(spec: ModelSpec<Api>, providerType: string)
463
464
  return buildModel({ ...spec, api, providerType });
464
465
  }
465
466
 
467
+ /** Every dated card instant in the compiled rules, ascending; collected on first use. */
468
+ let ruleCardInstants: number[] | undefined;
469
+
470
+ function collectRuleCardInstants(value: unknown, out: number[]): void {
471
+ if (Array.isArray(value)) {
472
+ for (const item of value) collectRuleCardInstants(item, out);
473
+ return;
474
+ }
475
+ if (typeof value !== "object" || value === null) return;
476
+ for (const key in value) {
477
+ const child: unknown = Reflect.get(value, key);
478
+ if (key === "effectiveFrom" && typeof child === "string") {
479
+ const instant = Date.parse(child);
480
+ if (Number.isFinite(instant)) out.push(instant);
481
+ } else {
482
+ collectRuleCardInstants(child, out);
483
+ }
484
+ }
485
+ }
486
+
487
+ /**
488
+ * The first instant after `now` at which {@link buildModel} could select a
489
+ * different dated price card for a spec carrying `cost`: the next
490
+ * `effectiveFrom` of the spec's own rate schedule or of any dated card in the
491
+ * compiled rules. A build of an unchanged spec made at or after `now` is
492
+ * identical to a fresh one until then. `Infinity` when no change is pending.
493
+ */
494
+ export function nextBuildPriceChange(cost: ModelCost, now: number): number {
495
+ if (ruleCardInstants === undefined) {
496
+ const instants: number[] = [];
497
+ collectRuleCardInstants(RULES, instants);
498
+ ruleCardInstants = instants.sort((a, b) => a - b);
499
+ }
500
+ let next = Number.POSITIVE_INFINITY;
501
+ for (const instant of ruleCardInstants) {
502
+ if (instant > now) {
503
+ next = instant;
504
+ break;
505
+ }
506
+ }
507
+ for (const rate of cost.timeBased?.effectiveRates ?? []) {
508
+ if (rate.effectiveFrom > now && rate.effectiveFrom < next) next = rate.effectiveFrom;
509
+ }
510
+ return next;
511
+ }
512
+
466
513
  /**
467
- * Build one model from an authored spec. Bundled models.json rows are fully
468
- * materialized by the generator and consumed directly (see `models.ts`), so
469
- * this only runs for discovered/custom/override specs.
514
+ * Build one model from an authored spec. Bundled models are prebuilt by the
515
+ * generator and decoded by `models.ts`, so this only runs for discovered,
516
+ * custom, and override specs.
470
517
  */
471
518
  export function buildModel<TApi extends Api>(spec: ModelSpec<TApi>): Model<TApi> {
472
519
  const policy = resolveModelPolicy(spec);
@@ -469,6 +469,12 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
469
469
  values: ["gemini", "anthropic", "codex", "xai", "openrouter", "openai"],
470
470
  },
471
471
  "web-search-model": { key: "webSearchModel", set: "catalog", shape: "scalar" },
472
+ /**
473
+ * Revision suffix the host appends to the model's id (Vertex `@default`): a
474
+ * `provider/id` selector written without it names this model exactly.
475
+ * Unassigned: the id has no implied revision and must be written in full.
476
+ */
477
+ "selector-revision-suffix": { key: "selectorRevisionSuffix", set: "catalog", shape: "scalar" },
472
478
  "hosted-image": { key: "hostedImage", set: "catalog", shape: "scalar", values: [true, false] },
473
479
  /** How the model line bills an input image; shape and formulas in `./image-tokenization`. */
474
480
  "image-tokenization": { key: "imageTokenization", set: "catalog", shape: "object" },
@@ -496,8 +502,9 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
496
502
  "image-max-edge": { key: "imageMaxEdge", set: "catalog", shape: "scalar" },
497
503
  "inline-image-byte-budget": { key: "inlineImageByteBudget", set: "catalog", shape: "scalar" },
498
504
  /**
499
- * Largest request body the host accepts. Where set, the inline-image byte clamp keeps the newest user message's
500
- * and newest tool result's images past `inline-image-byte-budget` while they alone fit this limit.
505
+ * Largest request body the host accepts. Where set, the inline-image byte clamp keeps images within what the
506
+ * request's other bytes leave of it, and keeps the newest user message's and newest tool result's images past
507
+ * `inline-image-byte-budget` while they fit that room.
501
508
  */
502
509
  "request-body-byte-limit": { key: "requestBodyByteLimit", set: "catalog", shape: "scalar" },
503
510
  /**
@@ -179,16 +179,16 @@ provider "openrouter" {
179
179
 
180
180
  ### Selectors and nesting
181
181
 
182
- | Selector | Form | Matching semantics |
183
- | ---------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
184
- | `class` | `class "id" { ... }` | Exact class ID. At document root it may contain `on`, `on-api`, `family`, `revision`, and `models`. Under `provider` it may contain `family`, `revision`, and `models`. |
185
- | `provider` | `provider "id" { ... }` | Exact provider ID. It is root-only and may contain `class`, `on-api`, and `models`. |
186
- | `on` | `on "provider-a" "provider-b" { ... }` | One or more provider IDs, combined as OR. It is allowed under a root `class`, and may contain `on-api` to conjoin an API scope plus `family`, `revision`, and `models`. |
187
- | `on-api` | `on-api "adapter-a" "adapter-b" { ... }` | One or more request adapter IDs, combined as OR. At document root it may contain `class` and `models`; under a root `class` (directly or nested inside `on`) or `provider` it may contain `family`, `revision`, and `models`. |
188
- | `family` | `family "id" { ... }` | Exact classified family ID. It may contain `revision` and `models`. A target with no family does not match. |
189
- | `revision` | `revision ">=2.5 <4" { ... }` | A non-empty, whitespace-separated conjunction of comparisons. It may contain `models`. A target with no revision does not match. |
190
- | `models` | `models "id" "vendor/*" { ... }` | One or more alternatives, combined as OR. It may contain only `on-upstream`. `token="name"` matches an ASCII-case-insensitive token bounded by non-alphanumerics. |
191
- | `on-upstream` | `on-upstream "a" "b" { ... }` | Exact selected upstream IDs, combined as OR. Allowed inside any selector scope once; preserves the containing scope's other permitted children. Absent upstream never matches. |
182
+ | Selector | Form | Matching semantics |
183
+ | ------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
184
+ | `class` | `class "id" { ... }` | Exact class ID. At document root it may contain `on`, `on-api`, `family`, `revision`, and `models`. Under `provider` it may contain `family`, `revision`, and `models`. |
185
+ | `provider` | `provider "id" { ... }` | Exact provider ID. It is root-only and may contain `class`, `on-api`, and `models`. |
186
+ | `on` | `on "provider-a" "provider-b" { ... }` | One or more provider IDs, combined as OR. It is allowed under a root `class`, and may contain `on-api` to conjoin an API scope plus `family`, `revision`, and `models`. |
187
+ | `on-api` | `on-api "adapter-a" "adapter-b" { ... }` | One or more request adapter IDs, combined as OR. At document root it may contain `class` and `models`; under a root `class` (directly or nested inside `on`) or `provider` it may contain `family`, `revision`, and `models`. |
188
+ | `family` | `family "id" { ... }` | Exact classified family ID. It may contain `revision` and `models`. A target with no family does not match. |
189
+ | `revision` | `revision ">=2.5 <4" { ... }` | A non-empty, whitespace-separated conjunction of comparisons. It may contain `models`. A target with no revision does not match. |
190
+ | `models` | `models "id" "vendor/*" { ... }` | One or more alternatives, combined as OR. It may contain only `on-upstream`. `token="name"` matches an ASCII-case-insensitive token bounded by non-alphanumerics. |
191
+ | `on-upstream` | `on-upstream "a" "b" { ... }` | Exact selected upstream IDs, combined as OR. Allowed inside any selector scope once; preserves the containing scope's other permitted children. Absent upstream never matches. |
192
192
 
193
193
  Every selector scope may additionally contain `on-upstream`; it cannot replace an already constrained upstream. Class, provider/`on`, `on-api`, `on-upstream`, and family values are compared exactly and case-sensitively to the structured resolve target. Revision operators are `>=`, `>`, `<=`, `<`, and `=`; operands have one to three dot-separated unsigned 8-bit components, omitted components zero.
194
194
 
@@ -227,7 +227,7 @@ One object axis carries a computed form: `long-context-cost` accepts either the
227
227
 
228
228
  `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, and snapcompact gives its archive frames half of it (`frameDataBytesBudget`). The budget applies only while the request reaches the host the rules describe (the Anthropic API, first-party OpenAI, the Codex backend at its default URL, DeepSeek's API, or Factory Droid's own global or EU API host), which callers decide with pi-ai's `reachesBudgetedHost`: `compat.officialEndpoint` for the configured `baseUrl`, for first-party Anthropic models also the `ANTHROPIC_BASE_URL` or Foundry `FOUNDRY_BASE_URL` reroute applied at request time, and for Factory Droid models the `baseUrl` origin. A custom or rerouted endpoint imposes its own body limit.
229
229
 
230
- `request-body-byte-limit` is the largest request body the host accepts (Factory Droid's hosted routes: 4,500,000 bytes), under the same gate. Where it is set, the byte clamp keeps the inline images of the newest user message and then of the newest image-bearing tool result even past `inline-image-byte-budget`, each only while the kept images fit the limit less the request's other serialized bytes, and evicts older images instead; read through `resolveRequestBodyByteLimit`.
230
+ `request-body-byte-limit` is the largest request body the host accepts (Factory Droid's hosted routes: 4,500,000 bytes; Anthropic's Messages API: 128,000,000 bytes), under the same gate. Where it is set, the byte clamp also keeps inline images within what the request's other serialized bytes leave of the limit, and keeps the inline images of the newest user message and then of the newest image-bearing tool result even past `inline-image-byte-budget`, each only while the kept images fit the limit less the request's other serialized bytes, and evicts older images instead; read through `resolveRequestBodyByteLimit`.
231
231
 
232
232
  `catalog-endpoint-budgets #true` makes these host budgets apply whenever a request reaches the provider's own catalog endpoint (an unset `baseUrl`, or an `endpoint-hosts` host; see `isCatalogEndpointUrl`), for a hosted gateway such as Factory Droid whose compat names no official endpoint, or a first-party provider such as DeepSeek whose `openai-completions` compat resolves no `officialEndpoint`. Unassigned, they apply only on the official endpoint.
233
233
 
@@ -415,7 +415,7 @@ auth "anthropic" {
415
415
 
416
416
  Login kinds:
417
417
 
418
- - `login "api-key" { auth-url "…"; instructions "…"; prompt "…" placeholder="…"; empty-fallback "…"; normalize "strip-bearer"; validate … }` — `validate "chat-completions" base-url= model= tolerate-model-denied= max-tokens-field= max-tokens=`, `validate "anthropic-messages" base-url= model=`, or `validate "models-endpoint" url= base-url-env= headers-hook=`; all accept `label=` (error-message label, defaults to `name`), `optional=#true` (only auth failures reject), and `trust-forbidden=#true` (requires `optional=#true`; a 403 no longer rejects, so only a 401 does).
418
+ - `login "api-key" { auth-url "…"; instructions "…"; prompt "…" placeholder="…"; empty-fallback "…"; normalize "strip-bearer"; validate … }` — `validate "chat-completions" base-url= model= tolerate-model-denied= max-tokens-field= max-tokens=`, `validate "anthropic-messages" base-url= model=`, or `validate "models-endpoint" url= base-url-env= base-url-path= headers-hook=` (`base-url-path`, default `/models`, is appended to the `base-url-env` value and needs it); all accept `label=` (error-message label, defaults to `name`), `optional=#true` (only auth failures reject), and `trust-forbidden=#true` (requires `optional=#true`; a 403 no longer rejects, so only a 401 does).
419
419
  - `login "oauth-code" { … }` as above; `token`/`refresh` `params` values may use `{code}`, `{state}`, `{redirect_uri}`, `{code_verifier}`, `{client_id}`, `{client_secret}`, `{refresh_token}`, `{scope}`; the standard grant parameters are sent unless `standard=#false`.
420
420
  - `login "device-code" { client-id …; base-url "…"; scopes …; headers-hook "…"; device url="{base}/…" body="form" { params {…} headers {…} }; token url="…" url-hook="…"; response user-code= device-code= verification-uri= verification-uri-complete= interval= expires-in=; instructions "Enter code: {user_code}"; credential {…}; userinfo …; after-exchange hook=… }`.
421
421
  - `login "custom" hook="name"` — the whole flow is a named `@oh-my-pi/pi-ai` hook (`src/registry/hooks/custom.ts`).
@@ -8,6 +8,7 @@ auth "typesafe" {
8
8
  auth-url "https://console.typesafe.ai/"
9
9
  instructions "Create or copy your API key from the TypeSafe console."
10
10
  prompt "Paste your TypeSafe API key" placeholder="API key"
11
- validate "models-endpoint" label="TypeSafe" url="https://api.typesafe.ai/v1/models" base-url-env="TYPESAFE_BASE_URL"
11
+ // TYPESAFE_BASE_URL is an origin without `/v1` (the SDK appends `/v1/systemone`).
12
+ validate "models-endpoint" label="TypeSafe" url="https://api.typesafe.ai/v1/models" base-url-env="TYPESAFE_BASE_URL" base-url-path="/v1/models"
12
13
  }
13
14
  }
@@ -90,10 +90,11 @@ class "deepseek" {
90
90
  }
91
91
  // V4.1 Flash is natively multimodal (vision encoder + projector, image-text
92
92
  // pre-training) but its release id carries no `vision` token to key on.
93
+ // The leading glob also covers publisher-namespaced ids (`ds/deepseek-flash`).
93
94
  // Same priority as the V4.1 Flash carve-out: the OpenCode Go provider rule
94
95
  // declares the same value for its `deepseek-flash` lane and would otherwise
95
96
  // tie at equal rank.
96
- models "deepseek-flash" priority=10 {
97
+ models "*deepseek-flash" priority=10 {
97
98
  strip-image-input #false
98
99
  }
99
100
  // DSML is the model's own tool-call grammar, not a host quirk: any server
@@ -78,12 +78,16 @@ provider "anthropic" {
78
78
  // Code's `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS` kill switch for gateways
79
79
  // that reject experimental beta fields.
80
80
  honors-claude-code-beta-kill-switch #true
81
- // Messages requests cap at 32 MB. Reserve 8 MB for text, tools, and JSON
82
- // framing when retaining inline base64 images in a long conversation.
83
- // Applied only on the official endpoint; a custom baseUrl has its own limit.
81
+ // Messages requests cap at 128 MB. Reserve 8 MB for text, tools, and JSON
82
+ // framing when retaining inline base64 images in a long conversation; the
83
+ // body limit keeps images within what the other bytes leave, and lets the
84
+ // newest user message's and tool result's images use the reserve.
85
+ // Applied only on the official endpoint; a custom baseUrl has its own limit
86
+ // (Bedrock 20 MB; Vertex, Foundry, and Claude Platform on AWS 30-32 MB).
84
87
  // https://platform.claude.com/docs/en/api/overview#request-size-limits
85
88
  on-api "anthropic-messages" {
86
- inline-image-byte-budget 24000000
89
+ inline-image-byte-budget 120000000
90
+ request-body-byte-limit 128000000
87
91
  }
88
92
  // The official endpoint carries at most 600 images and documents per
89
93
  // request, 100 on models with a 200k-token window: every Claude line before
@@ -387,6 +387,16 @@ provider "cursor" {
387
387
  cache-write 0
388
388
  }
389
389
  }
390
+ // DeepSeek V4.1 Flash 1017: $0.30/$1.20, cache read $0.006, no write fee.
391
+ // The bare id and every reasoning-effort wire id share one card.
392
+ models "deepseek-v41-flash-1017*" {
393
+ cost-patch {
394
+ input 0.3
395
+ output 1.2
396
+ cache-read 0.006
397
+ cache-write 0
398
+ }
399
+ }
390
400
  // Gemini 3 Flash: $0.50/$3.00, cache read $0.05.
391
401
  models "gemini-3-flash" {
392
402
  cost-patch {
@@ -7,8 +7,8 @@ provider "deepseek" {
7
7
 
8
8
  // DeepSeek's vision guide documents 600 images per request and a 48 MiB
9
9
  // request body. OMP reserves 8 MiB for text, tools, and the JSON envelope
10
- // (48 MiB - 8 MiB = 40 MiB); Anthropic's 24,000,000-byte image budget under
11
- // its 32 MB body uses an 8 MB reserve. DeepSeek documents MiB, so this uses
10
+ // (48 MiB - 8 MiB = 40 MiB); Anthropic's 120,000,000-byte image budget under
11
+ // its 128 MB body uses an 8 MB reserve. DeepSeek documents MiB, so this uses
12
12
  // an 8 MiB reserve. The split is OMP's conservative margin, not a
13
13
  // DeepSeek-documented figure. A custom baseUrl keeps the conservative
14
14
  // catalog floor because the gateway's own limits are unknown.
@@ -4,6 +4,9 @@ provider "google-vertex" {
4
4
  default-model "gemini-3.1-pro-preview"
5
5
  allow-unauthenticated #true
6
6
  image-count-budget 200
7
+ // Vertex publishes Anthropic ids pinned to a revision (`claude-opus-4-8@default`);
8
+ // `google-vertex/claude-opus-4-8` names that model exactly.
9
+ selector-revision-suffix "@default"
7
10
 
8
11
  // Replaces google-shared.ts stripping functionCall/functionResponse ids for Vertex.
9
12
  supports-function-part-id #false
@@ -29,6 +29,14 @@ provider "mistral" {
29
29
  // without it, requests only hit replica-local implicit cache. Same shape as
30
30
  // the kimi-code rule (#6049).
31
31
  supports-prompt-cache-key #true
32
+ // The gateway accepts assistant reasoning as text, not DeepSeek's native
33
+ // reasoning_content field, even when it serves DeepSeek-family models.
34
+ requires-reasoning-content-for-tool-calls #false
35
+ requires-reasoning-content-for-all-assistant-turns #false
36
+ // Reasoning streams and replays as typed `thinking` content parts
37
+ // (https://docs.mistral.ai/studio/conversations/reasoning) for every
38
+ // reasoning model the host serves, including GLM and DeepSeek (#17375).
39
+ mistral-reasoning-content-parts #true
32
40
  class "unknown" {
33
41
  thinking-efforts "minimal" "low" "medium" "high" "xhigh"
34
42
  }
@@ -16,7 +16,7 @@ provider "openai-codex" {
16
16
 
17
17
  // Policy budgets: the Codex backend documents no request-size or image
18
18
  // limit, and requests with several thousand images fail mid-stream at
19
- // random. Inline images keep Anthropic's 24 MB; the official endpoint's
19
+ // random. Inline images keep a 24 MB budget; the official endpoint's
20
20
  // image count stays well below those failures. A custom baseUrl keeps 200.
21
21
  inline-image-byte-budget 24000000
22
22
  image-count-budget 200
@@ -428,7 +428,6 @@ behavior {
428
428
  model "oss-120b"
429
429
  model "cerebras/zai-glm-4.7"
430
430
  model "cerebras/zai-glm-4.6"
431
- model "cerebras/zai-glm"
432
431
  model "claude-haiku-4-5"
433
432
  model "claude-haiku-4.5"
434
433
  model "haiku-4-5"
@@ -463,7 +462,6 @@ behavior {
463
462
  model "anthropic/claude-opus-5"
464
463
  model "claude-opus-5"
465
464
  model "opus-5"
466
- model "openai-codex/gpt-5.5"
467
465
  model "gpt-5.5"
468
466
  model "claude-opus-4-8"
469
467
  model "claude-opus-4.8"
@@ -479,6 +477,11 @@ behavior {
479
477
  model "openrouter/google/gemini-3-pro-image"
480
478
  model "google/gemini-3-pro-image"
481
479
  model "deepinfra/black-forest-labs/FLUX-2-pro"
480
+ // Factory Droid last, in the CLI's own preference order: a Factory
481
+ // login alone can generate, and no other provider's turn moves.
482
+ model "factory-droid/gpt-image-2.5-sunburst"
483
+ model "factory-droid/gpt-image-2.5-flare"
484
+ model "factory-droid/gemini-3-pro-image-preview"
482
485
  }
483
486
  role-defaults "web" {
484
487
  model "web/parallel"