@oh-my-pi/pi-catalog 18.2.0 → 18.2.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 +27 -0
- package/dist/types/compat/behavior.d.ts +11 -0
- package/dist/types/compat/types.d.ts +21 -0
- package/dist/types/discovery/antigravity.d.ts +10 -1
- package/dist/types/model-thinking.d.ts +7 -0
- package/dist/types/provider-models/openai-compat.d.ts +2 -0
- package/dist/types/types.d.ts +26 -1
- package/package.json +4 -4
- package/src/compat/axes.ts +14 -0
- package/src/compat/behavior.ts +22 -2
- package/src/compat/cascade.ts +3 -1
- package/src/compat/context-window.ts +11 -1
- package/src/compat/resolve.ts +42 -16
- package/src/compat/rules/README.md +3 -1
- package/src/compat/rules/classes/deepseek.kdl +9 -1
- package/src/compat/rules/classes/kimi.kdl +6 -0
- package/src/compat/rules/providers/alibaba-token-plan.kdl +16 -8
- package/src/compat/rules/providers/amazon-bedrock.kdl +30 -0
- package/src/compat/rules/providers/azure.kdl +6 -0
- package/src/compat/rules/providers/cerebras.kdl +10 -0
- package/src/compat/rules/providers/commandcode.kdl +20 -4
- package/src/compat/rules/providers/cursor.kdl +32 -0
- package/src/compat/rules/providers/deepseek.kdl +5 -5
- package/src/compat/rules/providers/google-vertex.kdl +15 -0
- package/src/compat/rules/providers/meta.kdl +3 -0
- package/src/compat/rules/providers/muse-code.kdl +3 -0
- package/src/compat/rules/providers/openrouter.kdl +6 -0
- package/src/compat/rules/runtime/behavior.kdl +17 -0
- package/src/compat/rules/taxonomy/deepseek.kdl +5 -0
- package/src/compat/rules.json +1 -1
- package/src/compat/types.ts +23 -0
- package/src/discovery/antigravity.ts +80 -43
- package/src/identity/bundled.ts +4 -3
- package/src/model-cache.ts +115 -37
- package/src/model-thinking.ts +10 -7
- package/src/models.json +1 -1
- package/src/provider-models/bundled-references.ts +4 -3
- package/src/provider-models/cache-provider-id.ts +14 -8
- package/src/provider-models/ollama.ts +11 -31
- package/src/provider-models/openai-compat.ts +94 -24
- package/src/types.ts +28 -0
package/src/compat/types.ts
CHANGED
|
@@ -293,6 +293,12 @@ export interface CompiledExcludeModels {
|
|
|
293
293
|
match: CompiledMatchList;
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
/** Exact upstream discovery modes excluded from one provider's coding-model roster. */
|
|
297
|
+
export interface CompiledExcludeDiscoveryModes {
|
|
298
|
+
provider: string;
|
|
299
|
+
modes: string[];
|
|
300
|
+
}
|
|
301
|
+
|
|
296
302
|
/** Provider plan-requirement tiers keyed by matcher token lists. */
|
|
297
303
|
export interface CompiledPlanRequirement {
|
|
298
304
|
provider: string;
|
|
@@ -306,6 +312,12 @@ export interface CompiledPricingPeer {
|
|
|
306
312
|
aliases: { model: string; peerId: string }[];
|
|
307
313
|
}
|
|
308
314
|
|
|
315
|
+
/** Provider timezone assumption for offset-less absolute retry-reset timestamps. */
|
|
316
|
+
export interface CompiledRetryResetTimezone {
|
|
317
|
+
provider: string;
|
|
318
|
+
offset: string;
|
|
319
|
+
}
|
|
320
|
+
|
|
309
321
|
/** Compiled runtime behavior vocabulary (`runtime/behavior.kdl`). */
|
|
310
322
|
export interface CompiledBehavior {
|
|
311
323
|
openaiResponsesHeuristic?: CompiledResponsesHeuristic;
|
|
@@ -316,10 +328,13 @@ export interface CompiledBehavior {
|
|
|
316
328
|
hostedDefaults: CompiledHostedDefault[];
|
|
317
329
|
apiRoutes: CompiledApiRoutes[];
|
|
318
330
|
modelLimits: CompiledModelLimits[];
|
|
331
|
+
excludeDiscoveryModes: CompiledExcludeDiscoveryModes[];
|
|
319
332
|
excludeModels: CompiledExcludeModels[];
|
|
320
333
|
planRequirements: CompiledPlanRequirement[];
|
|
321
334
|
pricingPeers: CompiledPricingPeer[];
|
|
335
|
+
retryResetTimezones: CompiledRetryResetTimezone[];
|
|
322
336
|
retiredProviders: string[];
|
|
337
|
+
referenceIsolatedProviders: string[];
|
|
323
338
|
}
|
|
324
339
|
|
|
325
340
|
/**
|
|
@@ -672,4 +687,12 @@ export interface ResolvedAxes {
|
|
|
672
687
|
wire: Record<string, unknown>;
|
|
673
688
|
thinking: Record<string, unknown>;
|
|
674
689
|
catalog: Record<string, unknown>;
|
|
690
|
+
/**
|
|
691
|
+
* Reasoning capability after the exact-model effort upgrade: `true` when the
|
|
692
|
+
* target reported reasoning or an exact rule declares a ladder for it (the
|
|
693
|
+
* reviewed correction to metadata-less discovery rows). Compat resolvers
|
|
694
|
+
* read this instead of the raw spec flag, or one id resolves two different
|
|
695
|
+
* wire contracts depending on whether it came from discovery or the bake.
|
|
696
|
+
*/
|
|
697
|
+
reasoning: boolean;
|
|
675
698
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type } from "@oh-my-pi/omptype";
|
|
2
|
+
import type { FetchImpl } from "@oh-my-pi/pi-utils";
|
|
2
3
|
import { collapseVariants, type VariantCollapseTable } from "../compat/collapse";
|
|
3
4
|
import type { ModelSpec } from "../types";
|
|
4
5
|
import { discoveryFetch, toPositiveNumber } from "../utils";
|
|
@@ -51,6 +52,7 @@ export interface AntigravityDiscoveryAgentModelSort {
|
|
|
51
52
|
export interface AntigravityDiscoveryApiResponse {
|
|
52
53
|
models?: Record<string, AntigravityDiscoveryApiModel>;
|
|
53
54
|
agentModelSorts?: AntigravityDiscoveryAgentModelSort[];
|
|
55
|
+
imageGenerationModelIds?: string[];
|
|
54
56
|
}
|
|
55
57
|
const AntigravityDiscoveryApiModelSchema = type({
|
|
56
58
|
"displayName?": type("unknown").pipe(value => (typeof value === "string" ? value : undefined)),
|
|
@@ -123,6 +125,9 @@ const AntigravityDiscoveryApiResponseSchema = type({
|
|
|
123
125
|
}
|
|
124
126
|
return result;
|
|
125
127
|
}),
|
|
128
|
+
"imageGenerationModelIds?": type("unknown").pipe(value =>
|
|
129
|
+
Array.isArray(value) ? value.filter((modelId): modelId is string => typeof modelId === "string") : undefined,
|
|
130
|
+
),
|
|
126
131
|
});
|
|
127
132
|
/**
|
|
128
133
|
* Options for fetching Antigravity discovery models.
|
|
@@ -139,7 +144,7 @@ export interface FetchAntigravityDiscoveryModelsOptions {
|
|
|
139
144
|
/** Optional abort signal for request cancellation. */
|
|
140
145
|
signal?: AbortSignal;
|
|
141
146
|
/** Optional fetch implementation override for tests. */
|
|
142
|
-
fetcher?:
|
|
147
|
+
fetcher?: FetchImpl;
|
|
143
148
|
/**
|
|
144
149
|
* Hand collapse table to apply to the discovered list. Defaults to the
|
|
145
150
|
* Antigravity (budget-transport) table; `googleGeminiCli` passes the
|
|
@@ -157,6 +162,78 @@ export interface FetchAntigravityDiscoveryModelsOptions {
|
|
|
157
162
|
export async function fetchAntigravityDiscoveryModels(
|
|
158
163
|
options: FetchAntigravityDiscoveryModelsOptions,
|
|
159
164
|
): Promise<ModelSpec<"google-gemini-cli">[] | null> {
|
|
165
|
+
const discovered = await fetchAntigravityDiscoveryResponse(options);
|
|
166
|
+
if (!discovered) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const models: ModelSpec<"google-gemini-cli">[] = [];
|
|
171
|
+
const apiModels = discovered.payload.models;
|
|
172
|
+
if (apiModels) {
|
|
173
|
+
for (const modelId in apiModels) {
|
|
174
|
+
const model = apiModels[modelId];
|
|
175
|
+
if (ANTIGRAVITY_DISCOVERY_DENYLIST.has(modelId)) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (model.isInternal === true) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const supportsImages = model.supportsImages === true;
|
|
183
|
+
models.push({
|
|
184
|
+
id: modelId,
|
|
185
|
+
name: model.displayName || modelId,
|
|
186
|
+
api: "google-gemini-cli",
|
|
187
|
+
provider: "google-antigravity",
|
|
188
|
+
baseUrl: discovered.endpoint,
|
|
189
|
+
reasoning: model.supportsThinking === true,
|
|
190
|
+
input: supportsImages ? ["text", "image"] : ["text"],
|
|
191
|
+
cost: {
|
|
192
|
+
input: 0,
|
|
193
|
+
output: 0,
|
|
194
|
+
cacheRead: 0,
|
|
195
|
+
cacheWrite: 0,
|
|
196
|
+
},
|
|
197
|
+
contextWindow: toPositiveNumber(model.maxTokens, DEFAULT_CONTEXT_WINDOW),
|
|
198
|
+
maxTokens: toPositiveNumber(model.maxOutputTokens, DEFAULT_MAX_TOKENS),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Collapse effort-tier variants at the source so runtime discovery,
|
|
204
|
+
// the gemini-cli re-provision, and the catalog generator all see
|
|
205
|
+
// logical ids only.
|
|
206
|
+
const collapsed = collapseVariants(
|
|
207
|
+
models,
|
|
208
|
+
options.collapseTable === undefined ? undefined : { table: options.collapseTable },
|
|
209
|
+
);
|
|
210
|
+
collapsed.sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
|
|
211
|
+
return collapsed;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Advertised image model and serving endpoint for one Antigravity account. */
|
|
215
|
+
export interface AntigravityImageModel {
|
|
216
|
+
id: string;
|
|
217
|
+
endpoint: string;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Resolves the first image-generation model advertised by an Antigravity account. */
|
|
221
|
+
export async function fetchAntigravityImageModel(
|
|
222
|
+
options: FetchAntigravityDiscoveryModelsOptions,
|
|
223
|
+
): Promise<AntigravityImageModel | null> {
|
|
224
|
+
const discovered = await fetchAntigravityDiscoveryResponse(options);
|
|
225
|
+
const id = discovered?.payload.imageGenerationModelIds?.find(modelId => modelId.length > 0);
|
|
226
|
+
return id && discovered ? { id, endpoint: discovered.endpoint } : null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
interface AntigravityDiscoveryResponse {
|
|
230
|
+
payload: AntigravityDiscoveryApiResponse;
|
|
231
|
+
endpoint: string;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function fetchAntigravityDiscoveryResponse(
|
|
235
|
+
options: FetchAntigravityDiscoveryModelsOptions,
|
|
236
|
+
): Promise<AntigravityDiscoveryResponse | null> {
|
|
160
237
|
if (options.userAgent === undefined) {
|
|
161
238
|
await ensureAntigravityVersion(options.fetcher ?? fetch, options.signal);
|
|
162
239
|
}
|
|
@@ -195,49 +272,9 @@ export async function fetchAntigravityDiscoveryModels(
|
|
|
195
272
|
}
|
|
196
273
|
|
|
197
274
|
const parsed = parseAntigravityDiscoveryResponse(payload);
|
|
198
|
-
if (
|
|
199
|
-
|
|
275
|
+
if (parsed) {
|
|
276
|
+
return { payload: parsed, endpoint };
|
|
200
277
|
}
|
|
201
|
-
|
|
202
|
-
const models: ModelSpec<"google-gemini-cli">[] = [];
|
|
203
|
-
|
|
204
|
-
for (const [modelId, model] of Object.entries(parsed.models ?? {})) {
|
|
205
|
-
if (ANTIGRAVITY_DISCOVERY_DENYLIST.has(modelId)) {
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
if (model.isInternal === true) {
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const supportsImages = model.supportsImages === true;
|
|
213
|
-
models.push({
|
|
214
|
-
id: modelId,
|
|
215
|
-
name: model.displayName || modelId,
|
|
216
|
-
api: "google-gemini-cli",
|
|
217
|
-
provider: "google-antigravity",
|
|
218
|
-
baseUrl: endpoint,
|
|
219
|
-
reasoning: model.supportsThinking === true,
|
|
220
|
-
input: supportsImages ? ["text", "image"] : ["text"],
|
|
221
|
-
cost: {
|
|
222
|
-
input: 0,
|
|
223
|
-
output: 0,
|
|
224
|
-
cacheRead: 0,
|
|
225
|
-
cacheWrite: 0,
|
|
226
|
-
},
|
|
227
|
-
contextWindow: toPositiveNumber(model.maxTokens, DEFAULT_CONTEXT_WINDOW),
|
|
228
|
-
maxTokens: toPositiveNumber(model.maxOutputTokens, DEFAULT_MAX_TOKENS),
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Collapse effort-tier variants at the source so runtime discovery,
|
|
233
|
-
// the gemini-cli re-provision, and the catalog generator all see
|
|
234
|
-
// logical ids only.
|
|
235
|
-
const collapsed = collapseVariants(
|
|
236
|
-
models,
|
|
237
|
-
options.collapseTable === undefined ? undefined : { table: options.collapseTable },
|
|
238
|
-
);
|
|
239
|
-
collapsed.sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
|
|
240
|
-
return collapsed;
|
|
241
278
|
}
|
|
242
279
|
|
|
243
280
|
return null;
|
package/src/identity/bundled.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* non-bundled reference data use the pure builder directly
|
|
7
7
|
* ({@link buildModelReferenceIndex}).
|
|
8
8
|
*/
|
|
9
|
+
import { isBareIdReferenceProvider } from "../compat/behavior";
|
|
9
10
|
import { getBundledModels, getBundledProviders } from "../models";
|
|
10
11
|
import type { Api, Model } from "../types";
|
|
11
12
|
import { buildModelReferenceIndex, type ModelReferenceIndex } from "./reference";
|
|
@@ -13,9 +14,9 @@ import { buildModelReferenceIndex, type ModelReferenceIndex } from "./reference"
|
|
|
13
14
|
let bundledModels: readonly Model<Api>[] | undefined;
|
|
14
15
|
|
|
15
16
|
function getBundledModelList(): readonly Model<Api>[] {
|
|
16
|
-
bundledModels ??= getBundledProviders()
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
bundledModels ??= getBundledProviders()
|
|
18
|
+
.filter(isBareIdReferenceProvider)
|
|
19
|
+
.flatMap(provider => getBundledModels(provider as Parameters<typeof getBundledModels>[0]) as Model<Api>[]);
|
|
19
20
|
return bundledModels;
|
|
20
21
|
}
|
|
21
22
|
|
package/src/model-cache.ts
CHANGED
|
@@ -31,12 +31,19 @@ const HEADER_RESTORE_VERSION = 1;
|
|
|
31
31
|
* Explicit compatibility gate for materialized rows. Bump whenever buildModel
|
|
32
32
|
* semantics change without an app-version change. The compiled-rules content
|
|
33
33
|
* hash catches every KDL policy edit even when the package version is unchanged;
|
|
34
|
-
*
|
|
34
|
+
* computed lazily on first cache access and memoized, so processes that never
|
|
35
|
+
* touch the model cache skip the stringify entirely.
|
|
35
36
|
*/
|
|
36
37
|
const MODEL_MATERIALIZATION_VERSION = 1;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
let cachedMaterializationPolicy: string | undefined;
|
|
39
|
+
function materializationPolicy(): string {
|
|
40
|
+
if (cachedMaterializationPolicy === undefined) {
|
|
41
|
+
cachedMaterializationPolicy =
|
|
42
|
+
`app-${VERSION}:builder-${MODEL_MATERIALIZATION_VERSION}:rules-${RULES.version}-` +
|
|
43
|
+
Bun.hash(JSON.stringify(RULES)).toString(36);
|
|
44
|
+
}
|
|
45
|
+
return cachedMaterializationPolicy;
|
|
46
|
+
}
|
|
40
47
|
|
|
41
48
|
interface CacheRow {
|
|
42
49
|
provider_id: string;
|
|
@@ -88,6 +95,46 @@ export interface CacheEntry<TApi extends Api = Api> {
|
|
|
88
95
|
let sharedDb: Database | null = null;
|
|
89
96
|
let sharedDbPath: string | null = null;
|
|
90
97
|
|
|
98
|
+
const readRowCache = new Map<string, { dataVersion: number; entry: CacheEntry<Api> | null }>();
|
|
99
|
+
const READ_ROW_CACHE_MAX = 64;
|
|
100
|
+
|
|
101
|
+
function readCacheKey(resolvedPath: string, providerId: string): string {
|
|
102
|
+
return `${resolvedPath} ${providerId}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function dbDataVersion(db: Database): number | null {
|
|
106
|
+
try {
|
|
107
|
+
const row = db.query<{ data_version: number }, []>("PRAGMA data_version").get();
|
|
108
|
+
return typeof row?.data_version === "number" ? row.data_version : null;
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function withFreshness<TApi extends Api>(
|
|
115
|
+
entry: CacheEntry<TApi> | null,
|
|
116
|
+
ttlMs: number,
|
|
117
|
+
now: () => number,
|
|
118
|
+
): CacheEntry<TApi> | null {
|
|
119
|
+
if (entry === null) return null;
|
|
120
|
+
const ageMs = now() - entry.updatedAt;
|
|
121
|
+
const fresh = Number.isFinite(ageMs) && ageMs >= 0 && ageMs <= ttlMs;
|
|
122
|
+
return fresh === entry.fresh ? entry : { ...entry, fresh };
|
|
123
|
+
}
|
|
124
|
+
function invalidateReadRow(providerId: string, dbPath?: string): void {
|
|
125
|
+
try {
|
|
126
|
+
readRowCache.delete(readCacheKey(dbPath ?? getModelDbPath(), providerId));
|
|
127
|
+
} catch {
|
|
128
|
+
// Best-effort only; a missed invalidation just costs one extra parse.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function invalidateReadPath(resolvedPath: string): void {
|
|
133
|
+
for (const key of readRowCache.keys()) {
|
|
134
|
+
if (key.startsWith(`${resolvedPath} `)) readRowCache.delete(key);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
91
138
|
function openDb(resolvedPath: string): Database {
|
|
92
139
|
const db = new Database(resolvedPath, { create: true });
|
|
93
140
|
// Install the busy handler BEFORE any lock-taking statement. See
|
|
@@ -178,6 +225,7 @@ function healCorruptModelCache(resolvedPath: string, shared: boolean, err: unkno
|
|
|
178
225
|
sharedDb = null;
|
|
179
226
|
sharedDbPath = null;
|
|
180
227
|
}
|
|
228
|
+
invalidateReadPath(resolvedPath);
|
|
181
229
|
quarantineCorruptModelCache(resolvedPath);
|
|
182
230
|
const code = err && typeof err === "object" && "code" in err ? err.code : undefined;
|
|
183
231
|
if (reportedCorruptPaths.has(resolvedPath)) {
|
|
@@ -232,7 +280,7 @@ function migrateCacheSchema(db: Database): void {
|
|
|
232
280
|
// compaction path even after CACHE_SCHEMA_VERSION was bumped).
|
|
233
281
|
db.run("DELETE FROM model_cache WHERE version <> ? OR materialization_policy <> ?", [
|
|
234
282
|
CACHE_SCHEMA_VERSION,
|
|
235
|
-
|
|
283
|
+
materializationPolicy(),
|
|
236
284
|
]);
|
|
237
285
|
}
|
|
238
286
|
|
|
@@ -313,7 +361,6 @@ function parseModelIds(serialized: string): string[] | null {
|
|
|
313
361
|
return null;
|
|
314
362
|
}
|
|
315
363
|
}
|
|
316
|
-
|
|
317
364
|
export function readModelCache<TApi extends Api>(
|
|
318
365
|
providerId: string,
|
|
319
366
|
ttlMs: number,
|
|
@@ -321,44 +368,74 @@ export function readModelCache<TApi extends Api>(
|
|
|
321
368
|
dbPath?: string,
|
|
322
369
|
): CacheEntry<TApi> | null {
|
|
323
370
|
try {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
371
|
+
const resolvedPath = dbPath ?? getModelDbPath();
|
|
372
|
+
const key = readCacheKey(resolvedPath, providerId);
|
|
373
|
+
// Monotonic change signal: same-shaped WAL overwrites after checkpoint
|
|
374
|
+
// can leave every size:mtime pair identical, so file metadata alone
|
|
375
|
+
// cannot invalidate. PRAGMA data_version increments on each committed
|
|
376
|
+
// write transaction visible to a new reader.
|
|
377
|
+
const entry = withModelCacheDb(dbPath, db => {
|
|
378
|
+
const dataVersion = dbDataVersion(db);
|
|
379
|
+
if (dataVersion !== null) {
|
|
380
|
+
const cached = readRowCache.get(key);
|
|
381
|
+
if (cached !== undefined && cached.dataVersion === dataVersion) {
|
|
382
|
+
// Freshness is time-relative: recompute per call from the
|
|
383
|
+
// cached row's updatedAt so a long-lived process goes stale.
|
|
384
|
+
return { hit: true as const, entry: withFreshness(cached.entry as CacheEntry<TApi> | null, ttlMs, now) };
|
|
330
385
|
}
|
|
331
|
-
const models = parseMaterializedModels<TApi>(row.models);
|
|
332
|
-
const headerOmittedModelIds = parseModelIds(row.header_omitted_model_ids);
|
|
333
|
-
const unrestorableHeaderModelIds = parseModelIds(row.unrestorable_header_model_ids);
|
|
334
|
-
if (models === null || headerOmittedModelIds === null || unrestorableHeaderModelIds === null) {
|
|
335
|
-
// Fail closed on corrupt header provenance: treating malformed
|
|
336
|
-
// markers as empty could return a model with required credentials
|
|
337
|
-
// silently absent. secure_delete scrubs the rejected payload.
|
|
338
|
-
db.run("DELETE FROM model_cache WHERE provider_id = ?", [providerId]);
|
|
339
|
-
return null;
|
|
340
|
-
}
|
|
341
|
-
const ageMs = now() - row.updated_at;
|
|
342
|
-
const fresh = Number.isFinite(ageMs) && ageMs >= 0 && ageMs <= ttlMs;
|
|
343
|
-
return {
|
|
344
|
-
models,
|
|
345
|
-
fresh,
|
|
346
|
-
authoritative: row.authoritative === 1,
|
|
347
|
-
updatedAt: row.updated_at,
|
|
348
|
-
headerOmittedModelIds,
|
|
349
|
-
unrestorableHeaderModelIds,
|
|
350
|
-
legacyHeaderRestoreMarkers: row.header_restore_version < HEADER_RESTORE_VERSION,
|
|
351
|
-
staticFingerprint: row.static_fingerprint ?? "",
|
|
352
|
-
};
|
|
353
|
-
} finally {
|
|
354
|
-
stmt.finalize();
|
|
355
386
|
}
|
|
387
|
+
const fresh = readRowUncached<TApi>(db, providerId, ttlMs, now);
|
|
388
|
+
if (dataVersion !== null) {
|
|
389
|
+
if (readRowCache.size >= READ_ROW_CACHE_MAX) readRowCache.clear();
|
|
390
|
+
readRowCache.set(key, { dataVersion, entry: fresh as CacheEntry<Api> | null });
|
|
391
|
+
}
|
|
392
|
+
return { hit: false as const, entry: fresh };
|
|
356
393
|
});
|
|
394
|
+
return entry.entry;
|
|
357
395
|
} catch {
|
|
358
396
|
return null;
|
|
359
397
|
}
|
|
360
398
|
}
|
|
361
399
|
|
|
400
|
+
function readRowUncached<TApi extends Api>(
|
|
401
|
+
db: Database,
|
|
402
|
+
providerId: string,
|
|
403
|
+
ttlMs: number,
|
|
404
|
+
now: () => number,
|
|
405
|
+
): CacheEntry<TApi> | null {
|
|
406
|
+
const stmt = db.query<CacheRow, [string]>("SELECT * FROM model_cache WHERE provider_id = ?");
|
|
407
|
+
try {
|
|
408
|
+
const row = stmt.get(providerId);
|
|
409
|
+
if (!row || row.version !== CACHE_SCHEMA_VERSION || row.materialization_policy !== materializationPolicy()) {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
const models = parseMaterializedModels<TApi>(row.models);
|
|
413
|
+
const headerOmittedModelIds = parseModelIds(row.header_omitted_model_ids);
|
|
414
|
+
const unrestorableHeaderModelIds = parseModelIds(row.unrestorable_header_model_ids);
|
|
415
|
+
if (models === null || headerOmittedModelIds === null || unrestorableHeaderModelIds === null) {
|
|
416
|
+
// Fail closed on corrupt header provenance: treating malformed
|
|
417
|
+
// markers as empty could return a model with required credentials
|
|
418
|
+
// silently absent. secure_delete scrubs the rejected payload.
|
|
419
|
+
db.run("DELETE FROM model_cache WHERE provider_id = ?", [providerId]);
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
const ageMs = now() - row.updated_at;
|
|
423
|
+
const fresh = Number.isFinite(ageMs) && ageMs >= 0 && ageMs <= ttlMs;
|
|
424
|
+
return {
|
|
425
|
+
models,
|
|
426
|
+
fresh,
|
|
427
|
+
authoritative: row.authoritative === 1,
|
|
428
|
+
updatedAt: row.updated_at,
|
|
429
|
+
headerOmittedModelIds,
|
|
430
|
+
unrestorableHeaderModelIds,
|
|
431
|
+
legacyHeaderRestoreMarkers: row.header_restore_version < HEADER_RESTORE_VERSION,
|
|
432
|
+
staticFingerprint: row.static_fingerprint ?? "",
|
|
433
|
+
};
|
|
434
|
+
} finally {
|
|
435
|
+
stmt.finalize();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
362
439
|
/** Whether a live model carries at least one request header. */
|
|
363
440
|
function hasModelHeaders(model: Model<Api>): boolean {
|
|
364
441
|
const headers = model.headers;
|
|
@@ -406,6 +483,7 @@ export function writeModelCache<TApi extends Api>(
|
|
|
406
483
|
restorableHeaderFallback?: Record<string, string>,
|
|
407
484
|
): void {
|
|
408
485
|
try {
|
|
486
|
+
invalidateReadRow(providerId, dbPath);
|
|
409
487
|
withModelCacheDb(dbPath, db => {
|
|
410
488
|
const headerOmittedModelIds: string[] = [];
|
|
411
489
|
const unrestorableHeaderModelIds: string[] = [];
|
|
@@ -442,7 +520,7 @@ export function writeModelCache<TApi extends Api>(
|
|
|
442
520
|
[
|
|
443
521
|
providerId,
|
|
444
522
|
CACHE_SCHEMA_VERSION,
|
|
445
|
-
|
|
523
|
+
materializationPolicy(),
|
|
446
524
|
updatedAt,
|
|
447
525
|
authoritative ? 1 : 0,
|
|
448
526
|
staticFingerprint,
|
package/src/model-thinking.ts
CHANGED
|
@@ -108,19 +108,22 @@ export function mapEffortToGoogleThinkingLevel<TApi extends Api>(
|
|
|
108
108
|
/**
|
|
109
109
|
* Maps a normalized thinking effort to Anthropic adaptive effort values via
|
|
110
110
|
* the model's baked `thinking.effortMap` (identity for unmapped efforts).
|
|
111
|
+
*
|
|
112
|
+
* The Anthropic adaptive wire vocabulary has no `minimal` tier (valid values:
|
|
113
|
+
* `low`, `medium`, `high`, `xhigh`, `max`); a model ladder that exposes
|
|
114
|
+
* `minimal` — custom `anthropic-messages` providers do, unlike the built-in
|
|
115
|
+
* Claude ladders — would otherwise serialize `output_config.effort: "minimal"`
|
|
116
|
+
* and 400 (`level "minimal" not supported`). Clamp it to the lowest real tier,
|
|
117
|
+
* mirroring {@link mapEffortToGoogleThinkingLevel}'s `minimal` handling.
|
|
111
118
|
*/
|
|
112
119
|
export function mapEffortToAnthropicAdaptiveEffort<TApi extends Api>(
|
|
113
120
|
model: ApiModel<TApi>,
|
|
114
121
|
effort: Effort,
|
|
115
122
|
): "low" | "medium" | "high" | "xhigh" | "max" | "adaptive" {
|
|
116
123
|
const supported = requireSupportedEffort(model, effort);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
| "high"
|
|
121
|
-
| "xhigh"
|
|
122
|
-
| "max"
|
|
123
|
-
| "adaptive";
|
|
124
|
+
const mapped = model.thinking?.effortMap?.[supported] ?? supported;
|
|
125
|
+
if (mapped === Effort.Minimal) return "low";
|
|
126
|
+
return mapped as "low" | "medium" | "high" | "xhigh" | "max" | "adaptive";
|
|
124
127
|
}
|
|
125
128
|
|
|
126
129
|
/**
|