@linxiraos/pi-catalog 1.1.27 → 1.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 +4 -17
- package/dist/types/compat/auth-ids.d.ts +2 -2
- 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/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 +2 -0
- package/dist/types/types.d.ts +106 -20
- 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/package.json +5 -5
- package/src/build.ts +39 -35
- package/src/compat/auth-ids.ts +4 -0
- package/src/compat/axes.ts +37 -3
- package/src/compat/cascade.ts +6 -1
- 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 +2 -0
- package/src/compat/reasoning-carry.ts +64 -0
- package/src/compat/request-size.ts +25 -0
- package/src/compat/resolve.ts +16 -2
- package/src/compat/rules/README.md +12 -2
- 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/stencil.kdl +1 -1
- package/src/compat/rules/classes/anthropic.kdl +87 -1
- package/src/compat/rules/classes/deepseek.kdl +6 -0
- package/src/compat/rules/classes/glm.kdl +16 -7
- package/src/compat/rules/classes/kimi.kdl +8 -0
- package/src/compat/rules/classes/minimax.kdl +31 -1
- package/src/compat/rules/classes/openai.kdl +5 -3
- package/src/compat/rules/classes/qwen.kdl +2 -1
- package/src/compat/rules/providers/abliteration.kdl +7 -0
- 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 +22 -0
- 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/groq.kdl +21 -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/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/openai-codex.kdl +17 -13
- package/src/compat/rules/providers/openai.kdl +22 -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/zhipu-coding-plan.kdl +10 -0
- package/src/compat/rules/runtime/behavior.kdl +7 -4
- package/src/compat/rules/taxonomy/_collapse.kdl +18 -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 +2 -1
- package/src/discovery/devin.ts +20 -2
- package/src/model-cache.ts +113 -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 +3 -2
- package/src/provider-models/descriptors.ts +4 -0
- package/src/provider-models/google.d.ts +23 -1
- package/src/provider-models/google.ts +70 -14
- package/src/provider-models/openai-compat.d.ts +19 -1
- package/src/provider-models/openai-compat.ts +215 -31
- package/src/provider-models/special.d.ts +2 -0
- package/src/provider-models/special.ts +15 -18
- package/src/types.d.ts +106 -20
- package/src/types.ts +149 -17
- 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/compat/taxonomy.d.ts
CHANGED
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runtime model-identity taxonomy: classifies wire model ids into
|
|
3
|
-
* `(class, family, revision)` ranks, applies reviewed identity overrides, and
|
|
4
|
-
* exposes the collapse/discovery vocabularies.
|
|
5
|
-
*
|
|
6
|
-
* Faithful port of the o2 reference (`taxonomy.rs` / `classify.rs`):
|
|
7
|
-
* matchers rank `exact(4) > bounded(3) > namespace(2) > prefix(1) > glob(0)`
|
|
8
|
-
* with token byte length as tiebreak; equal cross-class or cross-family ranks
|
|
9
|
-
* throw unless classification is `lenient` (discovery normalization).
|
|
10
|
-
*/
|
|
11
1
|
import type { Effort } from "../effort";
|
|
12
2
|
import type { ModelIdentity } from "./types";
|
|
13
3
|
/** Two classes, families, or reviewed override patterns tie for one model id. */
|
package/src/compat/taxonomy.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* with token byte length as tiebreak; equal cross-class or cross-family ranks
|
|
9
9
|
* throw unless classification is `lenient` (discovery normalization).
|
|
10
10
|
*/
|
|
11
|
+
import { LRUCache } from "@linxiraos/pi-utils/lru";
|
|
11
12
|
import type { Effort } from "../effort";
|
|
12
13
|
import { globMatch } from "./cascade";
|
|
13
14
|
import { formatRevision, parseRevisionPrefix } from "./revision";
|
|
@@ -433,8 +434,8 @@ export function stripThinkingVariantSuffix(model: string): string | undefined {
|
|
|
433
434
|
* @throws AmbiguousIdentityError on equal-rank cross-class, cross-family, or
|
|
434
435
|
* reviewed-pattern matches unless `opts.lenient`.
|
|
435
436
|
*/
|
|
436
|
-
|
|
437
|
-
const
|
|
437
|
+
// LRU sized above the bundled catalog so catalog-wide passes stay hot (a full `clear()` thrashed them).
|
|
438
|
+
const classifyMemo = new LRUCache<string, ModelIdentity>({ max: 16384 });
|
|
438
439
|
|
|
439
440
|
function classifyMemoKey(provider: string, modelId: string, lenient: boolean): string {
|
|
440
441
|
return `${provider.length}:${provider}${modelId.length}:${modelId}${lenient ? 1 : 0}`;
|
|
@@ -446,7 +447,6 @@ export function classifyModel(provider: string, modelId: string, opts?: Classify
|
|
|
446
447
|
const cached = classifyMemo.get(key);
|
|
447
448
|
if (cached !== undefined) return { ...cached };
|
|
448
449
|
const identity = classifyModelUncached(provider, modelId, opts);
|
|
449
|
-
if (classifyMemo.size >= CLASSIFY_MEMO_MAX) classifyMemo.clear();
|
|
450
450
|
classifyMemo.set(key, Object.freeze(identity));
|
|
451
451
|
return { ...identity };
|
|
452
452
|
}
|
package/src/compat/types.d.ts
CHANGED
|
@@ -593,6 +593,8 @@ export interface CompiledSeedModel {
|
|
|
593
593
|
baseUrl: string;
|
|
594
594
|
reasoning: boolean;
|
|
595
595
|
input: ("text" | "image")[];
|
|
596
|
+
/** Wire model id sent upstream when it differs from the row id (`request-model-id`). */
|
|
597
|
+
requestModelId?: string;
|
|
596
598
|
supportsTools?: boolean;
|
|
597
599
|
cost: TokenCost;
|
|
598
600
|
contextWindow: number | null;
|
|
@@ -622,6 +624,8 @@ export interface CompiledProvider {
|
|
|
622
624
|
id: string;
|
|
623
625
|
/** Preferred model id when no explicit selection is made. */
|
|
624
626
|
defaultModel: string;
|
|
627
|
+
/** Whether the provider participates in automatic default selection (defaults to true). */
|
|
628
|
+
automaticDefault?: boolean;
|
|
625
629
|
/** Env vars consulted, in order, for the runtime API-key fallback. */
|
|
626
630
|
envVars?: string[];
|
|
627
631
|
/** The runtime creates a model manager even without a valid API key. */
|
package/src/compat/types.ts
CHANGED
|
@@ -625,6 +625,8 @@ export interface CompiledSeedModel {
|
|
|
625
625
|
baseUrl: string;
|
|
626
626
|
reasoning: boolean;
|
|
627
627
|
input: ("text" | "image")[];
|
|
628
|
+
/** Wire model id sent upstream when it differs from the row id (`request-model-id`). */
|
|
629
|
+
requestModelId?: string;
|
|
628
630
|
supportsTools?: boolean;
|
|
629
631
|
cost: TokenCost;
|
|
630
632
|
contextWindow: number | null;
|
|
@@ -656,6 +658,8 @@ export interface CompiledProvider {
|
|
|
656
658
|
id: string;
|
|
657
659
|
/** Preferred model id when no explicit selection is made. */
|
|
658
660
|
defaultModel: string;
|
|
661
|
+
/** Whether the provider participates in automatic default selection (defaults to true). */
|
|
662
|
+
automaticDefault?: boolean;
|
|
659
663
|
/** Env vars consulted, in order, for the runtime API-key fallback. */
|
|
660
664
|
envVars?: string[];
|
|
661
665
|
/** The runtime creates a model manager even without a valid API key. */
|
|
@@ -63,13 +63,23 @@ export interface FetchAntigravityDiscoveryModelsOptions {
|
|
|
63
63
|
*/
|
|
64
64
|
collapseTable?: VariantCollapseTable;
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* A complete account roster, or a definitive 401/403 credential rejection.
|
|
68
|
+
* `null` from {@link fetchAntigravityDiscoveryModels} instead means a transient
|
|
69
|
+
* failure or invalid response, which must not replace an authoritative catalog.
|
|
70
|
+
*/
|
|
71
|
+
export interface AntigravityModelDiscoveryResult {
|
|
72
|
+
models: ModelSpec<"google-gemini-cli">[];
|
|
73
|
+
rejectedStatus?: 401 | 403;
|
|
74
|
+
}
|
|
66
75
|
/**
|
|
67
76
|
* Fetches discoverable Antigravity models and normalizes them into canonical model entries.
|
|
68
77
|
*
|
|
69
|
-
* Returns `null` on network
|
|
70
|
-
*
|
|
78
|
+
* Returns `null` on network, server, or payload failures; a 401/403 is reported
|
|
79
|
+
* separately so multi-account discovery can skip a rejected credential.
|
|
80
|
+
* A successful empty roster has `models: []` without `rejectedStatus`.
|
|
71
81
|
*/
|
|
72
|
-
export declare function fetchAntigravityDiscoveryModels(options: FetchAntigravityDiscoveryModelsOptions): Promise<
|
|
82
|
+
export declare function fetchAntigravityDiscoveryModels(options: FetchAntigravityDiscoveryModelsOptions): Promise<AntigravityModelDiscoveryResult | null>;
|
|
73
83
|
/** Advertised image model and serving endpoint for one Antigravity account. */
|
|
74
84
|
export interface AntigravityImageModel {
|
|
75
85
|
id: string;
|
|
@@ -153,19 +153,29 @@ export interface FetchAntigravityDiscoveryModelsOptions {
|
|
|
153
153
|
collapseTable?: VariantCollapseTable;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
/**
|
|
157
|
+
* A complete account roster, or a definitive 401/403 credential rejection.
|
|
158
|
+
* `null` from {@link fetchAntigravityDiscoveryModels} instead means a transient
|
|
159
|
+
* failure or invalid response, which must not replace an authoritative catalog.
|
|
160
|
+
*/
|
|
161
|
+
export interface AntigravityModelDiscoveryResult {
|
|
162
|
+
models: ModelSpec<"google-gemini-cli">[];
|
|
163
|
+
rejectedStatus?: 401 | 403;
|
|
164
|
+
}
|
|
165
|
+
|
|
156
166
|
/**
|
|
157
167
|
* Fetches discoverable Antigravity models and normalizes them into canonical model entries.
|
|
158
168
|
*
|
|
159
|
-
* Returns `null` on network
|
|
160
|
-
*
|
|
169
|
+
* Returns `null` on network, server, or payload failures; a 401/403 is reported
|
|
170
|
+
* separately so multi-account discovery can skip a rejected credential.
|
|
171
|
+
* A successful empty roster has `models: []` without `rejectedStatus`.
|
|
161
172
|
*/
|
|
162
173
|
export async function fetchAntigravityDiscoveryModels(
|
|
163
174
|
options: FetchAntigravityDiscoveryModelsOptions,
|
|
164
|
-
): Promise<
|
|
175
|
+
): Promise<AntigravityModelDiscoveryResult | null> {
|
|
165
176
|
const discovered = await fetchAntigravityDiscoveryResponse(options);
|
|
166
|
-
if (!discovered)
|
|
167
|
-
|
|
168
|
-
}
|
|
177
|
+
if (!discovered) return null;
|
|
178
|
+
if ("rejectedStatus" in discovered) return { models: [], rejectedStatus: discovered.rejectedStatus };
|
|
169
179
|
|
|
170
180
|
const models: ModelSpec<"google-gemini-cli">[] = [];
|
|
171
181
|
const apiModels = discovered.payload.models;
|
|
@@ -208,7 +218,7 @@ export async function fetchAntigravityDiscoveryModels(
|
|
|
208
218
|
options.collapseTable === undefined ? undefined : { table: options.collapseTable },
|
|
209
219
|
);
|
|
210
220
|
collapsed.sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
|
|
211
|
-
return collapsed;
|
|
221
|
+
return { models: collapsed };
|
|
212
222
|
}
|
|
213
223
|
|
|
214
224
|
/** Advertised image model and serving endpoint for one Antigravity account. */
|
|
@@ -222,14 +232,14 @@ export async function fetchAntigravityImageModel(
|
|
|
222
232
|
options: FetchAntigravityDiscoveryModelsOptions,
|
|
223
233
|
): Promise<AntigravityImageModel | null> {
|
|
224
234
|
const discovered = await fetchAntigravityDiscoveryResponse(options);
|
|
225
|
-
|
|
226
|
-
|
|
235
|
+
if (!discovered || "rejectedStatus" in discovered) return null;
|
|
236
|
+
const id = discovered.payload.imageGenerationModelIds?.find(modelId => modelId.length > 0);
|
|
237
|
+
return id ? { id, endpoint: discovered.endpoint } : null;
|
|
227
238
|
}
|
|
228
239
|
|
|
229
|
-
|
|
230
|
-
payload: AntigravityDiscoveryApiResponse;
|
|
231
|
-
|
|
232
|
-
}
|
|
240
|
+
type AntigravityDiscoveryResponse =
|
|
241
|
+
| { payload: AntigravityDiscoveryApiResponse; endpoint: string }
|
|
242
|
+
| { rejectedStatus: 401 | 403 };
|
|
233
243
|
|
|
234
244
|
async function fetchAntigravityDiscoveryResponse(
|
|
235
245
|
options: FetchAntigravityDiscoveryModelsOptions,
|
|
@@ -243,6 +253,8 @@ async function fetchAntigravityDiscoveryResponse(
|
|
|
243
253
|
? [trimTrailingSlashes(options.endpoint)]
|
|
244
254
|
: DEFAULT_ANTIGRAVITY_DISCOVERY_ENDPOINTS.map(trimTrailingSlashes);
|
|
245
255
|
|
|
256
|
+
let rejectedStatus: 401 | 403 | undefined;
|
|
257
|
+
let transientFailure = false;
|
|
246
258
|
for (const endpoint of endpoints) {
|
|
247
259
|
let response: Response;
|
|
248
260
|
try {
|
|
@@ -257,10 +269,14 @@ async function fetchAntigravityDiscoveryResponse(
|
|
|
257
269
|
signal: options.signal,
|
|
258
270
|
});
|
|
259
271
|
} catch {
|
|
272
|
+
transientFailure = true;
|
|
260
273
|
continue;
|
|
261
274
|
}
|
|
262
275
|
|
|
263
276
|
if (!response.ok) {
|
|
277
|
+
if (response.status === 401) rejectedStatus = 401;
|
|
278
|
+
else if (response.status === 403) rejectedStatus = 403;
|
|
279
|
+
else transientFailure = true;
|
|
264
280
|
continue;
|
|
265
281
|
}
|
|
266
282
|
|
|
@@ -268,16 +284,16 @@ async function fetchAntigravityDiscoveryResponse(
|
|
|
268
284
|
try {
|
|
269
285
|
payload = await response.json();
|
|
270
286
|
} catch {
|
|
287
|
+
transientFailure = true;
|
|
271
288
|
continue;
|
|
272
289
|
}
|
|
273
290
|
|
|
274
291
|
const parsed = parseAntigravityDiscoveryResponse(payload);
|
|
275
|
-
if (parsed) {
|
|
276
|
-
|
|
277
|
-
}
|
|
292
|
+
if (parsed) return { payload: parsed, endpoint };
|
|
293
|
+
transientFailure = true;
|
|
278
294
|
}
|
|
279
295
|
|
|
280
|
-
return null;
|
|
296
|
+
return rejectedStatus !== undefined && !transientFailure ? { rejectedStatus } : null;
|
|
281
297
|
}
|
|
282
298
|
|
|
283
299
|
function parseAntigravityDiscoveryResponse(value: unknown): AntigravityDiscoveryApiResponse | null {
|
package/src/discovery/cursor.ts
CHANGED
|
@@ -882,7 +882,8 @@ function buildRichCursorLane(
|
|
|
882
882
|
cursorRequiresDataRetention: details.requiresDataRetention,
|
|
883
883
|
cursorSupportsAgent: details.supportsAgent,
|
|
884
884
|
cursorSupportsSandboxing: details.supportsSandboxing,
|
|
885
|
-
isProviderDefault,
|
|
885
|
+
// Not marked `isProviderDefault`: Cursor keeps its bundled startup default, so
|
|
886
|
+
// the account default only picks the lane's default wire id and the badge.
|
|
886
887
|
isRecommended: isProviderDefault || details.defaultOn,
|
|
887
888
|
description:
|
|
888
889
|
variant?.tagline ??
|
package/src/discovery/devin.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type ClientModelConfig,
|
|
10
10
|
DisplayOption,
|
|
11
11
|
GetCliModelConfigsRequestSchema,
|
|
12
|
+
type GetCliModelConfigsResponse,
|
|
12
13
|
GetCliModelConfigsResponseSchema,
|
|
13
14
|
type Metadata,
|
|
14
15
|
MetadataSchema,
|
|
@@ -346,7 +347,7 @@ export async function fetchDevinModels(
|
|
|
346
347
|
GetCliModelConfigsResponseSchema,
|
|
347
348
|
new Uint8Array(await response.arrayBuffer()),
|
|
348
349
|
);
|
|
349
|
-
return decoded ? normalizeDevinModels(decoded
|
|
350
|
+
return decoded ? normalizeDevinModels(decoded, options.baseUrl) : null;
|
|
350
351
|
} catch {
|
|
351
352
|
return null;
|
|
352
353
|
}
|
|
@@ -497,9 +498,12 @@ function routeDevinFusionLead(spec: ModelSpec<"devin-agent">, lead: ModelSpec<"d
|
|
|
497
498
|
}
|
|
498
499
|
|
|
499
500
|
function normalizeDevinModels(
|
|
500
|
-
|
|
501
|
+
response: GetCliModelConfigsResponse,
|
|
501
502
|
baseUrlOverride: string | undefined,
|
|
502
503
|
): ModelSpec<"devin-agent">[] {
|
|
504
|
+
const configs = response.clientModelConfigs;
|
|
505
|
+
// The model the native client starts this account on (plan-dependent).
|
|
506
|
+
const accountDefaultUid = response.defaultOverrideModelConfig?.modelUid.trim();
|
|
503
507
|
const baseUrl = baseUrlOverride ?? DEVIN_DEFAULT_BASE_URL;
|
|
504
508
|
const specs: ModelSpec<"devin-agent">[] = [];
|
|
505
509
|
const seen = new Set<string>();
|
|
@@ -539,6 +543,7 @@ function normalizeDevinModels(
|
|
|
539
543
|
if (lead === null) continue;
|
|
540
544
|
const spec = devinModelSpec(config, uid, baseUrl, isAssignModelRouter);
|
|
541
545
|
if (lead !== undefined) routeDevinFusionLead(spec, devinModelSpec(liveConfigs.get(lead)!, lead, baseUrl, false));
|
|
546
|
+
if (uid === accountDefaultUid) spec.isProviderDefault = true;
|
|
542
547
|
specs.push(spec);
|
|
543
548
|
// A router is a server-side dispatcher, not an effort tier: it stays a
|
|
544
549
|
// standalone model even when upstream files it under a family.
|
|
@@ -554,5 +559,18 @@ function normalizeDevinModels(
|
|
|
554
559
|
const families = devinDynamicFamilies(lanes.values());
|
|
555
560
|
const dynamic = families.length > 0 ? collapseVariants(specs, { table: { families } }) : specs;
|
|
556
561
|
const collapsed = collapseVariants(dynamic);
|
|
562
|
+
// The account default outranks its family's own default lane when an effort
|
|
563
|
+
// routes to it, from server metadata or the static table: the family starts
|
|
564
|
+
// on that wire uid and effort. Other lanes (a no-thinking `off` default) keep
|
|
565
|
+
// the family's own default.
|
|
566
|
+
const accountFamily = collapsed.find(spec => spec.isProviderDefault && spec.thinking?.effortRouting);
|
|
567
|
+
const accountEffort = THINKING_EFFORTS.find(
|
|
568
|
+
effort => accountFamily?.thinking?.effortRouting?.[effort] === accountDefaultUid,
|
|
569
|
+
);
|
|
570
|
+
if (accountFamily?.thinking && accountEffort !== undefined) {
|
|
571
|
+
accountFamily.thinking = { ...accountFamily.thinking, defaultLevel: accountEffort };
|
|
572
|
+
if (accountFamily.id === accountDefaultUid) delete accountFamily.requestModelId;
|
|
573
|
+
else accountFamily.requestModelId = accountDefaultUid;
|
|
574
|
+
}
|
|
557
575
|
return collapsed.sort((a, b) => a.id.localeCompare(b.id));
|
|
558
576
|
}
|
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 {
|
|
@@ -280,8 +313,22 @@ function runModelCacheDb<T>(resolvedPath: string, shared: boolean, useDb: (db: D
|
|
|
280
313
|
}
|
|
281
314
|
|
|
282
315
|
function withModelCacheDb<T>(dbPath: string | undefined, useDb: (db: Database) => T): T {
|
|
283
|
-
const resolvedPath
|
|
284
|
-
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 };
|
|
285
332
|
}
|
|
286
333
|
|
|
287
334
|
function migrateCacheSchema(db: Database): void {
|
|
@@ -306,6 +353,12 @@ function migrateCacheSchema(db: Database): void {
|
|
|
306
353
|
// header matching was introduced.
|
|
307
354
|
db.run("ALTER TABLE model_cache ADD COLUMN header_restore_version INTEGER NOT NULL DEFAULT 0");
|
|
308
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
|
+
}
|
|
309
362
|
} finally {
|
|
310
363
|
stmt.finalize();
|
|
311
364
|
}
|
|
@@ -419,15 +472,14 @@ export function readModelCache<TApi extends Api>(
|
|
|
419
472
|
dbPath?: string,
|
|
420
473
|
): CacheEntry<TApi> | null {
|
|
421
474
|
try {
|
|
422
|
-
const resolvedPath = dbPath ?? getModelDbPath();
|
|
423
|
-
const key = readCacheKey(resolvedPath, providerId);
|
|
424
475
|
// Monotonic change signal: same-shaped WAL overwrites after checkpoint
|
|
425
476
|
// can leave every size:mtime pair identical, so file metadata alone
|
|
426
477
|
// cannot invalidate. PRAGMA data_version increments on each committed
|
|
427
478
|
// write transaction visible to a new reader. It is only comparable
|
|
428
479
|
// across calls on the same connection, so it gates the no-query fast
|
|
429
480
|
// path for the shared handle only.
|
|
430
|
-
const shared = dbPath
|
|
481
|
+
const { resolvedPath, shared } = resolveCacheDb(dbPath);
|
|
482
|
+
const key = readCacheKey(resolvedPath, providerId);
|
|
431
483
|
return runModelCacheDb(resolvedPath, shared, db => {
|
|
432
484
|
const dataVersion = shared ? dbDataVersion(db) : null;
|
|
433
485
|
const cached = readRowCache.get(key);
|
|
@@ -436,34 +488,32 @@ export function readModelCache<TApi extends Api>(
|
|
|
436
488
|
// cached row's updatedAt so a long-lived process goes stale.
|
|
437
489
|
return withFreshness(cached.entry as CacheEntry<TApi> | null, ttlMs, now);
|
|
438
490
|
}
|
|
439
|
-
// Any commit (to any provider's row) bumps data_version.
|
|
440
|
-
//
|
|
441
|
-
|
|
442
|
-
let
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
+
}
|
|
447
504
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
cached.row &&
|
|
452
|
-
row &&
|
|
453
|
-
row.materialization_policy === materializationPolicy() &&
|
|
454
|
-
cacheRowsEqual(cached.row, row)
|
|
455
|
-
) {
|
|
456
|
-
// Payload bytes unchanged; only side-table freshness may have moved.
|
|
457
|
-
entry = withFreshness(withRowState(cached.entry as CacheEntry<TApi>, row), ttlMs, now);
|
|
458
|
-
} 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);
|
|
459
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
|
+
}
|
|
460
514
|
}
|
|
461
515
|
if (readRowCache.size >= READ_ROW_CACHE_MAX) readRowCache.clear();
|
|
462
|
-
readRowCache.set(key, {
|
|
463
|
-
dataVersion,
|
|
464
|
-
row: entry === null ? null : row,
|
|
465
|
-
entry: entry as CacheEntry<Api> | null,
|
|
466
|
-
});
|
|
516
|
+
readRowCache.set(key, { dataVersion, meta, entry: entry as CacheEntry<Api> | null });
|
|
467
517
|
return entry;
|
|
468
518
|
});
|
|
469
519
|
} catch {
|
|
@@ -471,8 +521,20 @@ export function readModelCache<TApi extends Api>(
|
|
|
471
521
|
}
|
|
472
522
|
}
|
|
473
523
|
|
|
474
|
-
|
|
475
|
-
|
|
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 {
|
|
476
538
|
return (
|
|
477
539
|
left.version === right.version &&
|
|
478
540
|
left.materialization_policy === right.materialization_policy &&
|
|
@@ -482,7 +544,8 @@ function cacheRowsEqual(left: CacheRow, right: CacheRow): boolean {
|
|
|
482
544
|
left.header_omitted_model_ids === right.header_omitted_model_ids &&
|
|
483
545
|
left.unrestorable_header_model_ids === right.unrestorable_header_model_ids &&
|
|
484
546
|
left.header_restore_version === right.header_restore_version &&
|
|
485
|
-
left.
|
|
547
|
+
left.models_hash !== "" &&
|
|
548
|
+
left.models_hash === right.models_hash
|
|
486
549
|
);
|
|
487
550
|
}
|
|
488
551
|
|
|
@@ -498,7 +561,7 @@ function rowState(row: StoredRowState): { updatedAt: number; authoritative: bool
|
|
|
498
561
|
return { updatedAt: row.updated_at, authoritative: row.authoritative === 1 };
|
|
499
562
|
}
|
|
500
563
|
|
|
501
|
-
function withRowState<TApi extends Api>(entry: CacheEntry<TApi>, row:
|
|
564
|
+
function withRowState<TApi extends Api>(entry: CacheEntry<TApi>, row: StoredRowState): CacheEntry<TApi> {
|
|
502
565
|
const { updatedAt, authoritative } = rowState(row);
|
|
503
566
|
return entry.updatedAt === updatedAt && entry.authoritative === authoritative
|
|
504
567
|
? entry
|
|
@@ -648,6 +711,8 @@ export function writeModelCache<TApi extends Api>(
|
|
|
648
711
|
const serializedHeaderOmitted = JSON.stringify(headerOmittedModelIds);
|
|
649
712
|
const serializedUnrestorable = JSON.stringify(unrestorableHeaderModelIds);
|
|
650
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)}`;
|
|
651
716
|
const outcome = db
|
|
652
717
|
.transaction((): "skipped" | "refreshed" | "written" => {
|
|
653
718
|
const matchStmt = db.prepare<
|
|
@@ -662,7 +727,7 @@ export function writeModelCache<TApi extends Api>(
|
|
|
662
727
|
LEFT JOIN model_cache_refresh r ON r.provider_id = m.provider_id
|
|
663
728
|
WHERE m.provider_id = ? AND m.version = ? AND m.materialization_policy = ?
|
|
664
729
|
AND m.static_fingerprint = ? AND m.header_omitted_model_ids = ?
|
|
665
|
-
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 = ?`,
|
|
666
731
|
);
|
|
667
732
|
let stored: StoredRowState | null;
|
|
668
733
|
try {
|
|
@@ -674,7 +739,7 @@ export function writeModelCache<TApi extends Api>(
|
|
|
674
739
|
serializedHeaderOmitted,
|
|
675
740
|
serializedUnrestorable,
|
|
676
741
|
HEADER_RESTORE_VERSION,
|
|
677
|
-
|
|
742
|
+
modelsHash,
|
|
678
743
|
);
|
|
679
744
|
} finally {
|
|
680
745
|
matchStmt.finalize();
|
|
@@ -697,8 +762,8 @@ export function writeModelCache<TApi extends Api>(
|
|
|
697
762
|
`INSERT OR REPLACE INTO model_cache (
|
|
698
763
|
provider_id, version, materialization_policy, updated_at, authoritative, static_fingerprint,
|
|
699
764
|
header_omitted_model_ids, unrestorable_header_model_ids,
|
|
700
|
-
header_restore_version, models
|
|
701
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
765
|
+
header_restore_version, models_hash, models
|
|
766
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
702
767
|
[
|
|
703
768
|
providerId,
|
|
704
769
|
CACHE_SCHEMA_VERSION,
|
|
@@ -709,6 +774,7 @@ export function writeModelCache<TApi extends Api>(
|
|
|
709
774
|
serializedHeaderOmitted,
|
|
710
775
|
serializedUnrestorable,
|
|
711
776
|
HEADER_RESTORE_VERSION,
|
|
777
|
+
modelsHash,
|
|
712
778
|
serializedModels,
|
|
713
779
|
],
|
|
714
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
|