@linxiraos/pi-catalog 1.1.26 → 1.2.0
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 +4 -6
- package/dist/types/compat/auth-ids.d.ts +2 -2
- package/dist/types/compat/collapse.d.ts +7 -0
- package/dist/types/compat/image-tokenization.d.ts +52 -0
- package/dist/types/compat/judgment-questions.d.ts +7 -0
- package/dist/types/compat/prompt-cache-lookback.d.ts +7 -0
- package/dist/types/compat/provider-ids.d.ts +1 -1
- package/dist/types/compat/reasoning-carry.d.ts +21 -0
- package/dist/types/compat/request-size.d.ts +7 -0
- package/dist/types/compat/taxonomy.d.ts +0 -10
- package/dist/types/compat/types.d.ts +4 -0
- package/dist/types/discovery/antigravity.d.ts +13 -3
- package/dist/types/hosts.d.ts +4 -0
- package/dist/types/model-cache.d.ts +6 -0
- package/dist/types/model-manager.d.ts +16 -0
- package/dist/types/provider-models/account-access.d.ts +11 -0
- package/dist/types/provider-models/google.d.ts +23 -1
- package/dist/types/provider-models/openai-compat.d.ts +19 -1
- package/dist/types/provider-models/special.d.ts +8 -0
- package/dist/types/types.d.ts +134 -23
- package/dist/types/wire/cloudflare-ai-gateway.d.ts +0 -10
- package/dist/types/wire/cloudflare.d.ts +10 -0
- package/dist/types/wire/factory-droid.d.ts +1 -1
- package/dist/types/wire/gemini-headers.d.ts +6 -3
- package/package.json +4 -4
- package/src/build.ts +39 -35
- package/src/compat/auth-ids.ts +6 -0
- package/src/compat/axes.ts +43 -5
- package/src/compat/cascade.ts +6 -1
- package/src/compat/collapse.d.ts +7 -0
- package/src/compat/collapse.ts +11 -0
- package/src/compat/image-tokenization.ts +166 -0
- package/src/compat/judgment-questions.ts +12 -0
- package/src/compat/prompt-cache-lookback.ts +23 -0
- package/src/compat/provider-ids.d.ts +1 -1
- package/src/compat/provider-ids.ts +3 -0
- package/src/compat/reasoning-carry.ts +64 -0
- package/src/compat/request-size.ts +25 -0
- package/src/compat/resolve.ts +36 -25
- package/src/compat/rules/README.md +13 -3
- package/src/compat/rules/auth/_order.kdl +2 -2
- package/src/compat/rules/auth/cloudflare-workers-ai.kdl +9 -0
- package/src/compat/rules/auth/coralbricks.kdl +13 -0
- package/src/compat/rules/auth/minimax-code-cn.kdl +1 -1
- package/src/compat/rules/auth/minimax-code.kdl +1 -1
- package/src/compat/rules/auth/openai-codex.kdl +1 -1
- package/src/compat/rules/auth/snowflake.kdl +11 -0
- package/src/compat/rules/auth/stencil.kdl +1 -1
- package/src/compat/rules/classes/anthropic.kdl +188 -7
- package/src/compat/rules/classes/deepseek.kdl +11 -3
- package/src/compat/rules/classes/gemini.kdl +12 -0
- package/src/compat/rules/classes/glm.kdl +17 -8
- package/src/compat/rules/classes/kimi.kdl +15 -0
- package/src/compat/rules/classes/meta.kdl +4 -4
- package/src/compat/rules/classes/minimax.kdl +31 -1
- package/src/compat/rules/classes/openai.kdl +114 -4
- package/src/compat/rules/classes/qwen.kdl +2 -1
- package/src/compat/rules/providers/abliteration.kdl +7 -0
- package/src/compat/rules/providers/amazon-bedrock.kdl +0 -7
- package/src/compat/rules/providers/anthropic.kdl +13 -0
- package/src/compat/rules/providers/apple.kdl +1 -0
- package/src/compat/rules/providers/cloudflare-workers-ai.kdl +34 -0
- package/src/compat/rules/providers/coralbricks.kdl +62 -0
- package/src/compat/rules/providers/cursor.kdl +28 -8
- package/src/compat/rules/providers/devin.kdl +2 -2
- package/src/compat/rules/providers/factory-droid.kdl +77 -13
- package/src/compat/rules/providers/github-copilot.kdl +29 -0
- package/src/compat/rules/providers/google-antigravity.kdl +8 -1
- package/src/compat/rules/providers/groq.kdl +21 -0
- package/src/compat/rules/providers/image-tokenization.kdl +42 -0
- package/src/compat/rules/providers/judgment-questions.kdl +53 -0
- package/src/compat/rules/providers/litellm.kdl +4 -1
- package/src/compat/rules/providers/meta.kdl +3 -3
- package/src/compat/rules/providers/minimax-code-cn.kdl +10 -5
- package/src/compat/rules/providers/minimax-code.kdl +10 -5
- package/src/compat/rules/providers/mistral.kdl +25 -1
- package/src/compat/rules/providers/moonshot.kdl +11 -0
- package/src/compat/rules/providers/muse-code.kdl +7 -0
- package/src/compat/rules/providers/ollama.kdl +3 -0
- package/src/compat/rules/providers/openai-codex.kdl +18 -13
- package/src/compat/rules/providers/openai.kdl +22 -0
- package/src/compat/rules/providers/opencode-go.kdl +11 -0
- package/src/compat/rules/providers/opencode-zen.kdl +20 -0
- package/src/compat/rules/providers/openrouter.kdl +6 -0
- package/src/compat/rules/providers/snowflake.kdl +135 -0
- package/src/compat/rules/providers/tool-result-images.kdl +16 -0
- package/src/compat/rules/providers/zhipu-coding-plan.kdl +10 -0
- package/src/compat/rules/runtime/behavior.kdl +8 -5
- package/src/compat/rules/taxonomy/_collapse.kdl +52 -2
- package/src/compat/rules/taxonomy/deepseek.kdl +4 -0
- package/src/compat/rules/taxonomy/kimi.kdl +5 -0
- package/src/compat/rules.json +1 -1
- package/src/compat/taxonomy.d.ts +0 -10
- package/src/compat/taxonomy.ts +3 -3
- package/src/compat/types.d.ts +4 -0
- package/src/compat/types.ts +4 -0
- package/src/discovery/antigravity.d.ts +13 -3
- package/src/discovery/antigravity.ts +33 -17
- package/src/discovery/cursor.ts +28 -4
- package/src/discovery/devin.ts +20 -2
- package/src/hosts.d.ts +4 -0
- package/src/hosts.ts +2 -0
- package/src/model-cache.d.ts +6 -0
- package/src/model-cache.ts +123 -47
- package/src/model-manager.d.ts +16 -0
- package/src/model-manager.ts +107 -41
- package/src/model-tokenizer.ts +3 -0
- package/src/models.json +1 -1
- package/src/provider-models/account-access.d.ts +11 -0
- package/src/provider-models/account-access.ts +30 -0
- package/src/provider-models/cache-provider-id.ts +13 -6
- package/src/provider-models/cline-pass.ts +33 -0
- package/src/provider-models/descriptors.ts +4 -0
- package/src/provider-models/google.d.ts +23 -1
- package/src/provider-models/google.ts +72 -14
- package/src/provider-models/openai-compat.d.ts +19 -1
- package/src/provider-models/openai-compat.ts +281 -42
- package/src/provider-models/special.d.ts +8 -0
- package/src/provider-models/special.ts +26 -22
- package/src/types.d.ts +134 -23
- package/src/types.ts +183 -20
- package/src/wire/cloudflare-ai-gateway.d.ts +0 -10
- package/src/wire/cloudflare-ai-gateway.ts +0 -36
- package/src/wire/cloudflare.ts +40 -0
- package/src/wire/factory-droid.d.ts +1 -1
- package/src/wire/factory-droid.ts +1 -1
- package/src/wire/gemini-headers.d.ts +6 -3
- package/src/wire/gemini-headers.ts +39 -21
package/src/model-cache.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Replaces per-provider JSON files with a single cache.db.
|
|
4
4
|
*/
|
|
5
5
|
import type { Database } from "bun:sqlite";
|
|
6
|
+
import * as path from "node:path";
|
|
6
7
|
import { getModelDbPath, isSqliteCorruptionError, logger, openSqliteDatabaseSync, VERSION } from "@linxiraos/pi-utils";
|
|
7
8
|
import RULES from "./compat/rules.json" with { type: "json" };
|
|
8
9
|
import type { Api, Model } from "./types";
|
|
@@ -46,14 +47,19 @@ function materializationPolicy(): string {
|
|
|
46
47
|
return cachedMaterializationPolicy;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
interface
|
|
50
|
+
interface CacheRowMeta {
|
|
50
51
|
provider_id: string;
|
|
51
52
|
version: number;
|
|
52
53
|
materialization_policy: string;
|
|
53
54
|
updated_at: number;
|
|
54
55
|
authoritative: number;
|
|
55
56
|
static_fingerprint: string;
|
|
56
|
-
|
|
57
|
+
/**
|
|
58
|
+
* `Bun.hash` of `models`; empty for rows written before the column existed
|
|
59
|
+
* (never equal). An in-place `UPDATE` of `models` that leaves the hash
|
|
60
|
+
* untouched resets it to empty (`model_cache_models_hash_reset` trigger).
|
|
61
|
+
*/
|
|
62
|
+
models_hash: string;
|
|
57
63
|
header_omitted_model_ids: string;
|
|
58
64
|
unrestorable_header_model_ids: string;
|
|
59
65
|
header_restore_version: number;
|
|
@@ -63,6 +69,10 @@ interface CacheRow {
|
|
|
63
69
|
refresh_authoritative: number | null;
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
interface CacheRow extends CacheRowMeta {
|
|
73
|
+
models: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
66
76
|
/**
|
|
67
77
|
* Payload rows are multi-MB and SQLite rewrites a whole record (including its
|
|
68
78
|
* overflow pages) on any column update. Freshness that advances without a
|
|
@@ -71,15 +81,23 @@ interface CacheRow {
|
|
|
71
81
|
* the payload row's `updated_at`, so a payload rewritten by another binary
|
|
72
82
|
* silently invalidates it.
|
|
73
83
|
*/
|
|
74
|
-
const
|
|
75
|
-
SELECT m.*,
|
|
76
|
-
r.payload_updated_at AS refresh_payload_updated_at,
|
|
77
|
-
r.updated_at AS refresh_updated_at,
|
|
78
|
-
r.authoritative AS refresh_authoritative
|
|
84
|
+
const CACHE_ROW_FROM = `
|
|
79
85
|
FROM model_cache m
|
|
80
86
|
LEFT JOIN model_cache_refresh r ON r.provider_id = m.provider_id
|
|
81
87
|
WHERE m.provider_id = ?
|
|
82
88
|
`;
|
|
89
|
+
const CACHE_ROW_REFRESH_COLUMNS = `
|
|
90
|
+
r.payload_updated_at AS refresh_payload_updated_at,
|
|
91
|
+
r.updated_at AS refresh_updated_at,
|
|
92
|
+
r.authoritative AS refresh_authoritative`;
|
|
93
|
+
const SELECT_CACHE_ROW = `SELECT m.*, ${CACHE_ROW_REFRESH_COLUMNS} ${CACHE_ROW_FROM}`;
|
|
94
|
+
/** Scalar columns plus `models_hash`: validates a memoized parse without reading the multi-MB payload. */
|
|
95
|
+
const SELECT_CACHE_ROW_META = `
|
|
96
|
+
SELECT m.provider_id, m.version, m.materialization_policy, m.updated_at, m.authoritative,
|
|
97
|
+
m.static_fingerprint, m.models_hash, m.header_omitted_model_ids,
|
|
98
|
+
m.unrestorable_header_model_ids, m.header_restore_version, ${CACHE_ROW_REFRESH_COLUMNS}
|
|
99
|
+
${CACHE_ROW_FROM}
|
|
100
|
+
`;
|
|
83
101
|
|
|
84
102
|
/** Cumulative model-cache write counters for this process. */
|
|
85
103
|
export interface ModelCacheWriteStats {
|
|
@@ -149,8 +167,11 @@ let sharedDbPath: string | null = null;
|
|
|
149
167
|
interface ReadRowCacheEntry {
|
|
150
168
|
/** `PRAGMA data_version` of the shared connection when validated; null for per-call connections. */
|
|
151
169
|
dataVersion: number | null;
|
|
152
|
-
/**
|
|
153
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Scalar columns (plus `models_hash`) of the row the entry was parsed from;
|
|
172
|
+
* null when the row was absent or rejected. Never the multi-MB payload itself.
|
|
173
|
+
*/
|
|
174
|
+
meta: CacheRowMeta | null;
|
|
154
175
|
entry: CacheEntry<Api> | null;
|
|
155
176
|
}
|
|
156
177
|
|
|
@@ -182,7 +203,7 @@ function withFreshness<TApi extends Api>(
|
|
|
182
203
|
}
|
|
183
204
|
function invalidateReadRow(providerId: string, dbPath?: string): void {
|
|
184
205
|
try {
|
|
185
|
-
readRowCache.delete(readCacheKey(dbPath
|
|
206
|
+
readRowCache.delete(readCacheKey(resolveCacheDb(dbPath).resolvedPath, providerId));
|
|
186
207
|
} catch {
|
|
187
208
|
// Best-effort only; a missed invalidation just costs one extra parse.
|
|
188
209
|
}
|
|
@@ -213,6 +234,7 @@ function initializeDb(db: Database): void {
|
|
|
213
234
|
header_omitted_model_ids TEXT NOT NULL DEFAULT '[]',
|
|
214
235
|
unrestorable_header_model_ids TEXT NOT NULL DEFAULT '[]',
|
|
215
236
|
header_restore_version INTEGER NOT NULL DEFAULT 0,
|
|
237
|
+
models_hash TEXT NOT NULL DEFAULT '',
|
|
216
238
|
models TEXT NOT NULL
|
|
217
239
|
)
|
|
218
240
|
`);
|
|
@@ -225,6 +247,17 @@ function initializeDb(db: Database): void {
|
|
|
225
247
|
)
|
|
226
248
|
`);
|
|
227
249
|
migrateCacheSchema(db);
|
|
250
|
+
// `models_hash` is the only payload-change detector for the read memo and the
|
|
251
|
+
// unchanged-write skip. Writers here replace whole rows; this keeps an
|
|
252
|
+
// in-place edit of `models` by any other tool or binary from reusing a stale hash.
|
|
253
|
+
db.run(`
|
|
254
|
+
CREATE TRIGGER IF NOT EXISTS model_cache_models_hash_reset
|
|
255
|
+
AFTER UPDATE OF models ON model_cache
|
|
256
|
+
WHEN NEW.models_hash = OLD.models_hash
|
|
257
|
+
BEGIN
|
|
258
|
+
UPDATE model_cache SET models_hash = '' WHERE provider_id = NEW.provider_id;
|
|
259
|
+
END
|
|
260
|
+
`);
|
|
228
261
|
}
|
|
229
262
|
|
|
230
263
|
function closeSharedDb(): void {
|
|
@@ -234,6 +267,16 @@ function closeSharedDb(): void {
|
|
|
234
267
|
sharedDbPath = null;
|
|
235
268
|
}
|
|
236
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Closes the shared handle to the default `<agent-dir>/models.db`; the next
|
|
272
|
+
* default-path access reopens it. Call before deleting an agent directory the
|
|
273
|
+
* cache was opened under — Windows cannot remove a database that is still open.
|
|
274
|
+
*/
|
|
275
|
+
export function closeModelCache(): void {
|
|
276
|
+
if (sharedDbPath) invalidateReadPath(sharedDbPath);
|
|
277
|
+
closeSharedDb();
|
|
278
|
+
}
|
|
279
|
+
|
|
237
280
|
function runModelCacheDb<T>(resolvedPath: string, shared: boolean, useDb: (db: Database) => T): T {
|
|
238
281
|
if (shared && sharedDb && sharedDbPath !== resolvedPath) closeSharedDb();
|
|
239
282
|
if (shared && sharedDb) {
|
|
@@ -270,8 +313,22 @@ function runModelCacheDb<T>(resolvedPath: string, shared: boolean, useDb: (db: D
|
|
|
270
313
|
}
|
|
271
314
|
|
|
272
315
|
function withModelCacheDb<T>(dbPath: string | undefined, useDb: (db: Database) => T): T {
|
|
273
|
-
const resolvedPath
|
|
274
|
-
return runModelCacheDb(resolvedPath,
|
|
316
|
+
const { resolvedPath, shared } = resolveCacheDb(dbPath);
|
|
317
|
+
return runModelCacheDb(resolvedPath, shared, useDb);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* An explicit path naming the default `models.db` (e.g. the SDK passing
|
|
322
|
+
* `getModelDbPath(agentDir)` for the default agent dir) shares the long-lived
|
|
323
|
+
* handle, so it gets the `data_version` fast path instead of opening,
|
|
324
|
+
* initializing and re-reading the row on every call.
|
|
325
|
+
*/
|
|
326
|
+
function resolveCacheDb(dbPath: string | undefined): { resolvedPath: string; shared: boolean } {
|
|
327
|
+
const defaultPath = getModelDbPath();
|
|
328
|
+
if (dbPath === undefined || dbPath === defaultPath || path.resolve(dbPath) === path.resolve(defaultPath)) {
|
|
329
|
+
return { resolvedPath: defaultPath, shared: true };
|
|
330
|
+
}
|
|
331
|
+
return { resolvedPath: dbPath, shared: false };
|
|
275
332
|
}
|
|
276
333
|
|
|
277
334
|
function migrateCacheSchema(db: Database): void {
|
|
@@ -296,6 +353,12 @@ function migrateCacheSchema(db: Database): void {
|
|
|
296
353
|
// header matching was introduced.
|
|
297
354
|
db.run("ALTER TABLE model_cache ADD COLUMN header_restore_version INTEGER NOT NULL DEFAULT 0");
|
|
298
355
|
}
|
|
356
|
+
if (!columns.some(column => column.name === "models_hash")) {
|
|
357
|
+
// Rows from older binaries keep '' (never equal), so they are re-read in
|
|
358
|
+
// full until the next write records their hash; no version bump needed.
|
|
359
|
+
// Older binaries rewriting a row via INSERT OR REPLACE reset it to ''.
|
|
360
|
+
db.run("ALTER TABLE model_cache ADD COLUMN models_hash TEXT NOT NULL DEFAULT ''");
|
|
361
|
+
}
|
|
299
362
|
} finally {
|
|
300
363
|
stmt.finalize();
|
|
301
364
|
}
|
|
@@ -409,15 +472,14 @@ export function readModelCache<TApi extends Api>(
|
|
|
409
472
|
dbPath?: string,
|
|
410
473
|
): CacheEntry<TApi> | null {
|
|
411
474
|
try {
|
|
412
|
-
const resolvedPath = dbPath ?? getModelDbPath();
|
|
413
|
-
const key = readCacheKey(resolvedPath, providerId);
|
|
414
475
|
// Monotonic change signal: same-shaped WAL overwrites after checkpoint
|
|
415
476
|
// can leave every size:mtime pair identical, so file metadata alone
|
|
416
477
|
// cannot invalidate. PRAGMA data_version increments on each committed
|
|
417
478
|
// write transaction visible to a new reader. It is only comparable
|
|
418
479
|
// across calls on the same connection, so it gates the no-query fast
|
|
419
480
|
// path for the shared handle only.
|
|
420
|
-
const shared = dbPath
|
|
481
|
+
const { resolvedPath, shared } = resolveCacheDb(dbPath);
|
|
482
|
+
const key = readCacheKey(resolvedPath, providerId);
|
|
421
483
|
return runModelCacheDb(resolvedPath, shared, db => {
|
|
422
484
|
const dataVersion = shared ? dbDataVersion(db) : null;
|
|
423
485
|
const cached = readRowCache.get(key);
|
|
@@ -426,34 +488,32 @@ export function readModelCache<TApi extends Api>(
|
|
|
426
488
|
// cached row's updatedAt so a long-lived process goes stale.
|
|
427
489
|
return withFreshness(cached.entry as CacheEntry<TApi> | null, ttlMs, now);
|
|
428
490
|
}
|
|
429
|
-
// Any commit (to any provider's row) bumps data_version.
|
|
430
|
-
//
|
|
431
|
-
|
|
432
|
-
let
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
491
|
+
// Any commit (to any provider's row) bumps data_version. Validate the
|
|
492
|
+
// memoized parse against the row's scalar columns and payload hash
|
|
493
|
+
// first; only a changed payload pays for the multi-MB `models` read.
|
|
494
|
+
let entry: CacheEntry<TApi> | null = null;
|
|
495
|
+
let meta: CacheRowMeta | null = null;
|
|
496
|
+
if (cached?.entry && cached.meta) {
|
|
497
|
+
meta = queryRow<CacheRowMeta>(db, SELECT_CACHE_ROW_META, providerId);
|
|
498
|
+
if (meta && meta.materialization_policy === materializationPolicy() && cacheRowsEqual(cached.meta, meta)) {
|
|
499
|
+
// Payload unchanged; only side-table freshness may have moved.
|
|
500
|
+
entry = withFreshness(withRowState(cached.entry as CacheEntry<TApi>, meta), ttlMs, now);
|
|
501
|
+
} else {
|
|
502
|
+
meta = null;
|
|
503
|
+
}
|
|
437
504
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
cached.row &&
|
|
442
|
-
row &&
|
|
443
|
-
row.materialization_policy === materializationPolicy() &&
|
|
444
|
-
cacheRowsEqual(cached.row, row)
|
|
445
|
-
) {
|
|
446
|
-
// Payload bytes unchanged; only side-table freshness may have moved.
|
|
447
|
-
entry = withFreshness(withRowState(cached.entry as CacheEntry<TApi>, row), ttlMs, now);
|
|
448
|
-
} else {
|
|
505
|
+
if (entry === null) {
|
|
506
|
+
// One statement reads payload and scalars from the same snapshot.
|
|
507
|
+
const row = queryRow<CacheRow>(db, SELECT_CACHE_ROW, providerId);
|
|
449
508
|
entry = parseCacheRow<TApi>(db, providerId, row, ttlMs, now);
|
|
509
|
+
if (entry !== null && row !== null) {
|
|
510
|
+
// Drop the payload so the memo never pins the multi-MB `models` string.
|
|
511
|
+
const { models: _models, ...rowScalars } = row;
|
|
512
|
+
meta = rowScalars;
|
|
513
|
+
}
|
|
450
514
|
}
|
|
451
515
|
if (readRowCache.size >= READ_ROW_CACHE_MAX) readRowCache.clear();
|
|
452
|
-
readRowCache.set(key, {
|
|
453
|
-
dataVersion,
|
|
454
|
-
row: entry === null ? null : row,
|
|
455
|
-
entry: entry as CacheEntry<Api> | null,
|
|
456
|
-
});
|
|
516
|
+
readRowCache.set(key, { dataVersion, meta, entry: entry as CacheEntry<Api> | null });
|
|
457
517
|
return entry;
|
|
458
518
|
});
|
|
459
519
|
} catch {
|
|
@@ -461,8 +521,20 @@ export function readModelCache<TApi extends Api>(
|
|
|
461
521
|
}
|
|
462
522
|
}
|
|
463
523
|
|
|
464
|
-
|
|
465
|
-
|
|
524
|
+
function queryRow<T>(db: Database, sql: string, providerId: string): T | null {
|
|
525
|
+
const stmt = db.query<T, [string]>(sql);
|
|
526
|
+
try {
|
|
527
|
+
return stmt.get(providerId);
|
|
528
|
+
} finally {
|
|
529
|
+
stmt.finalize();
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Whether two rows carry the same payload record (refresh side-row columns
|
|
535
|
+
* excluded). Payloads compare by `models_hash`; rows without one never match.
|
|
536
|
+
*/
|
|
537
|
+
function cacheRowsEqual(left: CacheRowMeta, right: CacheRowMeta): boolean {
|
|
466
538
|
return (
|
|
467
539
|
left.version === right.version &&
|
|
468
540
|
left.materialization_policy === right.materialization_policy &&
|
|
@@ -472,7 +544,8 @@ function cacheRowsEqual(left: CacheRow, right: CacheRow): boolean {
|
|
|
472
544
|
left.header_omitted_model_ids === right.header_omitted_model_ids &&
|
|
473
545
|
left.unrestorable_header_model_ids === right.unrestorable_header_model_ids &&
|
|
474
546
|
left.header_restore_version === right.header_restore_version &&
|
|
475
|
-
left.
|
|
547
|
+
left.models_hash !== "" &&
|
|
548
|
+
left.models_hash === right.models_hash
|
|
476
549
|
);
|
|
477
550
|
}
|
|
478
551
|
|
|
@@ -488,7 +561,7 @@ function rowState(row: StoredRowState): { updatedAt: number; authoritative: bool
|
|
|
488
561
|
return { updatedAt: row.updated_at, authoritative: row.authoritative === 1 };
|
|
489
562
|
}
|
|
490
563
|
|
|
491
|
-
function withRowState<TApi extends Api>(entry: CacheEntry<TApi>, row:
|
|
564
|
+
function withRowState<TApi extends Api>(entry: CacheEntry<TApi>, row: StoredRowState): CacheEntry<TApi> {
|
|
492
565
|
const { updatedAt, authoritative } = rowState(row);
|
|
493
566
|
return entry.updatedAt === updatedAt && entry.authoritative === authoritative
|
|
494
567
|
? entry
|
|
@@ -638,6 +711,8 @@ export function writeModelCache<TApi extends Api>(
|
|
|
638
711
|
const serializedHeaderOmitted = JSON.stringify(headerOmittedModelIds);
|
|
639
712
|
const serializedUnrestorable = JSON.stringify(unrestorableHeaderModelIds);
|
|
640
713
|
const serializedModels = JSON.stringify(cachedModels);
|
|
714
|
+
// Length prefix: a collision would also need an equal payload size.
|
|
715
|
+
const modelsHash = `${serializedModels.length.toString(36)}:${Bun.hash(serializedModels).toString(36)}`;
|
|
641
716
|
const outcome = db
|
|
642
717
|
.transaction((): "skipped" | "refreshed" | "written" => {
|
|
643
718
|
const matchStmt = db.prepare<
|
|
@@ -652,7 +727,7 @@ export function writeModelCache<TApi extends Api>(
|
|
|
652
727
|
LEFT JOIN model_cache_refresh r ON r.provider_id = m.provider_id
|
|
653
728
|
WHERE m.provider_id = ? AND m.version = ? AND m.materialization_policy = ?
|
|
654
729
|
AND m.static_fingerprint = ? AND m.header_omitted_model_ids = ?
|
|
655
|
-
AND m.unrestorable_header_model_ids = ? AND m.header_restore_version = ? AND m.
|
|
730
|
+
AND m.unrestorable_header_model_ids = ? AND m.header_restore_version = ? AND m.models_hash = ?`,
|
|
656
731
|
);
|
|
657
732
|
let stored: StoredRowState | null;
|
|
658
733
|
try {
|
|
@@ -664,7 +739,7 @@ export function writeModelCache<TApi extends Api>(
|
|
|
664
739
|
serializedHeaderOmitted,
|
|
665
740
|
serializedUnrestorable,
|
|
666
741
|
HEADER_RESTORE_VERSION,
|
|
667
|
-
|
|
742
|
+
modelsHash,
|
|
668
743
|
);
|
|
669
744
|
} finally {
|
|
670
745
|
matchStmt.finalize();
|
|
@@ -687,8 +762,8 @@ export function writeModelCache<TApi extends Api>(
|
|
|
687
762
|
`INSERT OR REPLACE INTO model_cache (
|
|
688
763
|
provider_id, version, materialization_policy, updated_at, authoritative, static_fingerprint,
|
|
689
764
|
header_omitted_model_ids, unrestorable_header_model_ids,
|
|
690
|
-
header_restore_version, models
|
|
691
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
765
|
+
header_restore_version, models_hash, models
|
|
766
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
692
767
|
[
|
|
693
768
|
providerId,
|
|
694
769
|
CACHE_SCHEMA_VERSION,
|
|
@@ -699,6 +774,7 @@ export function writeModelCache<TApi extends Api>(
|
|
|
699
774
|
serializedHeaderOmitted,
|
|
700
775
|
serializedUnrestorable,
|
|
701
776
|
HEADER_RESTORE_VERSION,
|
|
777
|
+
modelsHash,
|
|
702
778
|
serializedModels,
|
|
703
779
|
],
|
|
704
780
|
);
|
package/src/model-manager.d.ts
CHANGED
|
@@ -35,6 +35,22 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
|
|
|
35
35
|
cacheTtlMs?: number;
|
|
36
36
|
/** When true, a successful dynamic fetch is the complete provider catalog and prunes static-only models. */
|
|
37
37
|
dynamicModelsAuthoritative?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* When true, a provider's dynamic rows are the deployment's whole truth
|
|
40
|
+
* for input modality: a live text-only row strips the bundled reference's
|
|
41
|
+
* image support instead of OR-merging it back. Declared by providers whose
|
|
42
|
+
* capability flags are authoritative and whose endpoints reject
|
|
43
|
+
* unsupported content (`github-copilot`, `deepinfra`, `coralbricks`).
|
|
44
|
+
*/
|
|
45
|
+
dynamicInputAuthoritative?: boolean;
|
|
46
|
+
/** When true, live reasoning capability replaces the fallback, including an explicit false. */
|
|
47
|
+
dynamicReasoningAuthoritative?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* When true, a live per-million price replaces the fallback even when it is
|
|
50
|
+
* an explicit `0` (a free model or a dropped cache-write charge). Without it,
|
|
51
|
+
* a zero discovery price is treated as unknown and the bundled rate is kept.
|
|
52
|
+
*/
|
|
53
|
+
dynamicCostAuthoritative?: boolean;
|
|
38
54
|
/**
|
|
39
55
|
* When true, a fresh cache never satisfies an online-eligible refresh: the
|
|
40
56
|
* dynamic fetch always runs (an explicit `"offline"` strategy is still
|
package/src/model-manager.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { VERSION } from "@linxiraos/pi-utils";
|
|
1
|
+
import { logger, VERSION } from "@linxiraos/pi-utils";
|
|
2
2
|
import { buildModel } from "./build";
|
|
3
3
|
import { collapseBuiltVariants } from "./compat/collapse";
|
|
4
4
|
import { applyCatalogMetrics, CatalogMetricsIndex } from "./identity/metrics";
|
|
@@ -57,6 +57,22 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
|
|
|
57
57
|
cacheTtlMs?: number;
|
|
58
58
|
/** When true, a successful dynamic fetch is the complete provider catalog and prunes static-only models. */
|
|
59
59
|
dynamicModelsAuthoritative?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* When true, a provider's dynamic rows are the deployment's whole truth
|
|
62
|
+
* for input modality: a live text-only row strips the bundled reference's
|
|
63
|
+
* image support instead of OR-merging it back. Declared by providers whose
|
|
64
|
+
* capability flags are authoritative and whose endpoints reject
|
|
65
|
+
* unsupported content (`github-copilot`, `deepinfra`, `coralbricks`).
|
|
66
|
+
*/
|
|
67
|
+
dynamicInputAuthoritative?: boolean;
|
|
68
|
+
/** When true, live reasoning capability replaces the fallback, including an explicit false. */
|
|
69
|
+
dynamicReasoningAuthoritative?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* When true, a live per-million price replaces the fallback even when it is
|
|
72
|
+
* an explicit `0` (a free model or a dropped cache-write charge). Without it,
|
|
73
|
+
* a zero discovery price is treated as unknown and the bundled rate is kept.
|
|
74
|
+
*/
|
|
75
|
+
dynamicCostAuthoritative?: boolean;
|
|
60
76
|
/**
|
|
61
77
|
* When true, a fresh cache never satisfies an online-eligible refresh: the
|
|
62
78
|
* dynamic fetch always runs (an explicit `"offline"` strategy is still
|
|
@@ -255,6 +271,11 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
255
271
|
const usableCachedModels = restoredCache.models.filter(model => !restoredCache.unresolvedModelIds.has(model.id));
|
|
256
272
|
const cacheHasUnresolvedHeaders = restoredCache.unresolvedModelIds.size > 0;
|
|
257
273
|
const dynamicModelsAuthoritative = options.dynamicModelsAuthoritative ?? false;
|
|
274
|
+
const mergeAuthority: DynamicMergeAuthority = {
|
|
275
|
+
input: options.dynamicInputAuthoritative ?? false,
|
|
276
|
+
reasoning: options.dynamicReasoningAuthoritative ?? false,
|
|
277
|
+
cost: options.dynamicCostAuthoritative ?? false,
|
|
278
|
+
};
|
|
258
279
|
const cacheDropIds = options.dropCachedModelIdsOnStaticMismatch;
|
|
259
280
|
const staticCatalogFingerprint = fingerprintStaticModels(staticModels, dynamicModelsAuthoritative);
|
|
260
281
|
// Endpoint-migration policy is cache identity: adding an id must invalidate
|
|
@@ -298,7 +319,10 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
298
319
|
? restoredCache.models.filter(model => !additiveStaticModelIds.has(model.id))
|
|
299
320
|
: restoredCache.models;
|
|
300
321
|
const cachedModels = additiveStaticModelIds
|
|
301
|
-
? mergeCatalogMetrics(
|
|
322
|
+
? mergeCatalogMetrics(
|
|
323
|
+
mergeDynamicModels(staticModels, cacheContribution, mergeAuthority),
|
|
324
|
+
restoredCache.models,
|
|
325
|
+
)
|
|
302
326
|
: restoredCache.models;
|
|
303
327
|
const source: ModelResolutionSource = cacheContribution.length > 0 ? "cache" : "bundled";
|
|
304
328
|
return {
|
|
@@ -351,10 +375,11 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
351
375
|
dynamicModels.length > 0 &&
|
|
352
376
|
(dynamicModelsAuthoritative || !hasModelsDevFetcher || modelsDevFetchSucceeded)
|
|
353
377
|
: modelsDevFetchSucceeded;
|
|
354
|
-
const mergedWithCache = mergeDynamicModels(staticModels, cacheModels);
|
|
378
|
+
const mergedWithCache = mergeDynamicModels(staticModels, cacheModels, mergeAuthority);
|
|
355
379
|
const mergedWithModelsDev = mergeDynamicModels(
|
|
356
380
|
mergedWithCache,
|
|
357
381
|
modelsDevModels,
|
|
382
|
+
mergeAuthority,
|
|
358
383
|
fetchedModelsDevModels?.explicitKindModels,
|
|
359
384
|
);
|
|
360
385
|
const catalogMetricsSource = modelsDevFetchSucceeded ? normalizedModelsDevModels : preparedCacheModels;
|
|
@@ -364,6 +389,7 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
364
389
|
const mergedModels = mergeDynamicModels(
|
|
365
390
|
mergedWithCatalogMetrics,
|
|
366
391
|
dynamicModels,
|
|
392
|
+
mergeAuthority,
|
|
367
393
|
fetchedDynamicModels?.explicitKindModels,
|
|
368
394
|
);
|
|
369
395
|
const models = collapseBuiltVariants(
|
|
@@ -416,7 +442,11 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
416
442
|
? preparedLatestCacheModels.filter(model => !additiveStaticModelIds.has(model.id))
|
|
417
443
|
: preparedLatestCacheModels;
|
|
418
444
|
const fallbackSnapshotModels = collapseBuiltVariants(
|
|
419
|
-
mergeDynamicModels(
|
|
445
|
+
mergeDynamicModels(
|
|
446
|
+
mergeDynamicModels(staticModels, latestCacheModels, mergeAuthority),
|
|
447
|
+
modelsDevModels,
|
|
448
|
+
mergeAuthority,
|
|
449
|
+
),
|
|
420
450
|
);
|
|
421
451
|
if (fallbackSnapshotModels.length > 0 || latestCache !== null || cache !== null) {
|
|
422
452
|
writeModelCache(
|
|
@@ -544,9 +574,20 @@ function mergeCatalogMetrics<TApi extends Api>(
|
|
|
544
574
|
return applyCatalogMetrics(models, new CatalogMetricsIndex(catalogModels));
|
|
545
575
|
}
|
|
546
576
|
|
|
577
|
+
/**
|
|
578
|
+
* Which dynamic-row fields replace the existing row outright instead of
|
|
579
|
+
* merging additively. Set per provider by `ModelManagerOptions`.
|
|
580
|
+
*/
|
|
581
|
+
interface DynamicMergeAuthority {
|
|
582
|
+
readonly input: boolean;
|
|
583
|
+
readonly reasoning: boolean;
|
|
584
|
+
readonly cost: boolean;
|
|
585
|
+
}
|
|
586
|
+
|
|
547
587
|
function mergeDynamicModels<TApi extends Api>(
|
|
548
588
|
baseModels: readonly Model<TApi>[],
|
|
549
589
|
dynamicModels: readonly Model<TApi>[],
|
|
590
|
+
authority: DynamicMergeAuthority,
|
|
550
591
|
explicitKindModels?: ReadonlySet<Model<TApi>>,
|
|
551
592
|
): Model<TApi>[] {
|
|
552
593
|
// Empty-side fast paths: `mergeDynamicModels(base, [])` is the common shape
|
|
@@ -567,7 +608,7 @@ function mergeDynamicModels<TApi extends Api>(
|
|
|
567
608
|
// A policy-derived kind on a chat row is not permission to replace an
|
|
568
609
|
// authored runner. Only a kind present before materialization can do so.
|
|
569
610
|
if (modelKind(existingModel) !== "chat" && !explicitKindModels?.has(dynamicModel)) continue;
|
|
570
|
-
merged.set(dynamicModel.id, mergeDynamicModel(existingModel, dynamicModel));
|
|
611
|
+
merged.set(dynamicModel.id, mergeDynamicModel(existingModel, dynamicModel, authority));
|
|
571
612
|
}
|
|
572
613
|
return Array.from(merged.values());
|
|
573
614
|
}
|
|
@@ -598,39 +639,30 @@ export function fingerprintStaticModels<TApi extends Api>(
|
|
|
598
639
|
return `${MODEL_CACHE_FINGERPRINT_VERSION}:${Bun.hash(JSON.stringify(models)).toString(36)}`;
|
|
599
640
|
}
|
|
600
641
|
|
|
601
|
-
function mergeDynamicModel<TApi extends Api>(
|
|
642
|
+
function mergeDynamicModel<TApi extends Api>(
|
|
643
|
+
existingModel: Model<TApi>,
|
|
644
|
+
dynamicModel: Model<TApi>,
|
|
645
|
+
authority: DynamicMergeAuthority,
|
|
646
|
+
): Model<TApi> {
|
|
602
647
|
// When discovery resolves the same model id to a different endpoint (e.g.
|
|
603
648
|
// a GitHub Copilot business/enterprise host), the bundled reference's
|
|
604
|
-
// capabilities are pinned to another endpoint and no longer apply.
|
|
605
|
-
//
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
//
|
|
611
|
-
// there: without this carve-out a model that dropped those tags would keep
|
|
612
|
-
// the bundled reference's image support and the agent would go on sending
|
|
649
|
+
// capabilities are pinned to another endpoint and no longer apply.
|
|
650
|
+
// `authority.input` carries the descriptor's declaration that a
|
|
651
|
+
// provider's discovery rows are the deployment's whole truth for input
|
|
652
|
+
// modality (Copilot's pre-applied `supports.vision` fallback, DeepInfra's
|
|
653
|
+
// `vision`/`vlm` tags, CoralBricks' authoritative `supports_image_input`
|
|
654
|
+
// flag): without it, a live row that dropped image support would keep the
|
|
655
|
+
// bundled reference's image support and the agent would go on sending
|
|
613
656
|
// images to a now text-only route.
|
|
614
657
|
const endpointChanged = existingModel.baseUrl !== dynamicModel.baseUrl;
|
|
615
|
-
const dynamicInputAuthoritative =
|
|
616
|
-
endpointChanged ||
|
|
617
|
-
(existingModel.provider === "github-copilot" && dynamicModel.provider === "github-copilot") ||
|
|
618
|
-
(existingModel.provider === "deepinfra" && dynamicModel.provider === "deepinfra");
|
|
658
|
+
const dynamicInputAuthoritative = endpointChanged || authority.input;
|
|
619
659
|
const supportsImage = dynamicInputAuthoritative
|
|
620
660
|
? dynamicModel.input.includes("image")
|
|
621
661
|
: existingModel.input.includes("image") || dynamicModel.input.includes("image");
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
|
|
626
|
-
// stale `reasoning: true` back would re-arm an effort dial the route
|
|
627
|
-
// doesn't expose. Other providers keep the OR so a bundled reasoning flag
|
|
628
|
-
// survives a discovery row that simply omits the capability.
|
|
629
|
-
const dynamicReasoningAuthoritative =
|
|
630
|
-
existingModel.provider === "synthetic" && dynamicModel.provider === "synthetic";
|
|
631
|
-
const reasoning = dynamicReasoningAuthoritative
|
|
632
|
-
? dynamicModel.reasoning
|
|
633
|
-
: existingModel.reasoning || dynamicModel.reasoning;
|
|
662
|
+
// Providers with authoritative reasoning metadata must not regain a
|
|
663
|
+
// stale bundled dial after explicitly reporting no reasoning. Others
|
|
664
|
+
// retain the additive fallback for discovery that omits capabilities.
|
|
665
|
+
const reasoning = authority.reasoning ? dynamicModel.reasoning : existingModel.reasoning || dynamicModel.reasoning;
|
|
634
666
|
const longContextCost = dynamicModel.cost.longContext ?? existingModel.cost.longContext;
|
|
635
667
|
const timeBasedCost = dynamicModel.cost.timeBased ?? existingModel.cost.timeBased;
|
|
636
668
|
const existingHeaders = existingModel.resolveHeaders ?? existingModel.headers;
|
|
@@ -651,17 +683,26 @@ function mergeDynamicModel<TApi extends Api>(existingModel: Model<TApi>, dynamic
|
|
|
651
683
|
// the effort dial once the id is pinned to Responses (#12901).
|
|
652
684
|
const compat =
|
|
653
685
|
dynamicModel.compatConfig ?? (dynamicModel.api === existingModel.api ? existingModel.compatConfig : undefined);
|
|
686
|
+
// A live row that trusts only its own thinking (e.g. CoralBricks' advertised
|
|
687
|
+
// ladder) must not inherit the fallback's dial. Cached rows lose an
|
|
688
|
+
// explicit `thinking: undefined` to JSON, so the spread alone would.
|
|
689
|
+
const liveCompat = dynamicModel.compatConfig;
|
|
690
|
+
const trustsOwnThinking =
|
|
691
|
+
liveCompat !== undefined &&
|
|
692
|
+
"trustExplicitThinkingOnly" in liveCompat &&
|
|
693
|
+
liveCompat.trustExplicitThinkingOnly === true;
|
|
654
694
|
return buildModel({
|
|
655
695
|
...existingModel,
|
|
656
696
|
...dynamicModel,
|
|
657
697
|
name: preferDiscoveryName(dynamicModel.name, existingModel.name, dynamicModel.id),
|
|
658
698
|
reasoning,
|
|
699
|
+
...(trustsOwnThinking ? { thinking: dynamicModel.thinking } : {}),
|
|
659
700
|
input: supportsImage ? ["text", "image"] : ["text"],
|
|
660
701
|
cost: {
|
|
661
|
-
input: preferDiscoveryCost(dynamicModel.cost.input, existingModel.cost.input),
|
|
662
|
-
output: preferDiscoveryCost(dynamicModel.cost.output, existingModel.cost.output),
|
|
663
|
-
cacheRead: preferDiscoveryCost(dynamicModel.cost.cacheRead, existingModel.cost.cacheRead),
|
|
664
|
-
cacheWrite: preferDiscoveryCost(dynamicModel.cost.cacheWrite, existingModel.cost.cacheWrite),
|
|
702
|
+
input: preferDiscoveryCost(dynamicModel.cost.input, existingModel.cost.input, authority.cost),
|
|
703
|
+
output: preferDiscoveryCost(dynamicModel.cost.output, existingModel.cost.output, authority.cost),
|
|
704
|
+
cacheRead: preferDiscoveryCost(dynamicModel.cost.cacheRead, existingModel.cost.cacheRead, authority.cost),
|
|
705
|
+
cacheWrite: preferDiscoveryCost(dynamicModel.cost.cacheWrite, existingModel.cost.cacheWrite, authority.cost),
|
|
665
706
|
...(longContextCost ? { longContext: longContextCost } : {}),
|
|
666
707
|
...(timeBasedCost ? { timeBased: timeBasedCost } : {}),
|
|
667
708
|
},
|
|
@@ -678,8 +719,12 @@ function mergeDynamicModel<TApi extends Api>(existingModel: Model<TApi>, dynamic
|
|
|
678
719
|
} as ModelSpec<TApi>);
|
|
679
720
|
}
|
|
680
721
|
|
|
681
|
-
|
|
682
|
-
|
|
722
|
+
/**
|
|
723
|
+
* A zero discovery price usually means "not reported", so the fallback wins
|
|
724
|
+
* unless the provider declared its live prices authoritative (`zeroIsPrice`).
|
|
725
|
+
*/
|
|
726
|
+
function preferDiscoveryCost(discoveryCost: number, fallbackCost: number, zeroIsPrice: boolean): number {
|
|
727
|
+
if (Number.isFinite(discoveryCost) && (discoveryCost > 0 || (zeroIsPrice && discoveryCost === 0))) {
|
|
683
728
|
return discoveryCost;
|
|
684
729
|
}
|
|
685
730
|
return fallbackCost;
|
|
@@ -708,19 +753,40 @@ function preferDiscoveryLimit(discoveryLimit: number | null, fallbackLimit: numb
|
|
|
708
753
|
return discoveryLimit;
|
|
709
754
|
}
|
|
710
755
|
|
|
756
|
+
/**
|
|
757
|
+
* Materializes a remote source's rows. A row `buildModel` rejects (an id newer
|
|
758
|
+
* than this build's rule tree that lands on an equal-rank overlap) is skipped
|
|
759
|
+
* with a warning instead of discarding the source's other rows.
|
|
760
|
+
*
|
|
761
|
+
* @throws the first build error when rows were present but none built, so the
|
|
762
|
+
* caller treats the source as failed (cache/static fallback) rather than as an
|
|
763
|
+
* intentionally empty catalog that an authoritative provider would prune to.
|
|
764
|
+
*/
|
|
711
765
|
function buildDiscoveredModelSpecs<TApi extends Api>(value: unknown): DiscoveredModelSet<TApi> {
|
|
712
766
|
const models: Model<TApi>[] = [];
|
|
713
767
|
const explicitKindModels = new Set<Model<TApi>>();
|
|
714
768
|
if (!Array.isArray(value)) {
|
|
715
769
|
return { models, explicitKindModels };
|
|
716
770
|
}
|
|
771
|
+
const failures: unknown[] = [];
|
|
717
772
|
for (const item of value) {
|
|
718
|
-
if (isModelLike(item))
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
773
|
+
if (!isModelLike(item)) continue;
|
|
774
|
+
let model: Model<TApi>;
|
|
775
|
+
try {
|
|
776
|
+
model = buildModel(item as ModelSpec<TApi>);
|
|
777
|
+
} catch (error) {
|
|
778
|
+
failures.push(error);
|
|
779
|
+
logger.warn("model discovery skipped a model that failed to build", {
|
|
780
|
+
provider: item.provider,
|
|
781
|
+
model: item.id,
|
|
782
|
+
error: error instanceof Error ? error.message : String(error),
|
|
783
|
+
});
|
|
784
|
+
continue;
|
|
722
785
|
}
|
|
786
|
+
models.push(model);
|
|
787
|
+
if (item.kind !== undefined) explicitKindModels.add(model);
|
|
723
788
|
}
|
|
789
|
+
if (models.length === 0 && failures.length > 0) throw failures[0];
|
|
724
790
|
return { models, explicitKindModels };
|
|
725
791
|
}
|
|
726
792
|
|
package/src/model-tokenizer.ts
CHANGED
|
@@ -27,6 +27,9 @@ function claudeTokenizer(identity: ModelIdentity): ModelTokenizer | undefined {
|
|
|
27
27
|
if (identity.family === "sonnet" || identity.family === "fable" || identity.family === "mythos") {
|
|
28
28
|
return revisionAtLeast(identity.revision, "5") ? "claude-v5-sonnet" : "claude-v3";
|
|
29
29
|
}
|
|
30
|
+
// Haiku 5.5 moved to the post-4.7 tokenizer (~30% more tokens than Haiku
|
|
31
|
+
// 4.5). No Haiku-measured vocabulary is embedded; Sonnet 5's is the nearest.
|
|
32
|
+
if (identity.family === "haiku" && revisionAtLeast(identity.revision, "5.5")) return "claude-v5-sonnet";
|
|
30
33
|
return "claude-v3";
|
|
31
34
|
}
|
|
32
35
|
|