@oh-my-pi/pi-catalog 17.2.1 → 17.2.3

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,31 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ### Fixed
6
+
7
+ - Fixed `gen:models` Codex discovery to union models across every stored OAuth account and fail closed on partial resolution, matching runtime discovery ([#6265](https://github.com/can1357/oh-my-pi/issues/6265)); restored the bundled `gpt-5.4`, `gpt-5.6-sol`, and `gpt-5.3-codex-spark` entries a single-account regen had dropped.
8
+
9
+ ## [17.2.3] - 2026-08-01
10
+
11
+ ### Added
12
+
13
+ - Added support for the ai& provider (`aiand`), an OpenAI-compatible inference API with dynamic model discovery (context windows, capabilities, reasoning efforts, and USD pricing from `/v1/models`) and API-key authentication via the `AIAND_API_KEY` environment variable.
14
+
15
+ ## [17.2.2] - 2026-07-31
16
+
17
+ ### Added
18
+
19
+ - Added support for the GMI Cloud provider (`gmi-cloud`), an OpenAI-compatible inference gateway with dynamic model discovery and API-key authentication via the `GMI_API_KEY` environment variable.
20
+ - Added optional authoritative context occupancy to usage records for providers with separate checkpoint telemetry and billable token buckets.
21
+
22
+ ### Fixed
23
+
24
+ - Fixed classification of dynamically discovered Cursor Kimi K3 effort variants as non-reasoning models when `thinkingDetails` is omitted.
25
+ - Fixed Google AI Studio OpenAI-compatible requests failing with HTTP 400 by omitting the unsupported `store` field.
26
+ - Fixed Synthetic models losing capabilities (such as reasoning/thinking selectors, vision input, output limits, and pricing) by correcting how the discovery mapper parses Synthetic's advertised features, effort vocabularies, and pricing structures.
27
+ - Fixed Cursor model discovery to correctly expose the 1M-token context window for supported models (including Claude, GPT, Kimi K3, and GLM 5.2+ families) instead of defaulting to 200k.
28
+ - Fixed GitHub Copilot routing for `grok-4.5` to use the correct Responses endpoint instead of the unsupported Chat Completions endpoint.
29
+
5
30
  ## [17.2.1] - 2026-07-30
6
31
 
7
32
  ### Fixed
package/README.md CHANGED
@@ -4,24 +4,24 @@ Model catalog for [oh-my-pi](https://github.com/can1357/oh-my-pi): bundled model
4
4
 
5
5
  ## What's inside
6
6
 
7
- | Module | Purpose |
8
- | --- | --- |
9
- | `models.json` + `models` | Bundled model database (pricing, context windows, modalities, thinking support) |
10
- | `provider-models` | Provider catalog descriptors (`CATALOG_PROVIDERS`), per-provider model resolution rules |
11
- | `discovery` | Runtime model discovery for OpenAI-compatible endpoints, Gemini, Codex, Cursor, Antigravity, Ollama |
12
- | `identity` | Model id parsing and classification (family/version), reference resolution, equivalence, selection priority |
13
- | `model-thinking` | Thinking/reasoning metadata and generated per-model policies |
14
- | `model-manager` / `model-cache` | Runtime model registry with discovery refresh and on-disk caching |
15
- | `variant-collapse` | Collapsing provider-specific variants of the same underlying model |
16
- | `compat` | Request/response compatibility fixups for OpenAI- and Anthropic-shaped APIs |
17
- | `wire` | Wire-level helpers: Codex, Gemini headers, GitHub Copilot |
18
- | `effort` | Reasoning-effort level definitions |
7
+ | Module | Purpose |
8
+ | ------------------------------- | ----------------------------------------------------------------------------------------------------------- |
9
+ | `models.json` + `models` | Bundled model database (pricing, context windows, modalities, thinking support) |
10
+ | `provider-models` | Provider catalog descriptors (`CATALOG_PROVIDERS`), per-provider model resolution rules |
11
+ | `discovery` | Runtime model discovery for OpenAI-compatible endpoints, Gemini, Codex, Cursor, Antigravity, Ollama |
12
+ | `identity` | Model id parsing and classification (family/version), reference resolution, equivalence, selection priority |
13
+ | `model-thinking` | Thinking/reasoning metadata and generated per-model policies |
14
+ | `model-manager` / `model-cache` | Runtime model registry with discovery refresh and on-disk caching |
15
+ | `variant-collapse` | Collapsing provider-specific variants of the same underlying model |
16
+ | `compat` | Request/response compatibility fixups for OpenAI- and Anthropic-shaped APIs |
17
+ | `wire` | Wire-level helpers: Codex, Gemini headers, GitHub Copilot |
18
+ | `effort` | Reasoning-effort level definitions |
19
19
 
20
20
  Import from subpaths (`@oh-my-pi/pi-catalog/<module>`) or the root barrel.
21
21
 
22
22
  ## models.json is generated
23
23
 
24
- Never edit `src/models.json` by hand — it is produced from upstream sources (models.dev, provider catalog discovery, OpenCode docs) by `scripts/generate-models.ts` and the resolvers in `src/provider-models/`. Regenerate with:
24
+ Never edit `src/models.json` by hand — it is produced from upstream sources (stencil.so, provider catalog discovery, OpenCode docs) by `scripts/generate-models.ts` and the resolvers in `src/provider-models/`. Regenerate with:
25
25
 
26
26
  ```sh
27
27
  bun run gen:models
@@ -115,6 +115,11 @@ export declare const KNOWN_HOSTS: {
115
115
  readonly providers: readonly ["moonshot", "kimi-code"];
116
116
  readonly urlMarkers: readonly ["api.moonshot.ai", "api.kimi.com"];
117
117
  };
118
+ /** Google AI Studio's OpenAI-compatible shim (`/v1beta/openai`) — a subset of chat-completions; rejects `store` with a 400. Native Gemini uses `google-generative-ai` api instead. */
119
+ readonly googleAistudio: {
120
+ readonly providers: readonly [];
121
+ readonly urlMarkers: readonly ["generativelanguage.googleapis.com"];
122
+ };
118
123
  readonly opencode: {
119
124
  readonly providers: readonly ["opencode-go", "opencode-zen"];
120
125
  readonly urlMarkers: readonly ["opencode.ai"];
@@ -4,10 +4,10 @@ import type { Api, Model, ModelSpec, Provider } from "./types.js";
4
4
  */
5
5
  export type ModelRefreshStrategy = "online" | "offline" | "online-if-uncached";
6
6
  /**
7
- * Hook for loading and mapping models.dev fallback data into canonical model objects.
7
+ * Hook for loading and mapping stencil.so fallback data into canonical model objects.
8
8
  */
9
9
  export interface ModelsDevFallback<TApi extends Api = Api, TPayload = unknown> {
10
- /** Fetches raw fallback payload (for example from models.dev). */
10
+ /** Fetches raw fallback payload (for example from stencil.so). */
11
11
  fetch(): Promise<TPayload>;
12
12
  /** Maps payload into provider models. */
13
13
  map(payload: TPayload, providerId: Provider): readonly ModelSpec<TApi>[];
@@ -28,7 +28,7 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
28
28
  cacheTtlMs?: number;
29
29
  /** When true, a successful dynamic fetch is the complete provider catalog and prunes static-only models. */
30
30
  dynamicModelsAuthoritative?: boolean;
31
- /** Cached model ids to ignore when the cache was written against a different static catalog fingerprint. */
31
+ /** Cached model ids whose presence forces refresh when the static or migration-policy fingerprint changes. */
32
32
  dropCachedModelIdsOnStaticMismatch?: readonly string[];
33
33
  /**
34
34
  * Trusted, provider-wide request headers (compile-time constants, never
@@ -40,7 +40,7 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
40
40
  restorableHeaderFallback?: Record<string, string>;
41
41
  /** Optional dynamic endpoint fetcher. */
42
42
  fetchDynamicModels?: () => Promise<readonly ModelSpec<TApi>[] | null>;
43
- /** Optional models.dev fallback hook. */
43
+ /** Optional stencil.so fallback hook. */
44
44
  modelsDev?: ModelsDevFallback<TApi, TModelsDevPayload>;
45
45
  /** Clock override for deterministic tests. */
46
46
  now?: () => number;
@@ -70,7 +70,7 @@ export interface ModelManager<TApi extends Api = Api> {
70
70
  export declare function createModelManager<TApi extends Api = Api, TModelsDevPayload = unknown>(options: ModelManagerOptions<TApi, TModelsDevPayload>): ModelManager<TApi>;
71
71
  /**
72
72
  * Resolves provider models with source precedence:
73
- * static -> models.dev -> cache -> dynamic.
73
+ * static -> stencil.so -> cache -> dynamic.
74
74
  *
75
75
  * Later sources override earlier ones by model id.
76
76
  */
@@ -7,6 +7,15 @@
7
7
  */
8
8
  import type { ModelManagerConfig, ProviderCatalogEntry, ProviderDescriptor } from "./descriptor-types.js";
9
9
  export declare const CATALOG_PROVIDERS: readonly [{
10
+ readonly id: "aiand";
11
+ readonly defaultModel: "moonshotai/kimi-k2.7-code";
12
+ readonly envVars: readonly ["AIAND_API_KEY"];
13
+ readonly createModelManagerOptions: (config: ModelManagerConfig) => import("../index.js").ModelManagerOptions<"openai-completions", unknown>;
14
+ readonly dynamicModelsAuthoritative: true;
15
+ readonly catalogDiscovery: {
16
+ readonly label: "ai&";
17
+ };
18
+ }, {
10
19
  readonly id: "aimlapi";
11
20
  readonly defaultModel: "gpt-5.5-2026-04-23";
12
21
  readonly envVars: readonly ["AIMLAPI_API_KEY"];
@@ -129,6 +138,15 @@ export declare const CATALOG_PROVIDERS: readonly [{
129
138
  readonly envVars: readonly ["GITLAB_TOKEN"];
130
139
  readonly createModelManagerOptions: (config: ModelManagerConfig) => import("../index.js").ModelManagerOptions<"gitlab-duo-agent", unknown>;
131
140
  readonly dynamicModelsAuthoritative: true;
141
+ }, {
142
+ readonly id: "gmi-cloud";
143
+ readonly defaultModel: "deepseek-ai/DeepSeek-V4-Flash";
144
+ readonly envVars: readonly ["GMI_API_KEY"];
145
+ readonly createModelManagerOptions: (config: ModelManagerConfig) => import("../index.js").ModelManagerOptions<"openai-completions", unknown>;
146
+ readonly dynamicModelsAuthoritative: true;
147
+ readonly catalogDiscovery: {
148
+ readonly label: "GMI Cloud";
149
+ };
132
150
  }, {
133
151
  readonly id: "google";
134
152
  readonly defaultModel: "gemini-3.1-pro-preview";
@@ -25,6 +25,17 @@ export interface ModelsDevModel {
25
25
  npm?: string;
26
26
  };
27
27
  }
28
+ /**
29
+ * Fetches the models.dev catalog via catalog.stencil.so, which serves a
30
+ * field-pruned copy precompressed as a zstd blob (~93 KB vs ~3.3 MB raw).
31
+ * The frame magic is sniffed rather than trusting content-type so plain-JSON
32
+ * responses (test stubs, fallback mirrors) parse identically.
33
+ *
34
+ * Fetched fully once per process: concurrent callers share the in-flight
35
+ * request, repeat callers send a conditional GET that the server answers
36
+ * (and deliberately does not log) with `304`.
37
+ */
38
+ export declare function fetchWellKnownModels(fetchImpl?: FetchImpl, signal?: AbortSignal): Promise<unknown>;
28
39
  /**
29
40
  * Curated Anthropic models that are live or limited-availability on the
30
41
  * first-party `/v1/models` endpoint but that models.dev has not catalogued yet.
@@ -67,6 +78,26 @@ export declare function openaiModelManagerOptions(config?: OpenAIModelManagerCon
67
78
  * upstream model that occupies an alias id wins and suppresses the projection.
68
79
  */
69
80
  export declare function projectOpenAIProReasoningAliases(models: readonly ModelSpec<Api>[]): ModelSpec<Api>[];
81
+ /**
82
+ * Bundled seed for GMI Cloud. Generation has no `GMI_API_KEY`, so a regen
83
+ * without credentials would leave the provider slice empty and the declared
84
+ * `defaultModel` unresolvable on a fresh install before the async runtime
85
+ * discovery fires. Live `/v1/models` discovery is authoritative for the model
86
+ * ID set and overrides context/max-token limits, but `mapWithBundledReference`
87
+ * keeps the reference's cost/reasoning/thinking — so these fields carry GMI's
88
+ * direct-tariff values: V4-Flash at $0.14/$0.28 per 1M with Think High/Max
89
+ * modes per GMI's launch post
90
+ * (https://www.gmicloud.ai/en/blog/deepseek-v4-is-here-we-tested-it), not
91
+ * discounted gateway-route pricing. GMI publishes no cache-read tariff, so
92
+ * cacheRead stays 0 until a direct source confirms cached-token billing.
93
+ */
94
+ export declare const GMI_CLOUD_STATIC_MODELS: readonly ModelSpec<"openai-completions">[];
95
+ export interface GmiCloudModelManagerConfig {
96
+ apiKey?: string;
97
+ baseUrl?: string;
98
+ fetch?: FetchImpl;
99
+ }
100
+ export declare function gmiCloudModelManagerOptions(config?: GmiCloudModelManagerConfig): ModelManagerOptions<"openai-completions">;
70
101
  export interface GroqModelManagerConfig {
71
102
  apiKey?: string;
72
103
  baseUrl?: string;
@@ -412,6 +443,23 @@ export interface SakanaModelManagerConfig {
412
443
  fetch?: FetchImpl;
413
444
  }
414
445
  export declare function sakanaModelManagerOptions(config?: SakanaModelManagerConfig): ModelManagerOptions<"openai-responses">;
446
+ /**
447
+ * Documented ai& catalog (docs.aiand.com/models/catalog, 2026-08) bundled so
448
+ * the provider is usable when generation and first boot have no live key.
449
+ * The org-scoped `/v1/models` response is authoritative once discovery runs.
450
+ */
451
+ export declare const AIAND_STATIC_MODELS: readonly ModelSpec<"openai-completions">[];
452
+ export interface AiandModelManagerConfig {
453
+ apiKey?: string;
454
+ baseUrl?: string;
455
+ fetch?: FetchImpl;
456
+ }
457
+ /**
458
+ * ai& (aiand.com) model manager: OpenAI-compatible chat completions with an
459
+ * org-scoped `/v1/models` catalog carrying context, capability, effort, and
460
+ * pricing metadata, so discovery is authoritative over the bundled seed.
461
+ */
462
+ export declare function aiandModelManagerOptions(config?: AiandModelManagerConfig): ModelManagerOptions<"openai-completions">;
415
463
  export interface QwenPortalModelManagerConfig {
416
464
  apiKey?: string;
417
465
  baseUrl?: string;
@@ -75,6 +75,8 @@ export interface Usage {
75
75
  cacheWrite: number;
76
76
  /** Sum of input + output + cacheRead + cacheWrite plus provider-side orchestration tokens when reported. */
77
77
  totalTokens: number;
78
+ /** Provider-reported occupied context tokens when the value is authoritative but not a billable input/output bucket. */
79
+ contextTokens?: number;
78
80
  /** Provider-side orchestration tokens, billed but not part of the conversation prompt/cache buckets. */
79
81
  orchestration?: {
80
82
  /** Non-cached orchestration input tokens. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-catalog",
4
- "version": "17.2.1",
4
+ "version": "17.2.3",
5
5
  "description": "Model catalog for omp: bundled model database, provider discovery descriptors, model identity, classification, and equivalence",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -35,12 +35,12 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@bufbuild/protobuf": "^2.12.1",
38
- "@oh-my-pi/pi-utils": "17.2.1",
38
+ "@oh-my-pi/pi-utils": "17.2.3",
39
39
  "arktype": "2.2.3",
40
40
  "zod": "^4"
41
41
  },
42
42
  "devDependencies": {
43
- "@oh-my-pi/pi-ai": "17.2.1",
43
+ "@oh-my-pi/pi-ai": "17.2.3",
44
44
  "@types/bun": "^1.3.14"
45
45
  },
46
46
  "engines": {
@@ -322,10 +322,14 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
322
322
  const isGrok = modelMatchesHost(hostModel, "xai");
323
323
  const isMistral = modelMatchesHost(hostModel, "mistral");
324
324
  const isOpenCodeHost = modelMatchesHost(hostModel, "opencode");
325
+ // Google AI Studio's OpenAI-compat shim (`generativelanguage.googleapis.com/v1beta/openai`)
326
+ // implements a subset of chat-completions and 400s on `store` ("Unknown name \"store\"").
327
+ const isGoogleAistudioOpenAI = hostMatchesUrl(baseUrl, "googleAistudio");
325
328
  const isNonStandard =
326
329
  isCerebras ||
327
330
  isGrok ||
328
331
  isMistral ||
332
+ isGoogleAistudioOpenAI ||
329
333
  hostMatchesUrl(baseUrl, "chutes") ||
330
334
  hostMatchesUrl(baseUrl, "deepseekFamily") ||
331
335
  hostMatchesUrl(baseUrl, "fireworks") ||
@@ -1,6 +1,8 @@
1
1
  import * as http2 from "node:http2";
2
2
  import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
3
3
  import { type } from "arktype";
4
+ import { isKimiK3ModelId } from "../identity";
5
+ import { bareModelId, parseGlmModel, semverGte } from "../identity/classify";
4
6
  import { getBundledModels } from "../models";
5
7
  import { toModelSpec } from "../provider-models/bundled-references";
6
8
  import type { Model, ModelSpec } from "../types";
@@ -13,6 +15,19 @@ const CURSOR_GET_USABLE_MODELS_PATH = "/agent.v1.AgentService/GetUsableModels";
13
15
  const DEFAULT_CONTEXT_WINDOW = 200_000;
14
16
  const DEFAULT_MAX_TOKENS = 64_000;
15
17
 
18
+ /**
19
+ * `GetUsableModels` carries no context-window field, so the 1M ceiling is
20
+ * recovered from the signals Cursor does send:
21
+ * - display-name labels ("Opus 5 1M", "GPT-5.5 1M High") across families,
22
+ * - natively 1M families Cursor serves unlabeled (Kimi K3, GLM 5.2+),
23
+ * - the max-mode flag on Claude/Gemini ids, whose max-mode ceiling is 1M.
24
+ */
25
+ const CURSOR_1M_CONTEXT_WINDOW = 1_000_000;
26
+ const CURSOR_1M_NAME_PATTERN = /\b1m\b/i;
27
+ const CURSOR_MAX_MODE_1M_ID_PATTERN = /claude|gemini/;
28
+ /** Kimi's official bare K3 id (`k3`, `kimi/k3`); `k3-256k` is the 256k SKU and stays out. */
29
+ const CURSOR_KIMI_K3_BARE_ID_PATTERN = /(^|\/)k3$/i;
30
+
16
31
  /**
17
32
  * Model-id families whose native catalogs (anthropic, openai/openai-codex,
18
33
  * google) are multimodal. Cursor-only or text-only families (`composer-*`,
@@ -282,7 +297,7 @@ function normalizeCursorModel(
282
297
 
283
298
  const name = pickModelDisplayName(details, id);
284
299
  const reference = references.get(id);
285
- const reasoning = Boolean(details.thinkingDetails) || reference?.reasoning === true;
300
+ const reasoning = isKimiK3ModelId(id) || Boolean(details.thinkingDetails) || reference?.reasoning === true;
286
301
 
287
302
  if (reference) {
288
303
  return {
@@ -291,6 +306,7 @@ function normalizeCursorModel(
291
306
  name,
292
307
  baseUrl: baseUrlOverride ?? reference.baseUrl,
293
308
  reasoning,
309
+ contextWindow: resolveCursorContextWindow(details, id, reference.contextWindow),
294
310
  cursorMaxMode: details.maxMode,
295
311
  };
296
312
  }
@@ -303,12 +319,53 @@ function normalizeCursorModel(
303
319
  reasoning,
304
320
  input: inferInputFromCursorId(id),
305
321
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
306
- contextWindow: DEFAULT_CONTEXT_WINDOW,
322
+ contextWindow: resolveCursorContextWindow(details, id, DEFAULT_CONTEXT_WINDOW),
307
323
  maxTokens: DEFAULT_MAX_TOKENS,
308
324
  cursorMaxMode: details.maxMode,
309
325
  };
310
326
  }
311
327
 
328
+ /**
329
+ * Context window for a discovered Cursor model: the 1M ceiling when any 1M
330
+ * signal fires (never below a larger bundled reference), else the fallback.
331
+ */
332
+ function resolveCursorContextWindow(
333
+ model: CursorModelDetailsValue,
334
+ id: string,
335
+ fallback: number | null,
336
+ ): number | null {
337
+ const labeled1M =
338
+ CURSOR_1M_NAME_PATTERN.test(id) ||
339
+ [model.displayName, model.displayNameShort, model.displayModelId, ...model.aliases].some(
340
+ candidate => typeof candidate === "string" && CURSOR_1M_NAME_PATTERN.test(candidate),
341
+ );
342
+ if (labeled1M || isCursorNative1MModelId(id) || (model.maxMode && CURSOR_MAX_MODE_1M_ID_PATTERN.test(id))) {
343
+ return Math.max(fallback ?? 0, CURSOR_1M_CONTEXT_WINDOW);
344
+ }
345
+ return fallback;
346
+ }
347
+
348
+ /**
349
+ * Natively 1M-context families Cursor serves without a "1M" label: Kimi K3 and
350
+ * GLM 5.2+ coding SKUs. The shared family parsers cover namespace forms
351
+ * (`moonshotai/kimi-k3`, `z-ai/glm-5.2`) and future GLM versions (`glm-5.10`,
352
+ * `glm-6`); vision and sub-1M variants stay out via the same gates as
353
+ * `isGlm52ReasoningEffortModelId`.
354
+ */
355
+ function isCursorNative1MModelId(id: string): boolean {
356
+ if (isKimiK3ModelId(id) || CURSOR_KIMI_K3_BARE_ID_PATTERN.test(id)) {
357
+ return true;
358
+ }
359
+ const glm = parseGlmModel(bareModelId(id));
360
+ if (!glm || glm.vision) {
361
+ return false;
362
+ }
363
+ if (glm.variant !== "base" && glm.variant !== "air" && glm.variant !== "turbo") {
364
+ return false;
365
+ }
366
+ return semverGte(glm.version, "5.2");
367
+ }
368
+
312
369
  function pickModelDisplayName(model: CursorModelDetailsValue, fallbackId: string): string {
313
370
  const candidates = [model.displayName, model.displayNameShort, model.displayModelId, ...model.aliases, fallbackId];
314
371
  for (const candidate of candidates) {
package/src/hosts.ts CHANGED
@@ -62,6 +62,8 @@ export const KNOWN_HOSTS = {
62
62
  /** NVIDIA NIM (`integrate.api.nvidia.com`). Qwen NIM endpoints take `chat_template_kwargs.enable_thinking`, never top-level `enable_thinking`. */
63
63
  nvidia: { providers: ["nvidia"], urlMarkers: ["integrate.api.nvidia.com"] },
64
64
  moonshotNative: { providers: ["moonshot", "kimi-code"], urlMarkers: ["api.moonshot.ai", "api.kimi.com"] },
65
+ /** Google AI Studio's OpenAI-compatible shim (`/v1beta/openai`) — a subset of chat-completions; rejects `store` with a 400. Native Gemini uses `google-generative-ai` api instead. */
66
+ googleAistudio: { providers: [], urlMarkers: ["generativelanguage.googleapis.com"] },
65
67
  opencode: { providers: ["opencode-go", "opencode-zen"], urlMarkers: ["opencode.ai"] },
66
68
  /** ZenMux's Anthropic-compatible proxy (`zenmux.ai/api/anthropic`) forwards to signature-enforcing Anthropic. */
67
69
  zenmux: { providers: ["zenmux"], urlMarkers: ["zenmux.ai"] },
@@ -14,10 +14,10 @@ const NON_AUTHORITATIVE_RETRY_MS = 5 * 60 * 1000;
14
14
  export type ModelRefreshStrategy = "online" | "offline" | "online-if-uncached";
15
15
 
16
16
  /**
17
- * Hook for loading and mapping models.dev fallback data into canonical model objects.
17
+ * Hook for loading and mapping stencil.so fallback data into canonical model objects.
18
18
  */
19
19
  export interface ModelsDevFallback<TApi extends Api = Api, TPayload = unknown> {
20
- /** Fetches raw fallback payload (for example from models.dev). */
20
+ /** Fetches raw fallback payload (for example from stencil.so). */
21
21
  fetch(): Promise<TPayload>;
22
22
  /** Maps payload into provider models. */
23
23
  map(payload: TPayload, providerId: Provider): readonly ModelSpec<TApi>[];
@@ -39,7 +39,7 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
39
39
  cacheTtlMs?: number;
40
40
  /** When true, a successful dynamic fetch is the complete provider catalog and prunes static-only models. */
41
41
  dynamicModelsAuthoritative?: boolean;
42
- /** Cached model ids to ignore when the cache was written against a different static catalog fingerprint. */
42
+ /** Cached model ids whose presence forces refresh when the static or migration-policy fingerprint changes. */
43
43
  dropCachedModelIdsOnStaticMismatch?: readonly string[];
44
44
  /**
45
45
  * Trusted, provider-wide request headers (compile-time constants, never
@@ -51,7 +51,7 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
51
51
  restorableHeaderFallback?: Record<string, string>;
52
52
  /** Optional dynamic endpoint fetcher. */
53
53
  fetchDynamicModels?: () => Promise<readonly ModelSpec<TApi>[] | null>;
54
- /** Optional models.dev fallback hook. */
54
+ /** Optional stencil.so fallback hook. */
55
55
  modelsDev?: ModelsDevFallback<TApi, TModelsDevPayload>;
56
56
  /** Clock override for deterministic tests. */
57
57
  now?: () => number;
@@ -171,7 +171,7 @@ function restoreCachedModelHeaders<TApi extends Api>(
171
171
 
172
172
  /**
173
173
  * Resolves provider models with source precedence:
174
- * static -> models.dev -> cache -> dynamic.
174
+ * static -> stencil.so -> cache -> dynamic.
175
175
  *
176
176
  * Later sources override earlier ones by model id.
177
177
  */
@@ -199,10 +199,24 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
199
199
  const usableCachedModels = restoredCache.models.filter(model => !restoredCache.unresolvedModelIds.has(model.id));
200
200
  const cacheHasUnresolvedHeaders = restoredCache.unresolvedModelIds.size > 0;
201
201
  const dynamicModelsAuthoritative = options.dynamicModelsAuthoritative ?? false;
202
- const staticFingerprint = fingerprintStatic(staticModels, dynamicModelsAuthoritative);
202
+ const cacheDropIds = options.dropCachedModelIdsOnStaticMismatch;
203
+ const staticCatalogFingerprint = fingerprintStatic(staticModels, dynamicModelsAuthoritative);
204
+ // Endpoint-migration policy is cache identity: adding an id must invalidate
205
+ // matching-static-catalog caches written by the prior resolver.
206
+ const staticFingerprint =
207
+ cacheDropIds && cacheDropIds.length > 0
208
+ ? `${staticCatalogFingerprint}:drop:${Bun.hash(cacheDropIds.join("\0")).toString(36)}`
209
+ : staticCatalogFingerprint;
203
210
  const cacheFingerprintMatches = cache?.staticFingerprint === staticFingerprint && staticFingerprint.length > 0;
211
+ const cacheNeedsModelMigration =
212
+ !cacheFingerprintMatches &&
213
+ cacheDropIds !== undefined &&
214
+ usableCachedModels.some(model => cacheDropIds.includes(model.id));
204
215
  const hasUsableFreshCache =
205
- (cache?.fresh ?? false) && !cacheHasUnresolvedHeaders && (!dynamicModelsAuthoritative || cacheFingerprintMatches);
216
+ (cache?.fresh ?? false) &&
217
+ !cacheHasUnresolvedHeaders &&
218
+ !cacheNeedsModelMigration &&
219
+ (!dynamicModelsAuthoritative || cacheFingerprintMatches);
206
220
  const dynamicFetcher = options.fetchDynamicModels;
207
221
  const hasDynamicFetcher = typeof dynamicFetcher === "function";
208
222
  const hasAuthoritativeCache = ((cache?.authoritative ?? false) && hasUsableFreshCache) || !hasDynamicFetcher;
@@ -484,13 +498,25 @@ function mergeDynamicModel<TApi extends Api>(existingModel: Model<TApi>, dynamic
484
498
  const supportsImage = dynamicInputAuthoritative
485
499
  ? dynamicModel.input.includes("image")
486
500
  : existingModel.input.includes("image") || dynamicModel.input.includes("image");
501
+ // Synthetic's discovery is authoritative (`dynamicModelsAuthoritative`) and
502
+ // its per-model `reasoning_parameters.efforts` vocabulary is the route's
503
+ // whole truth: when the wire advertises only the `none` off-state the
504
+ // mapper emits `reasoning: false`, and OR-ing the bundled reference's
505
+ // stale `reasoning: true` back would re-arm an effort dial the route
506
+ // doesn't expose. Other providers keep the OR so a bundled reasoning flag
507
+ // survives a discovery row that simply omits the capability.
508
+ const dynamicReasoningAuthoritative =
509
+ existingModel.provider === "synthetic" && dynamicModel.provider === "synthetic";
510
+ const reasoning = dynamicReasoningAuthoritative
511
+ ? dynamicModel.reasoning
512
+ : existingModel.reasoning || dynamicModel.reasoning;
487
513
  // Re-build from spec stage: sparse compat comes from `compatConfig` (the
488
514
  // verbatim override vocabulary), never the resolved `compat` record.
489
515
  return buildModel({
490
516
  ...existingModel,
491
517
  ...dynamicModel,
492
518
  name: preferDiscoveryName(dynamicModel.name, existingModel.name, dynamicModel.id),
493
- reasoning: existingModel.reasoning || dynamicModel.reasoning,
519
+ reasoning,
494
520
  input: supportsImage ? ["text", "image"] : ["text"],
495
521
  cost: {
496
522
  input: preferDiscoveryCost(dynamicModel.cost.input, existingModel.cost.input),