jeopi-catalog 16.5.1 → 16.5.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,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.5.6] - 2026-10-01
6
+
7
+ ### Fixed
8
+
9
+ - Exclude all model request headers from the SQLite model cache and invalidate pre-fix cache rows with SQLite secure deletion enabled. Restore headers only from matching current static models or trusted provider/configuration sources; models with unreconstructible dynamic headers require fresh discovery and are not returned as authenticated offline fallbacks. Adapted from oh-my-pi's header omission, restoration, and Copilot variant fixes.
10
+
11
+ ## [16.5.5] - 2026-09-30
12
+
13
+ ### Fixed
14
+
15
+ - Refresh OpenAI account model lists with current OpenAI metadata from models.dev, including GPT-6 and GPT-6.1, dated snapshots, and fine-tunes; exclude audio, image, and specialized models unsupported by the coding agent.
16
+ - Replace stale OpenAI and Codex model lists after successful discovery and isolate cached lists by endpoint and credentials.
17
+
18
+ ### Added
19
+
20
+ - Support scoped catalog regeneration with repeatable `gen:models --provider <provider-id>` options.
21
+
5
22
  ## [16.4.3] - 2026-07-22
6
23
 
7
24
  ### Fixed
@@ -1,3 +1,4 @@
1
+ import type { FetchImpl } from "jeopi-utils";
1
2
  import type { ModelSpec } from "../types";
2
3
  /**
3
4
  * Fetch options for OpenAI Codex model discovery.
@@ -18,9 +19,9 @@ export interface CodexModelDiscoveryOptions {
18
19
  /** Abort signal for network request cancellation. */
19
20
  signal?: AbortSignal;
20
21
  /** Optional fetch implementation override for tests. */
21
- fetchFn?: typeof fetch;
22
+ fetchFn?: FetchImpl;
22
23
  /** Optional registry fetch implementation override for client version lookup. */
23
- registryFetchFn?: typeof fetch;
24
+ registryFetchFn?: FetchImpl;
24
25
  }
25
26
  /**
26
27
  * Normalized Codex discovery response.
@@ -4,6 +4,10 @@ interface CacheEntry<TApi extends Api = Api> {
4
4
  fresh: boolean;
5
5
  authoritative: boolean;
6
6
  updatedAt: number;
7
+ /** Model ids whose request headers were omitted from disk. */
8
+ headerOmittedModelIds: readonly string[];
9
+ /** Model ids whose headers did not match a trusted local source. */
10
+ unrestorableHeaderModelIds: readonly string[];
7
11
  /**
8
12
  * Hash of the static catalog slice that was merged into `models` when this
9
13
  * row was written. `resolveProviderModels` compares against the current
@@ -13,5 +17,5 @@ interface CacheEntry<TApi extends Api = Api> {
13
17
  staticFingerprint: string;
14
18
  }
15
19
  export declare function readModelCache<TApi extends Api>(providerId: string, ttlMs: number, now: () => number, dbPath?: string): CacheEntry<TApi> | null;
16
- export declare function writeModelCache<TApi extends Api>(providerId: string, updatedAt: number, models: Model<TApi>[], authoritative: boolean, staticFingerprint: string, dbPath?: string): void;
20
+ export declare function writeModelCache<TApi extends Api>(providerId: string, updatedAt: number, models: Model<TApi>[], authoritative: boolean, staticFingerprint: string, dbPath?: string, staticHeaderSources?: readonly Model<TApi>[], restorableHeaderFallback?: Record<string, string>, restoreCachedHeaders?: (model: Model<Api>) => Record<string, string> | undefined): void;
17
21
  export {};
@@ -30,6 +30,17 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
30
30
  dynamicModelsAuthoritative?: boolean;
31
31
  /** Cached model ids to ignore when the cache was written against a different static catalog fingerprint. */
32
32
  dropCachedModelIdsOnStaticMismatch?: readonly string[];
33
+ /** Trusted provider-wide headers restored locally, never persisted. */
34
+ restorableHeaderFallback?: Record<string, string>;
35
+ /**
36
+ * Reconstruct the complete header set from authoritative local configuration.
37
+ * Takes `Model<Api>` (not `Model<TApi>`): the parameter position is
38
+ * contravariant, so binding it to `TApi` would make `ModelManagerOptions<TApi>`
39
+ * invariant in `TApi` and break narrow-to-`ModelManagerOptions<Api>` assignment
40
+ * at every per-provider descriptor factory. Implementations needing identity
41
+ * use `model.id` / `model.requestModelId`, both present regardless of `api`.
42
+ */
43
+ restoreCachedHeaders?: (model: Model<Api>) => Record<string, string> | undefined;
33
44
  /** Optional dynamic endpoint fetcher. */
34
45
  fetchDynamicModels?: () => Promise<readonly ModelSpec<TApi>[] | null>;
35
46
  /** Optional models.dev fallback hook. */
@@ -59,6 +70,11 @@ export interface ModelManager<TApi extends Api = Api> {
59
70
  * Creates a reusable provider model manager.
60
71
  */
61
72
  export declare function createModelManager<TApi extends Api = Api, TModelsDevPayload = unknown>(options: ModelManagerOptions<TApi, TModelsDevPayload>): ModelManager<TApi>;
73
+ /** Restore omitted headers only from trusted local sources; unresolved models must not be used. */
74
+ export declare function restoreCachedModelHeaders<TApi extends Api>(cachedModels: readonly ModelSpec<TApi>[], staticModels: readonly Model<TApi>[], headerOmittedModelIds: readonly string[], unrestorableHeaderModelIds: readonly string[], restorableHeaderFallback?: Record<string, string>, restoreCachedHeaders?: (model: Model<Api>) => Record<string, string> | undefined): {
75
+ models: Model<TApi>[];
76
+ unresolvedModelIds: ReadonlySet<string>;
77
+ };
62
78
  /**
63
79
  * Resolves provider models with source precedence:
64
80
  * static -> models.dev -> cache -> dynamic.
@@ -5,6 +5,8 @@ export interface OpenAICodexModelManagerConfig {
5
5
  accessToken?: string;
6
6
  accountId?: string;
7
7
  clientVersion?: string;
8
+ baseUrl?: string;
9
+ fetch?: FetchImpl;
8
10
  }
9
11
  export declare function openaiCodexModelManagerOptions(config?: OpenAICodexModelManagerConfig): ModelManagerOptions<"openai-codex-responses">;
10
12
  export interface CursorModelManagerConfig {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "jeopi-catalog",
4
- "version": "16.5.1",
4
+ "version": "16.5.6",
5
5
  "description": "Model catalog for jeopi: bundled model database, provider discovery descriptors, model identity, classification, and equivalence",
6
6
  "homepage": "https://github.com/akillness/jeopi",
7
7
  "author": "Can Boluk",
@@ -34,12 +34,12 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@bufbuild/protobuf": "^2.12.0",
37
- "jeopi-utils": "16.5.1",
37
+ "jeopi-utils": "16.5.6",
38
38
  "arktype": "2.2.3",
39
39
  "zod": "^4"
40
40
  },
41
41
  "devDependencies": {
42
- "jeopi-ai": "16.5.1",
42
+ "jeopi-ai": "16.5.6",
43
43
  "@types/bun": "^1.3.14"
44
44
  },
45
45
  "engines": {
@@ -62,9 +62,9 @@ export interface CodexModelDiscoveryOptions {
62
62
  /** Abort signal for network request cancellation. */
63
63
  signal?: AbortSignal;
64
64
  /** Optional fetch implementation override for tests. */
65
- fetchFn?: typeof fetch;
65
+ fetchFn?: FetchImpl;
66
66
  /** Optional registry fetch implementation override for client version lookup. */
67
- registryFetchFn?: typeof fetch;
67
+ registryFetchFn?: FetchImpl;
68
68
  }
69
69
 
70
70
  /**
@@ -6,16 +6,10 @@ import { Database } from "bun:sqlite";
6
6
  import { getModelDbPath } from "jeopi-utils";
7
7
  import type { Api, Model, ModelSpec } from "./types";
8
8
 
9
- // Rows persist ModelSpec JSON (sparse `compat`, never the resolved record);
10
- // the model manager rebuilds via `buildModel` on load. v8 invalidates Codex
11
- // discovery rows predating provider-native V2 compaction metadata; v7
12
- // invalidated rows predating the Antigravity Gemini budget-mode migration
13
- // (cached specs still carrying `thinking.mode: "google-level"` and the old
14
- // 3.5-flash effort routing); v6 invalidated rows that may contain the retired
15
- // unknown-limit sentinels (222222/8888); v5 invalidated rows predating
16
- // effort-tier variant collapsing (raw `-low`/`-high`/`-thinking` member ids);
17
- // v4 dropped the pre-efforts ThinkingConfig shape.
18
- const CACHE_SCHEMA_VERSION = 8;
9
+ // Rows persist sparse ModelSpec metadata, never request headers: arbitrary
10
+ // provider-defined header names may carry credentials. v9 records restoration
11
+ // provenance and invalidates older rows, including v8 credential-bearing rows.
12
+ const CACHE_SCHEMA_VERSION = 9;
19
13
 
20
14
  interface CacheRow {
21
15
  provider_id: string;
@@ -24,6 +18,8 @@ interface CacheRow {
24
18
  authoritative: number;
25
19
  static_fingerprint: string;
26
20
  models: string;
21
+ header_omitted_model_ids: string;
22
+ unrestorable_header_model_ids: string;
27
23
  }
28
24
 
29
25
  interface TableInfoRow {
@@ -35,6 +31,10 @@ interface CacheEntry<TApi extends Api = Api> {
35
31
  fresh: boolean;
36
32
  authoritative: boolean;
37
33
  updatedAt: number;
34
+ /** Model ids whose request headers were omitted from disk. */
35
+ headerOmittedModelIds: readonly string[];
36
+ /** Model ids whose headers did not match a trusted local source. */
37
+ unrestorableHeaderModelIds: readonly string[];
38
38
  /**
39
39
  * Hash of the static catalog slice that was merged into `models` when this
40
40
  * row was written. `resolveProviderModels` compares against the current
@@ -52,6 +52,8 @@ function openDb(resolvedPath: string): Database {
52
52
  // Install the busy handler BEFORE any lock-taking statement. See
53
53
  // https://github.com/can1357/oh-my-pi/issues/2421.
54
54
  db.run("PRAGMA busy_timeout = 3000");
55
+ // Scrub deleted legacy credential-bearing cells rather than retaining them in free pages.
56
+ db.run("PRAGMA secure_delete = ON");
55
57
  db.run("PRAGMA journal_mode = WAL");
56
58
  db.run(`
57
59
  CREATE TABLE IF NOT EXISTS model_cache (
@@ -60,6 +62,8 @@ function openDb(resolvedPath: string): Database {
60
62
  updated_at INTEGER NOT NULL,
61
63
  authoritative INTEGER NOT NULL DEFAULT 0,
62
64
  static_fingerprint TEXT NOT NULL DEFAULT '',
65
+ header_omitted_model_ids TEXT NOT NULL DEFAULT '[]',
66
+ unrestorable_header_model_ids TEXT NOT NULL DEFAULT '[]',
63
67
  models TEXT NOT NULL
64
68
  )
65
69
  `);
@@ -98,6 +102,12 @@ function migrateCacheSchema(db: Database): void {
98
102
  if (!columns.some(column => column.name === "static_fingerprint")) {
99
103
  db.run("ALTER TABLE model_cache ADD COLUMN static_fingerprint TEXT NOT NULL DEFAULT ''");
100
104
  }
105
+ if (!columns.some(column => column.name === "header_omitted_model_ids")) {
106
+ db.run("ALTER TABLE model_cache ADD COLUMN header_omitted_model_ids TEXT NOT NULL DEFAULT '[]'");
107
+ }
108
+ if (!columns.some(column => column.name === "unrestorable_header_model_ids")) {
109
+ db.run("ALTER TABLE model_cache ADD COLUMN unrestorable_header_model_ids TEXT NOT NULL DEFAULT '[]'");
110
+ }
101
111
  } finally {
102
112
  stmt.finalize();
103
113
  }
@@ -124,6 +134,15 @@ export function readModelCache<TApi extends Api>(
124
134
  return null;
125
135
  }
126
136
  const models = JSON.parse(row.models) as ModelSpec<TApi>[];
137
+ const headerOmittedModelIds: unknown = JSON.parse(row.header_omitted_model_ids);
138
+ const unrestorableHeaderModelIds: unknown = JSON.parse(row.unrestorable_header_model_ids);
139
+ if (
140
+ !Array.isArray(headerOmittedModelIds) ||
141
+ !headerOmittedModelIds.every(id => typeof id === "string") ||
142
+ !Array.isArray(unrestorableHeaderModelIds) ||
143
+ !unrestorableHeaderModelIds.every(id => typeof id === "string")
144
+ )
145
+ return null;
127
146
  const ageMs = now() - row.updated_at;
128
147
  const fresh = Number.isFinite(ageMs) && ageMs >= 0 && ageMs <= ttlMs;
129
148
  return {
@@ -131,6 +150,8 @@ export function readModelCache<TApi extends Api>(
131
150
  fresh,
132
151
  authoritative: row.authoritative === 1,
133
152
  updatedAt: row.updated_at,
153
+ headerOmittedModelIds,
154
+ unrestorableHeaderModelIds,
134
155
  staticFingerprint: row.static_fingerprint ?? "",
135
156
  };
136
157
  } finally {
@@ -142,6 +163,14 @@ export function readModelCache<TApi extends Api>(
142
163
  }
143
164
  }
144
165
 
166
+ function headersEqual(left: Record<string, string>, right: Record<string, string> | undefined): boolean {
167
+ if (!right) return false;
168
+ return (
169
+ Object.keys(left).length === Object.keys(right).length &&
170
+ Object.keys(left).every(key => Object.hasOwn(right, key) && left[key] === right[key])
171
+ );
172
+ }
173
+
145
174
  export function writeModelCache<TApi extends Api>(
146
175
  providerId: string,
147
176
  updatedAt: number,
@@ -149,19 +178,44 @@ export function writeModelCache<TApi extends Api>(
149
178
  authoritative: boolean,
150
179
  staticFingerprint: string,
151
180
  dbPath?: string,
181
+ staticHeaderSources: readonly Model<TApi>[] = [],
182
+ restorableHeaderFallback?: Record<string, string>,
183
+ restoreCachedHeaders?: (model: Model<Api>) => Record<string, string> | undefined,
152
184
  ): void {
153
185
  try {
154
186
  withModelCacheDb(dbPath, db => {
187
+ const headerOmittedModelIds: string[] = [];
188
+ const unrestorableHeaderModelIds: string[] = [];
189
+ const staticById = new Map(staticHeaderSources.map(model => [model.id, model]));
190
+ const cachedModels = models.map(model => {
191
+ const { headers, compatConfig, ...metadata } = model;
192
+ if (headers && Object.keys(headers).length > 0) {
193
+ headerOmittedModelIds.push(model.id);
194
+ const source =
195
+ staticById.get(model.id) ?? (model.requestModelId ? staticById.get(model.requestModelId) : undefined);
196
+ if (
197
+ !headersEqual(headers, source?.headers ?? restorableHeaderFallback) &&
198
+ !headersEqual(headers, restoreCachedHeaders?.({ ...model, headers: undefined }))
199
+ ) {
200
+ unrestorableHeaderModelIds.push(model.id);
201
+ }
202
+ }
203
+ return { ...metadata, compat: compatConfig };
204
+ });
155
205
  db.run(
156
- `INSERT OR REPLACE INTO model_cache (provider_id, version, updated_at, authoritative, static_fingerprint, models)
157
- VALUES (?, ?, ?, ?, ?, ?)`,
206
+ `INSERT OR REPLACE INTO model_cache (
207
+ provider_id, version, updated_at, authoritative, static_fingerprint,
208
+ header_omitted_model_ids, unrestorable_header_model_ids, models
209
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
158
210
  [
159
211
  providerId,
160
212
  CACHE_SCHEMA_VERSION,
161
213
  updatedAt,
162
214
  authoritative ? 1 : 0,
163
215
  staticFingerprint,
164
- JSON.stringify(models.map(model => ({ ...model, compat: model.compatConfig, compatConfig: undefined }))),
216
+ JSON.stringify(headerOmittedModelIds),
217
+ JSON.stringify(unrestorableHeaderModelIds),
218
+ JSON.stringify(cachedModels),
165
219
  ],
166
220
  );
167
221
  });
@@ -41,6 +41,17 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
41
41
  dynamicModelsAuthoritative?: boolean;
42
42
  /** Cached model ids to ignore when the cache was written against a different static catalog fingerprint. */
43
43
  dropCachedModelIdsOnStaticMismatch?: readonly string[];
44
+ /** Trusted provider-wide headers restored locally, never persisted. */
45
+ restorableHeaderFallback?: Record<string, string>;
46
+ /**
47
+ * Reconstruct the complete header set from authoritative local configuration.
48
+ * Takes `Model<Api>` (not `Model<TApi>`): the parameter position is
49
+ * contravariant, so binding it to `TApi` would make `ModelManagerOptions<TApi>`
50
+ * invariant in `TApi` and break narrow-to-`ModelManagerOptions<Api>` assignment
51
+ * at every per-provider descriptor factory. Implementations needing identity
52
+ * use `model.id` / `model.requestModelId`, both present regardless of `api`.
53
+ */
54
+ restoreCachedHeaders?: (model: Model<Api>) => Record<string, string> | undefined;
44
55
  /** Optional dynamic endpoint fetcher. */
45
56
  fetchDynamicModels?: () => Promise<readonly ModelSpec<TApi>[] | null>;
46
57
  /** Optional models.dev fallback hook. */
@@ -101,6 +112,37 @@ function passModelList<TApi extends Api>(value: unknown): Model<TApi>[] {
101
112
  return out;
102
113
  }
103
114
 
115
+ /** Restore omitted headers only from trusted local sources; unresolved models must not be used. */
116
+ export function restoreCachedModelHeaders<TApi extends Api>(
117
+ cachedModels: readonly ModelSpec<TApi>[],
118
+ staticModels: readonly Model<TApi>[],
119
+ headerOmittedModelIds: readonly string[],
120
+ unrestorableHeaderModelIds: readonly string[],
121
+ restorableHeaderFallback?: Record<string, string>,
122
+ restoreCachedHeaders?: (model: Model<Api>) => Record<string, string> | undefined,
123
+ ): { models: Model<TApi>[]; unresolvedModelIds: ReadonlySet<string> } {
124
+ const models = passModelList<TApi>(cachedModels);
125
+ const unresolvedModelIds = new Set<string>();
126
+ if (headerOmittedModelIds.length === 0) return { models, unresolvedModelIds };
127
+ const omittedIds = new Set(headerOmittedModelIds);
128
+ const unrestorableIds = new Set(unrestorableHeaderModelIds);
129
+ const staticById = new Map(staticModels.map(model => [model.id, model]));
130
+ const restored = models.map(model => {
131
+ if (!omittedIds.has(model.id)) return model;
132
+ const source =
133
+ staticById.get(model.id) ?? (model.requestModelId ? staticById.get(model.requestModelId) : undefined);
134
+ const headers = unrestorableIds.has(model.id)
135
+ ? undefined
136
+ : (source?.headers ?? restorableHeaderFallback ?? restoreCachedHeaders?.(model));
137
+ if (!headers) {
138
+ unresolvedModelIds.add(model.id);
139
+ return model;
140
+ }
141
+ return { ...model, headers };
142
+ });
143
+ return { models: restored, unresolvedModelIds };
144
+ }
145
+
104
146
  /**
105
147
  * Resolves provider models with source precedence:
106
148
  * static -> models.dev -> cache -> dynamic.
@@ -119,10 +161,21 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
119
161
  ? passModelList<TApi>(options.staticModels)
120
162
  : (getBundledModels(options.providerId as GeneratedProvider) as Model<TApi>[]);
121
163
  const cache = readModelCache<TApi>(cacheProviderId, ttlMs, now, dbPath);
164
+ const restoredCache = restoreCachedModelHeaders(
165
+ cache?.models ?? [],
166
+ staticModels,
167
+ cache?.headerOmittedModelIds ?? [],
168
+ cache?.unrestorableHeaderModelIds ?? [],
169
+ options.restorableHeaderFallback,
170
+ options.restoreCachedHeaders,
171
+ );
172
+ const usableCachedModels = restoredCache.models.filter(model => !restoredCache.unresolvedModelIds.has(model.id));
173
+ const cacheHasUnresolvedHeaders = restoredCache.unresolvedModelIds.size > 0;
122
174
  const dynamicModelsAuthoritative = options.dynamicModelsAuthoritative ?? false;
123
175
  const staticFingerprint = fingerprintStatic(staticModels, dynamicModelsAuthoritative);
124
176
  const cacheFingerprintMatches = cache?.staticFingerprint === staticFingerprint && staticFingerprint.length > 0;
125
- const hasUsableFreshCache = (cache?.fresh ?? false) && (!dynamicModelsAuthoritative || cacheFingerprintMatches);
177
+ const hasUsableFreshCache =
178
+ (cache?.fresh ?? false) && !cacheHasUnresolvedHeaders && (!dynamicModelsAuthoritative || cacheFingerprintMatches);
126
179
  const dynamicFetcher = options.fetchDynamicModels;
127
180
  const hasDynamicFetcher = typeof dynamicFetcher === "function";
128
181
  const hasAuthoritativeCache = ((cache?.authoritative ?? false) && hasUsableFreshCache) || !hasDynamicFetcher;
@@ -139,8 +192,14 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
139
192
  // was merged in last time, the cache row IS the authoritative merge result.
140
193
  // Re-running `mergeDynamicModels(static, cache)` would just rebuild the same
141
194
  // objects (~800ms in the steady-state cold-start profile for `omp -p hi`).
142
- if (!shouldFetchFromNetwork && cache?.fresh && hasAuthoritativeCache && cacheFingerprintMatches) {
143
- return { models: collapseBuiltModelVariants(passModelList<TApi>(cache.models)), stale: false };
195
+ if (
196
+ !shouldFetchFromNetwork &&
197
+ cache?.fresh &&
198
+ hasAuthoritativeCache &&
199
+ cacheFingerprintMatches &&
200
+ !cacheHasUnresolvedHeaders
201
+ ) {
202
+ return { models: collapseBuiltModelVariants(restoredCache.models), stale: false };
144
203
  }
145
204
 
146
205
  const [fetchedModelsDevModels, fetchedDynamicModels] = shouldFetchFromNetwork
@@ -153,7 +212,7 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
153
212
  const cacheModels = dynamicFetchSucceeded
154
213
  ? []
155
214
  : prepareCacheModelsForStaticMismatch(
156
- normalizeModelList<TApi>(cache?.models ?? []),
215
+ usableCachedModels,
157
216
  staticModels,
158
217
  cacheFingerprintMatches,
159
218
  options.dropCachedModelIdsOnStaticMismatch,
@@ -162,7 +221,10 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
162
221
  const mergedWithCache = mergeDynamicModels(mergeModelSources(staticModels, modelsDevModels), cacheModels);
163
222
  const mergedModels = mergeDynamicModels(mergedWithCache, dynamicModels);
164
223
  const models = collapseBuiltModelVariants(
165
- dynamicModelsAuthoritative && dynamicFetchSucceeded ? retainModelIds(mergedModels, dynamicModels) : mergedModels,
224
+ (dynamicModelsAuthoritative && dynamicFetchSucceeded
225
+ ? retainModelIds(mergedModels, dynamicModels)
226
+ : mergedModels
227
+ ).filter(model => dynamicFetchSucceeded || !restoredCache.unresolvedModelIds.has(model.id)),
166
228
  );
167
229
  const dynamicAuthoritative = !hasDynamicFetcher || dynamicFetchSucceeded || shouldUseFreshCacheAsAuthoritative;
168
230
  if (shouldFetchFromNetwork) {
@@ -178,11 +240,25 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
178
240
  true,
179
241
  staticFingerprint,
180
242
  dbPath,
243
+ staticModels,
244
+ options.restorableHeaderFallback,
245
+ options.restoreCachedHeaders,
181
246
  );
182
247
  } else {
183
248
  // Dynamic fetch failed — update cache with a non-authoritative snapshot so
184
249
  // stale state remains visible while retry backoff still applies.
185
250
  const latestCache = readModelCache<TApi>(cacheProviderId, ttlMs, now, dbPath);
251
+ const latestRestoredCache = restoreCachedModelHeaders(
252
+ latestCache?.models ?? cache?.models ?? [],
253
+ staticModels,
254
+ latestCache?.headerOmittedModelIds ?? cache?.headerOmittedModelIds ?? [],
255
+ latestCache?.unrestorableHeaderModelIds ?? cache?.unrestorableHeaderModelIds ?? [],
256
+ options.restorableHeaderFallback,
257
+ options.restoreCachedHeaders,
258
+ );
259
+ // Dropping unresolved rows would also erase the evidence that a same-id
260
+ // static model lacks required headers. Keep that provenance until discovery succeeds.
261
+ if (latestRestoredCache.unresolvedModelIds.size > 0) return { models, stale: true };
186
262
  writeModelCache(
187
263
  cacheProviderId,
188
264
  now(),
@@ -190,16 +266,19 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
190
266
  mergeDynamicModels(
191
267
  mergeModelSources(staticModels, modelsDevModels),
192
268
  prepareCacheModelsForStaticMismatch(
193
- normalizeModelList<TApi>(latestCache?.models ?? cache?.models ?? []),
269
+ latestRestoredCache.models.filter(model => !latestRestoredCache.unresolvedModelIds.has(model.id)),
194
270
  staticModels,
195
271
  cacheFingerprintMatches,
196
272
  options.dropCachedModelIdsOnStaticMismatch,
197
273
  ),
198
- ),
274
+ ).filter(model => !latestRestoredCache.unresolvedModelIds.has(model.id)),
199
275
  ),
200
276
  false,
201
277
  staticFingerprint,
202
278
  dbPath,
279
+ staticModels,
280
+ options.restorableHeaderFallback,
281
+ options.restoreCachedHeaders,
203
282
  );
204
283
  }
205
284
  }
@@ -513,6 +513,9 @@ function impliesMandatoryReasoning(parsed: ParsedModel, modelId: string): boolea
513
513
  }
514
514
  if (isMinimaxM2FamilyModelId(modelId)) return true;
515
515
  if (OPENAI_O_SERIES_RE.test(bareModelId(modelId))) return true;
516
+ // These GPT-6 models reject "none"; Sol/Luna 6.0 still accept it.
517
+ const canonicalOpenAIId = bareModelId(modelId).replace(/^ft:/, "").split(":")[0];
518
+ if (/^gpt-(?:6-astra|6\.1-sol)(?:$|-)/i.test(canonicalOpenAIId)) return true;
516
519
  return findThinkingVariantToken(modelId) !== undefined;
517
520
  }
518
521