@linxiraos/pi-catalog 1.0.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 +999 -0
- package/README.md +43 -0
- package/dist/types/build.d.ts +14 -0
- package/dist/types/compat/anthropic.d.ts +18 -0
- package/dist/types/compat/apply.d.ts +7 -0
- package/dist/types/compat/bedrock.d.ts +3 -0
- package/dist/types/compat/devin.d.ts +9 -0
- package/dist/types/compat/openai.d.ts +26 -0
- package/dist/types/discovery/antigravity.d.ts +70 -0
- package/dist/types/discovery/codex.d.ts +36 -0
- package/dist/types/discovery/cursor-gen/agent_pb.d.ts +16769 -0
- package/dist/types/discovery/cursor.d.ts +23 -0
- package/dist/types/discovery/devin-gen/buf/validate/validate_pb.d.ts +1715 -0
- package/dist/types/discovery/devin-gen/exa/analytics_pb/analytics_pb.d.ts +693 -0
- package/dist/types/discovery/devin-gen/exa/api_server_pb/api_server_pb.d.ts +9158 -0
- package/dist/types/discovery/devin-gen/exa/auth_pb/auth_pb.d.ts +52 -0
- package/dist/types/discovery/devin-gen/exa/auto_cascade_common_pb/auto_cascade_common_pb.d.ts +276 -0
- package/dist/types/discovery/devin-gen/exa/bug_checker_pb/bug_checker_pb.d.ts +78 -0
- package/dist/types/discovery/devin-gen/exa/cascade_plugins_pb/cascade_plugins_pb.d.ts +999 -0
- package/dist/types/discovery/devin-gen/exa/chat_pb/chat_pb.d.ts +1704 -0
- package/dist/types/discovery/devin-gen/exa/code_edit/code_edit_pb/code_edit_pb.d.ts +656 -0
- package/dist/types/discovery/devin-gen/exa/codeium_common_pb/codeium_common_pb.d.ts +15014 -0
- package/dist/types/discovery/devin-gen/exa/context_module_pb/context_module_pb.d.ts +607 -0
- package/dist/types/discovery/devin-gen/exa/cortex_pb/cortex_pb.d.ts +11830 -0
- package/dist/types/discovery/devin-gen/exa/diff_action_pb/diff_action_pb.d.ts +248 -0
- package/dist/types/discovery/devin-gen/exa/index_pb/index_pb.d.ts +1747 -0
- package/dist/types/discovery/devin-gen/exa/knowledge_base_pb/knowledge_base_pb.d.ts +509 -0
- package/dist/types/discovery/devin-gen/exa/language_server_pb/language_server_pb.d.ts +9048 -0
- package/dist/types/discovery/devin-gen/exa/opensearch_clients_pb/opensearch_clients_pb.d.ts +1760 -0
- package/dist/types/discovery/devin-gen/exa/prompt_pb/prompt_pb.d.ts +286 -0
- package/dist/types/discovery/devin-gen/exa/reactive_component_pb/reactive_component_pb.d.ts +405 -0
- package/dist/types/discovery/devin-gen/exa/trust_pb/trust_pb.d.ts +582 -0
- package/dist/types/discovery/devin.d.ts +24 -0
- package/dist/types/discovery/gemini.d.ts +25 -0
- package/dist/types/discovery/gitlab-duo-workflow.d.ts +30 -0
- package/dist/types/discovery/index.d.ts +5 -0
- package/dist/types/discovery/openai-compatible.d.ts +74 -0
- package/dist/types/effort.d.ts +10 -0
- package/dist/types/fireworks-model-id.d.ts +23 -0
- package/dist/types/hosts.d.ts +164 -0
- package/dist/types/identity/bundled.d.ts +3 -0
- package/dist/types/identity/classify.d.ts +69 -0
- package/dist/types/identity/dialect.d.ts +3 -0
- package/dist/types/identity/family.d.ts +181 -0
- package/dist/types/identity/id.d.ts +12 -0
- package/dist/types/identity/index.d.ts +8 -0
- package/dist/types/identity/markers.d.ts +4 -0
- package/dist/types/identity/priority.d.ts +1 -0
- package/dist/types/identity/reference.d.ts +29 -0
- package/dist/types/index.d.ts +17 -0
- package/dist/types/model-cache.d.ts +23 -0
- package/dist/types/model-manager.d.ts +77 -0
- package/dist/types/model-thinking.d.ts +66 -0
- package/dist/types/models.d.ts +12 -0
- package/dist/types/provider-models/bundled-references.d.ts +13 -0
- package/dist/types/provider-models/cache-provider-id.d.ts +9 -0
- package/dist/types/provider-models/descriptor-types.d.ts +76 -0
- package/dist/types/provider-models/descriptors.d.ts +504 -0
- package/dist/types/provider-models/google.d.ts +27 -0
- package/dist/types/provider-models/index.d.ts +7 -0
- package/dist/types/provider-models/ollama.d.ts +22 -0
- package/dist/types/provider-models/openai-compat.d.ts +599 -0
- package/dist/types/provider-models/special.d.ts +53 -0
- package/dist/types/types.d.ts +811 -0
- package/dist/types/utils.d.ts +20 -0
- package/dist/types/variant-collapse.d.ts +133 -0
- package/dist/types/wire/alibaba-token-plan.d.ts +22 -0
- package/dist/types/wire/codex.d.ts +42 -0
- package/dist/types/wire/coreweave.d.ts +12 -0
- package/dist/types/wire/gemini-headers.d.ts +37 -0
- package/dist/types/wire/github-copilot.d.ts +39 -0
- package/package.json +101 -0
- package/src/build.ts +94 -0
- package/src/compat/anthropic.ts +179 -0
- package/src/compat/apply.ts +15 -0
- package/src/compat/bedrock.ts +153 -0
- package/src/compat/devin.ts +12 -0
- package/src/compat/openai.ts +796 -0
- package/src/discovery/antigravity.ts +253 -0
- package/src/discovery/codex.ts +346 -0
- package/src/discovery/cursor-gen/agent_pb.ts +19513 -0
- package/src/discovery/cursor.ts +396 -0
- package/src/discovery/devin-gen/buf/validate/validate_pb.ts +1862 -0
- package/src/discovery/devin-gen/exa/analytics_pb/analytics_pb.ts +871 -0
- package/src/discovery/devin-gen/exa/api_server_pb/api_server_pb.ts +11083 -0
- package/src/discovery/devin-gen/exa/auth_pb/auth_pb.ts +71 -0
- package/src/discovery/devin-gen/exa/auto_cascade_common_pb/auto_cascade_common_pb.ts +348 -0
- package/src/discovery/devin-gen/exa/bug_checker_pb/bug_checker_pb.ts +103 -0
- package/src/discovery/devin-gen/exa/cascade_plugins_pb/cascade_plugins_pb.ts +1198 -0
- package/src/discovery/devin-gen/exa/chat_pb/chat_pb.ts +2063 -0
- package/src/discovery/devin-gen/exa/code_edit/code_edit_pb/code_edit_pb.ts +810 -0
- package/src/discovery/devin-gen/exa/codeium_common_pb/codeium_common_pb.ts +18354 -0
- package/src/discovery/devin-gen/exa/context_module_pb/context_module_pb.ts +732 -0
- package/src/discovery/devin-gen/exa/cortex_pb/cortex_pb.ts +14277 -0
- package/src/discovery/devin-gen/exa/diff_action_pb/diff_action_pb.ts +312 -0
- package/src/discovery/devin-gen/exa/index_pb/index_pb.ts +2106 -0
- package/src/discovery/devin-gen/exa/knowledge_base_pb/knowledge_base_pb.ts +623 -0
- package/src/discovery/devin-gen/exa/language_server_pb/language_server_pb.ts +10918 -0
- package/src/discovery/devin-gen/exa/opensearch_clients_pb/opensearch_clients_pb.ts +2125 -0
- package/src/discovery/devin-gen/exa/prompt_pb/prompt_pb.ts +361 -0
- package/src/discovery/devin-gen/exa/reactive_component_pb/reactive_component_pb.ts +450 -0
- package/src/discovery/devin-gen/exa/trust_pb/trust_pb.ts +712 -0
- package/src/discovery/devin.ts +151 -0
- package/src/discovery/gemini.ts +260 -0
- package/src/discovery/gitlab-duo-workflow.ts +859 -0
- package/src/discovery/index.ts +5 -0
- package/src/discovery/openai-compatible.ts +249 -0
- package/src/effort.ts +18 -0
- package/src/fireworks-model-id.ts +50 -0
- package/src/hosts.ts +155 -0
- package/src/identity/bundled.ts +28 -0
- package/src/identity/classify.ts +219 -0
- package/src/identity/dialect.ts +42 -0
- package/src/identity/family.ts +366 -0
- package/src/identity/id.ts +81 -0
- package/src/identity/index.ts +8 -0
- package/src/identity/markers.ts +49 -0
- package/src/identity/priority.ts +59 -0
- package/src/identity/reference.ts +167 -0
- package/src/index.ts +17 -0
- package/src/model-cache.ts +285 -0
- package/src/model-manager.ts +672 -0
- package/src/model-thinking.ts +821 -0
- package/src/models.json +106099 -0
- package/src/models.json.d.ts +9 -0
- package/src/models.ts +93 -0
- package/src/provider-models/bundled-references.ts +75 -0
- package/src/provider-models/cache-provider-id.ts +68 -0
- package/src/provider-models/descriptor-types.ts +85 -0
- package/src/provider-models/descriptors.ts +584 -0
- package/src/provider-models/google.ts +107 -0
- package/src/provider-models/index.ts +7 -0
- package/src/provider-models/ollama.ts +204 -0
- package/src/provider-models/openai-compat.ts +5991 -0
- package/src/provider-models/special.ts +213 -0
- package/src/types.ts +952 -0
- package/src/utils.ts +69 -0
- package/src/variant-collapse.ts +1225 -0
- package/src/wire/alibaba-token-plan.ts +54 -0
- package/src/wire/codex.ts +60 -0
- package/src/wire/coreweave.ts +51 -0
- package/src/wire/gemini-headers.ts +74 -0
- package/src/wire/github-copilot.ts +115 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const LEADING_BRACKETED_AFFIX_PATTERN = /^(?:\s*(?:\[|【)[^\]】]+(?:\]|】)\s*)+/u;
|
|
2
|
+
const TRAILING_BRACKETED_AFFIX_PATTERN = /(?:\s*(?:\[|【)[^\]】]+(?:\]|】)\s*)+$/u;
|
|
3
|
+
const MODEL_ID_SEGMENT_PATTERN = /[a-z0-9.:-]+/g;
|
|
4
|
+
const MODEL_FAMILY_PREFIX_PATTERN =
|
|
5
|
+
/^(claude|gemini|gpt|grok|glm|qwen|deepseek|kimi|mimo|doubao|ernie|gpt-oss|gemma|minimax|step|command|jamba|llama|o[1345])/i;
|
|
6
|
+
|
|
7
|
+
function normalizeModelIdWhitespace(value: string): string {
|
|
8
|
+
return value.trim().replace(/\s+/g, " ");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Ordering for model-like segments: longest first, ties broken lexicographically. */
|
|
12
|
+
function compareSegmentPreference(left: string, right: string): number {
|
|
13
|
+
return left.length !== right.length ? right.length - left.length : left.localeCompare(right);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getModelLikeIdSegments(modelId: string): string[] {
|
|
17
|
+
const matches = normalizeModelIdWhitespace(modelId).toLowerCase().match(MODEL_ID_SEGMENT_PATTERN);
|
|
18
|
+
if (!matches) return [];
|
|
19
|
+
const segments = new Set<string>();
|
|
20
|
+
for (const segment of matches) {
|
|
21
|
+
if (MODEL_FAMILY_PREFIX_PATTERN.test(segment) && /\d/.test(segment)) segments.add(segment);
|
|
22
|
+
}
|
|
23
|
+
return [...segments].sort(compareSegmentPreference);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getLongestModelLikeIdSegment(modelId: string): string | undefined {
|
|
27
|
+
const matches = normalizeModelIdWhitespace(modelId).toLowerCase().match(MODEL_ID_SEGMENT_PATTERN);
|
|
28
|
+
if (!matches) return undefined;
|
|
29
|
+
let best: string | undefined;
|
|
30
|
+
for (const segment of matches) {
|
|
31
|
+
if (
|
|
32
|
+
MODEL_FAMILY_PREFIX_PATTERN.test(segment) &&
|
|
33
|
+
/\d/.test(segment) &&
|
|
34
|
+
(best === undefined || compareSegmentPreference(segment, best) < 0)
|
|
35
|
+
) {
|
|
36
|
+
best = segment;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return best;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function hasBracketAffixMarker(value: string): boolean {
|
|
43
|
+
for (let index = 0; index < value.length; index++) {
|
|
44
|
+
const code = value.charCodeAt(index);
|
|
45
|
+
if (code === 91 || code === 93 || code === 0x3010 || code === 0x3011) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Strip reseller / wrapper tags that are injected as bracketed affixes around an
|
|
54
|
+
* upstream model id, e.g.
|
|
55
|
+
* "[Kiro] claude-opus-4-8" -> "claude-opus-4-8"
|
|
56
|
+
* "[gcli转] gemini-3.1-pro-preview [假流]" -> "gemini-3.1-pro-preview"
|
|
57
|
+
*
|
|
58
|
+
* Candidates are returned most-stripped first: both ends, then leading-only, then trailing-only.
|
|
59
|
+
*/
|
|
60
|
+
export function getBracketStrippedModelIdCandidates(modelId: string): string[] {
|
|
61
|
+
if (!hasBracketAffixMarker(modelId)) return [];
|
|
62
|
+
const normalized = normalizeModelIdWhitespace(modelId);
|
|
63
|
+
if (!normalized) return [];
|
|
64
|
+
|
|
65
|
+
const strippedLeading = normalized.replace(LEADING_BRACKETED_AFFIX_PATTERN, "");
|
|
66
|
+
const withoutLeading = normalizeModelIdWhitespace(strippedLeading);
|
|
67
|
+
const withoutTrailing = normalizeModelIdWhitespace(normalized.replace(TRAILING_BRACKETED_AFFIX_PATTERN, ""));
|
|
68
|
+
const withoutBoth = normalizeModelIdWhitespace(strippedLeading.replace(TRAILING_BRACKETED_AFFIX_PATTERN, ""));
|
|
69
|
+
|
|
70
|
+
const candidates = new Set<string>();
|
|
71
|
+
for (const candidate of [withoutBoth, withoutLeading, withoutTrailing]) {
|
|
72
|
+
if (candidate && candidate !== normalized) {
|
|
73
|
+
candidates.add(candidate);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return [...candidates];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function stripBracketedModelIdAffixes(modelId: string): string | undefined {
|
|
80
|
+
return getBracketStrippedModelIdCandidates(modelId)[0];
|
|
81
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trailing-marker vocabulary shared by canonical-id resolution and
|
|
3
|
+
* proxy-reference lookup. A "marker" is a routing/quantization/effort suffix
|
|
4
|
+
* a reseller or aggregator appends to an upstream model id
|
|
5
|
+
* (`-thinking`, `:nitro`, `-fp8`, …) that does not change model identity.
|
|
6
|
+
*/
|
|
7
|
+
const TRAILING_MARKERS = [
|
|
8
|
+
"thinking",
|
|
9
|
+
"customtools",
|
|
10
|
+
"high",
|
|
11
|
+
"low",
|
|
12
|
+
"medium",
|
|
13
|
+
"minimal",
|
|
14
|
+
"xhigh",
|
|
15
|
+
"free",
|
|
16
|
+
"cloud",
|
|
17
|
+
"exacto",
|
|
18
|
+
"nitro",
|
|
19
|
+
"original",
|
|
20
|
+
"optimized",
|
|
21
|
+
"nvfp4",
|
|
22
|
+
"fp8",
|
|
23
|
+
"fp4",
|
|
24
|
+
"bf16",
|
|
25
|
+
"int8",
|
|
26
|
+
"int4",
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Markers treated as identity-preserving ONLY when recovering bundled metadata
|
|
31
|
+
* for a proxied model id, never during canonical-id coalescing: Perplexity's
|
|
32
|
+
* `sonar-pro-search` is a distinct model from `sonar-pro`, so canonical
|
|
33
|
+
* resolution must not strip `search`, while a proxy id like
|
|
34
|
+
* `claude-opus-4-6-search` should still inherit the upstream pricing/limits.
|
|
35
|
+
*/
|
|
36
|
+
const REFERENCE_ONLY_TRAILING_MARKERS = ["search"] as const;
|
|
37
|
+
|
|
38
|
+
function buildTrailingMarkerPattern(markers: readonly string[]): RegExp {
|
|
39
|
+
return new RegExp(`[-:](?:${markers.join("|")})$`, "i");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Marker pattern used by canonical-id resolution (`search` excluded). */
|
|
43
|
+
export const CANONICAL_TRAILING_MARKER_PATTERN = buildTrailingMarkerPattern(TRAILING_MARKERS);
|
|
44
|
+
|
|
45
|
+
/** Marker pattern used by proxy-reference lookup (`search` included). */
|
|
46
|
+
export const REFERENCE_TRAILING_MARKER_PATTERN = buildTrailingMarkerPattern([
|
|
47
|
+
...TRAILING_MARKERS,
|
|
48
|
+
...REFERENCE_ONLY_TRAILING_MARKERS,
|
|
49
|
+
]);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const DEFAULT_MODEL_PROVIDER_ORDER = [
|
|
2
|
+
// First-party / native account providers. Prefer these over relays when the
|
|
3
|
+
// same upstream model is available in more than one place.
|
|
4
|
+
"openai-codex",
|
|
5
|
+
"anthropic",
|
|
6
|
+
"openai",
|
|
7
|
+
"google-gemini-cli",
|
|
8
|
+
"google",
|
|
9
|
+
"google-vertex",
|
|
10
|
+
"kimi-code",
|
|
11
|
+
"moonshot",
|
|
12
|
+
"qwen-portal",
|
|
13
|
+
"alibaba-token-plan",
|
|
14
|
+
"zai",
|
|
15
|
+
"xai-oauth",
|
|
16
|
+
"xai",
|
|
17
|
+
"mistral",
|
|
18
|
+
"deepseek",
|
|
19
|
+
"groq",
|
|
20
|
+
|
|
21
|
+
// High-quality aggregators / hosted inference providers.
|
|
22
|
+
"fireworks",
|
|
23
|
+
"cerebras",
|
|
24
|
+
"baseten",
|
|
25
|
+
"openrouter",
|
|
26
|
+
"aimlapi",
|
|
27
|
+
"together",
|
|
28
|
+
|
|
29
|
+
// Generic gateways and editor/proxy providers. These are useful when picked
|
|
30
|
+
// explicitly, but should not win ambiguous automatic role selection.
|
|
31
|
+
"alibaba-coding-plan",
|
|
32
|
+
"umans",
|
|
33
|
+
"google-antigravity",
|
|
34
|
+
"opencode-zen",
|
|
35
|
+
"gitlab-duo",
|
|
36
|
+
"opencode-go",
|
|
37
|
+
"kilo",
|
|
38
|
+
"vercel-ai-gateway",
|
|
39
|
+
"cloudflare-ai-gateway",
|
|
40
|
+
"nanogpt",
|
|
41
|
+
"github-copilot",
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
44
|
+
function addProviderRank(rank: Map<string, number>, provider: string): void {
|
|
45
|
+
const normalized = provider.trim().toLowerCase();
|
|
46
|
+
if (!normalized || rank.has(normalized)) return;
|
|
47
|
+
rank.set(normalized, rank.size);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildModelProviderPriorityRank(configuredProviderOrder?: readonly string[]): Map<string, number> {
|
|
51
|
+
const rank = new Map<string, number>();
|
|
52
|
+
for (const provider of configuredProviderOrder ?? []) {
|
|
53
|
+
addProviderRank(rank, provider);
|
|
54
|
+
}
|
|
55
|
+
for (const provider of DEFAULT_MODEL_PROVIDER_ORDER) {
|
|
56
|
+
addProviderRank(rank, provider);
|
|
57
|
+
}
|
|
58
|
+
return rank;
|
|
59
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy/reseller reference lookup: given a custom model id served through a
|
|
3
|
+
* proxy (`[Kiro] claude-opus-4-8`, `gpt-5.4:cloud`, `vendor/claude-sonnet-4-6-thinking`),
|
|
4
|
+
* find the bundled upstream model so missing pricing/capability metadata can be
|
|
5
|
+
* inherited while keeping the custom transport.
|
|
6
|
+
*
|
|
7
|
+
* Kept separate from canonical-id resolution (`./equivalence`): this lookup
|
|
8
|
+
* may strip `search`-style markers and prefers cache-pricing-complete
|
|
9
|
+
* references, both of which would be wrong for canonical coalescing.
|
|
10
|
+
*/
|
|
11
|
+
import type { Api, Model, ThinkingConfig } from "../types";
|
|
12
|
+
import { getBracketStrippedModelIdCandidates, getLongestModelLikeIdSegment, getModelLikeIdSegments } from "./id";
|
|
13
|
+
import { REFERENCE_TRAILING_MARKER_PATTERN } from "./markers";
|
|
14
|
+
|
|
15
|
+
export interface ModelReferenceIndex {
|
|
16
|
+
exact: Map<string, Model<Api>>;
|
|
17
|
+
suffixAlias: Map<string, Model<Api>>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// xai-oauth subscription entries carry zero public pricing and inflated maxTokens;
|
|
21
|
+
// keep them provider-local so they cannot outrank paid/public Grok references.
|
|
22
|
+
export function isZeroCostXaiOAuthReference(candidate: Model<Api>): boolean {
|
|
23
|
+
return (
|
|
24
|
+
candidate.provider === "xai-oauth" &&
|
|
25
|
+
candidate.cost.input === 0 &&
|
|
26
|
+
candidate.cost.output === 0 &&
|
|
27
|
+
candidate.cost.cacheRead === 0 &&
|
|
28
|
+
candidate.cost.cacheWrite === 0
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Prefer the reference with the largest limits and complete cache pricing, then
|
|
33
|
+
// first-party OpenAI entries.
|
|
34
|
+
function shouldReplaceReference(existing: Model<Api> | undefined, candidate: Model<Api>): boolean {
|
|
35
|
+
if (!existing) return true;
|
|
36
|
+
if (candidate.contextWindow !== existing.contextWindow) {
|
|
37
|
+
return (candidate.contextWindow ?? 0) > (existing.contextWindow ?? 0);
|
|
38
|
+
}
|
|
39
|
+
if (candidate.maxTokens !== existing.maxTokens) {
|
|
40
|
+
return (candidate.maxTokens ?? 0) > (existing.maxTokens ?? 0);
|
|
41
|
+
}
|
|
42
|
+
const existingHasCachePricing = existing.cost.cacheRead > 0 || existing.cost.cacheWrite > 0;
|
|
43
|
+
const candidateHasCachePricing = candidate.cost.cacheRead > 0 || candidate.cost.cacheWrite > 0;
|
|
44
|
+
if (candidateHasCachePricing !== existingHasCachePricing) {
|
|
45
|
+
return candidateHasCachePricing;
|
|
46
|
+
}
|
|
47
|
+
return existing.provider !== "openai" && candidate.provider === "openai";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeReferenceKey(value: string): string {
|
|
51
|
+
return value.trim().toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build a reference index from a model catalog (typically the bundled models).
|
|
56
|
+
* Pure: callers are responsible for memoizing the result.
|
|
57
|
+
*/
|
|
58
|
+
export function buildModelReferenceIndex(models: Iterable<Model<Api>>): ModelReferenceIndex {
|
|
59
|
+
const exact = new Map<string, Model<Api>>();
|
|
60
|
+
for (const candidate of models) {
|
|
61
|
+
if (isZeroCostXaiOAuthReference(candidate)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const key = normalizeReferenceKey(candidate.id);
|
|
65
|
+
if (shouldReplaceReference(exact.get(key), candidate)) {
|
|
66
|
+
exact.set(key, candidate);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { exact, suffixAlias: buildSuffixAliasMap(exact) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildSuffixAliasMap(exactReferences: ReadonlyMap<string, Model<Api>>): Map<string, Model<Api>> {
|
|
73
|
+
const aliases = new Map<string, Model<Api>>();
|
|
74
|
+
for (const reference of exactReferences.values()) {
|
|
75
|
+
const slashIndex = reference.id.lastIndexOf("/");
|
|
76
|
+
if (slashIndex === -1) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const suffix = reference.id.slice(slashIndex + 1);
|
|
80
|
+
const alias = getLongestModelLikeIdSegment(suffix);
|
|
81
|
+
if (!alias) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (shouldReplaceReference(aliases.get(alias), reference)) {
|
|
85
|
+
aliases.set(alias, reference);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return aliases;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function stripReferenceTrailingMarker(candidate: string): string | undefined {
|
|
92
|
+
const match = REFERENCE_TRAILING_MARKER_PATTERN.exec(candidate);
|
|
93
|
+
return match ? candidate.slice(0, match.index) : undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getReferenceCandidateIds(modelId: string): string[] {
|
|
97
|
+
const candidates = new Set<string>();
|
|
98
|
+
const queue = [modelId];
|
|
99
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
100
|
+
const candidate = queue[index]?.trim();
|
|
101
|
+
if (!candidate || candidates.has(candidate)) continue;
|
|
102
|
+
candidates.add(candidate);
|
|
103
|
+
|
|
104
|
+
for (const stripped of getBracketStrippedModelIdCandidates(candidate)) {
|
|
105
|
+
queue.push(stripped);
|
|
106
|
+
}
|
|
107
|
+
for (const segment of getModelLikeIdSegments(candidate)) {
|
|
108
|
+
queue.push(segment);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const suffix of [":cloud", "-cloud"] as const) {
|
|
112
|
+
if (candidate.toLowerCase().endsWith(suffix)) {
|
|
113
|
+
queue.push(candidate.slice(0, -suffix.length));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const slashIndex = candidate.lastIndexOf("/");
|
|
118
|
+
if (slashIndex !== -1) {
|
|
119
|
+
queue.push(candidate.slice(slashIndex + 1));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const colonToDash = candidate.replace(/:/g, "-");
|
|
123
|
+
if (colonToDash !== candidate) {
|
|
124
|
+
queue.push(colonToDash);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const lowercased = candidate.toLowerCase();
|
|
128
|
+
if (lowercased !== candidate) {
|
|
129
|
+
queue.push(lowercased);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const strippedMarker = stripReferenceTrailingMarker(candidate);
|
|
133
|
+
if (strippedMarker) {
|
|
134
|
+
queue.push(strippedMarker);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return [...candidates];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Inherit bundled reference thinking only for same-provider matches. Wire routing
|
|
142
|
+
* (`effortRouting`) is provider-specific; cross-provider inheritance can rewrite
|
|
143
|
+
* gateway ids (e.g. Portkey `@modal/GLM-5-2-FP8` → devin `glm-5-2`).
|
|
144
|
+
*/
|
|
145
|
+
export function inheritReferenceThinking(
|
|
146
|
+
modelThinking: ThinkingConfig | undefined,
|
|
147
|
+
reference: Pick<Model<Api>, "provider" | "thinking"> | undefined,
|
|
148
|
+
provider: string,
|
|
149
|
+
): ThinkingConfig | undefined {
|
|
150
|
+
if (modelThinking !== undefined) return modelThinking;
|
|
151
|
+
if (!reference?.thinking) return undefined;
|
|
152
|
+
if (reference.provider !== provider) return undefined;
|
|
153
|
+
return reference.thinking;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Resolve a (possibly proxied/affixed) model id to its bundled upstream reference. */
|
|
157
|
+
export function resolveModelReference(modelId: string, index: ModelReferenceIndex): Model<Api> | undefined {
|
|
158
|
+
// Portkey/gateway wire ids (`@provider/model`) are opaque; fuzzy matching would
|
|
159
|
+
// map them to unrelated bundled entries (e.g. `@modal/GLM-5-2-FP8` → devin/glm-5-2).
|
|
160
|
+
if (modelId.startsWith("@")) return undefined;
|
|
161
|
+
for (const candidate of getReferenceCandidateIds(modelId)) {
|
|
162
|
+
const key = normalizeReferenceKey(candidate);
|
|
163
|
+
const reference = index.exact.get(key) ?? index.suffixAlias.get(key);
|
|
164
|
+
if (reference) return reference;
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export * from "./compat/openai";
|
|
2
|
+
export * from "./discovery";
|
|
3
|
+
export * from "./effort";
|
|
4
|
+
export * from "./fireworks-model-id";
|
|
5
|
+
export * from "./identity";
|
|
6
|
+
export * from "./model-cache";
|
|
7
|
+
export * from "./model-manager";
|
|
8
|
+
export * from "./model-thinking";
|
|
9
|
+
export * from "./models";
|
|
10
|
+
export * from "./provider-models";
|
|
11
|
+
export * from "./types";
|
|
12
|
+
export * from "./utils";
|
|
13
|
+
export * from "./variant-collapse";
|
|
14
|
+
export * from "./wire/codex";
|
|
15
|
+
export * from "./wire/coreweave";
|
|
16
|
+
export * from "./wire/gemini-headers";
|
|
17
|
+
export * from "./wire/github-copilot";
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite-backed model cache for atomic cross-process access.
|
|
3
|
+
* Replaces per-provider JSON files with a single cache.db.
|
|
4
|
+
*/
|
|
5
|
+
import { Database } from "bun:sqlite";
|
|
6
|
+
import { getModelDbPath } from "@linxiraos/pi-utils";
|
|
7
|
+
import type { Api, Model, ModelSpec } from "./types";
|
|
8
|
+
|
|
9
|
+
// Rows persist ModelSpec JSON (sparse `compat`, never the resolved record);
|
|
10
|
+
// the model manager rebuilds via `buildModel` on load. Request headers are
|
|
11
|
+
// intentionally omitted: arbitrary provider-defined header names can carry
|
|
12
|
+
// credentials. v12 invalidates Kimi Code rows carrying the blanket
|
|
13
|
+
// maxTokens: 32000 that predate per-family output caps (k3/k3-256k -> 131072,
|
|
14
|
+
// kimi-for-coding[-highspeed] -> 32768, #6711); v11 invalidates rows that may
|
|
15
|
+
// persist derived computer-use
|
|
16
|
+
// headers and records which model ids lost headers or cannot be rebuilt.
|
|
17
|
+
// v9 invalidated Kimi Code rows predating live effort and protocol metadata;
|
|
18
|
+
// v8 invalidated Codex discovery rows predating provider-native V2 compaction
|
|
19
|
+
// metadata; v7 invalidated rows predating the Antigravity Gemini budget-mode
|
|
20
|
+
// migration (cached specs still carrying `thinking.mode: "google-level"` and
|
|
21
|
+
// the old 3.5-flash effort routing); v6 invalidated rows that may contain the
|
|
22
|
+
// retired unknown-limit sentinels (222222/8888); v5 invalidated rows predating
|
|
23
|
+
// effort-tier variant collapsing (raw `-low`/`-high`/`-thinking` member ids);
|
|
24
|
+
// v4 dropped the pre-efforts ThinkingConfig shape.
|
|
25
|
+
const CACHE_SCHEMA_VERSION = 12;
|
|
26
|
+
const HEADER_RESTORE_VERSION = 1;
|
|
27
|
+
|
|
28
|
+
interface CacheRow {
|
|
29
|
+
provider_id: string;
|
|
30
|
+
version: number;
|
|
31
|
+
updated_at: number;
|
|
32
|
+
authoritative: number;
|
|
33
|
+
static_fingerprint: string;
|
|
34
|
+
models: string;
|
|
35
|
+
header_omitted_model_ids: string;
|
|
36
|
+
unrestorable_header_model_ids: string;
|
|
37
|
+
header_restore_version: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface TableInfoRow {
|
|
41
|
+
name: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface CacheEntry<TApi extends Api = Api> {
|
|
45
|
+
models: ModelSpec<TApi>[];
|
|
46
|
+
fresh: boolean;
|
|
47
|
+
authoritative: boolean;
|
|
48
|
+
updatedAt: number;
|
|
49
|
+
/** Model ids whose live headers were intentionally omitted from disk. */
|
|
50
|
+
headerOmittedModelIds: readonly string[];
|
|
51
|
+
/** Header-bearing model ids that cannot be rebuilt from the static source. */
|
|
52
|
+
unrestorableHeaderModelIds: readonly string[];
|
|
53
|
+
/** Whether unrestorable markers predate request-model header matching. */
|
|
54
|
+
legacyHeaderRestoreMarkers: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Hash of the static catalog slice that was merged into `models` when this
|
|
57
|
+
* row was written. `resolveProviderModels` compares against the current
|
|
58
|
+
* static fingerprint and bypasses the static+cache re-merge when they
|
|
59
|
+
* match — the cache already incorporates the same static state.
|
|
60
|
+
*/
|
|
61
|
+
staticFingerprint: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let sharedDb: Database | null = null;
|
|
65
|
+
let sharedDbPath: string | null = null;
|
|
66
|
+
|
|
67
|
+
function openDb(resolvedPath: string): Database {
|
|
68
|
+
const db = new Database(resolvedPath, { create: true });
|
|
69
|
+
// Install the busy handler BEFORE any lock-taking statement. See
|
|
70
|
+
// https://github.com/can1357/oh-my-pi/issues/2421.
|
|
71
|
+
db.run("PRAGMA busy_timeout = 3000");
|
|
72
|
+
// Schema invalidation can delete rows containing credentials written by old
|
|
73
|
+
// versions. Overwrite deleted SQLite cells instead of leaving their bytes in
|
|
74
|
+
// free pages where a raw scan of models.db can still recover them (#5780).
|
|
75
|
+
db.run("PRAGMA secure_delete = ON");
|
|
76
|
+
db.run("PRAGMA journal_mode = WAL");
|
|
77
|
+
db.run(`
|
|
78
|
+
CREATE TABLE IF NOT EXISTS model_cache (
|
|
79
|
+
provider_id TEXT PRIMARY KEY,
|
|
80
|
+
version INTEGER NOT NULL,
|
|
81
|
+
updated_at INTEGER NOT NULL,
|
|
82
|
+
authoritative INTEGER NOT NULL DEFAULT 0,
|
|
83
|
+
static_fingerprint TEXT NOT NULL DEFAULT '',
|
|
84
|
+
header_omitted_model_ids TEXT NOT NULL DEFAULT '[]',
|
|
85
|
+
unrestorable_header_model_ids TEXT NOT NULL DEFAULT '[]',
|
|
86
|
+
header_restore_version INTEGER NOT NULL DEFAULT 0,
|
|
87
|
+
models TEXT NOT NULL
|
|
88
|
+
)
|
|
89
|
+
`);
|
|
90
|
+
migrateCacheSchema(db);
|
|
91
|
+
return db;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getSharedDb(): Database {
|
|
95
|
+
const resolvedPath = getModelDbPath();
|
|
96
|
+
if (sharedDb && sharedDbPath === resolvedPath) {
|
|
97
|
+
return sharedDb;
|
|
98
|
+
}
|
|
99
|
+
if (sharedDb) {
|
|
100
|
+
sharedDb.close();
|
|
101
|
+
}
|
|
102
|
+
const db = openDb(resolvedPath);
|
|
103
|
+
sharedDb = db;
|
|
104
|
+
sharedDbPath = resolvedPath;
|
|
105
|
+
return db;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function withModelCacheDb<T>(dbPath: string | undefined, useDb: (db: Database) => T): T {
|
|
109
|
+
if (!dbPath) return useDb(getSharedDb());
|
|
110
|
+
const db = openDb(dbPath);
|
|
111
|
+
try {
|
|
112
|
+
return useDb(db);
|
|
113
|
+
} finally {
|
|
114
|
+
db.close();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function migrateCacheSchema(db: Database): void {
|
|
119
|
+
const stmt = db.prepare("PRAGMA table_info(model_cache)");
|
|
120
|
+
try {
|
|
121
|
+
const columns = stmt.all() as TableInfoRow[];
|
|
122
|
+
if (!columns.some(column => column.name === "static_fingerprint")) {
|
|
123
|
+
db.run("ALTER TABLE model_cache ADD COLUMN static_fingerprint TEXT NOT NULL DEFAULT ''");
|
|
124
|
+
}
|
|
125
|
+
if (!columns.some(column => column.name === "header_omitted_model_ids")) {
|
|
126
|
+
db.run("ALTER TABLE model_cache ADD COLUMN header_omitted_model_ids TEXT NOT NULL DEFAULT '[]'");
|
|
127
|
+
}
|
|
128
|
+
if (!columns.some(column => column.name === "unrestorable_header_model_ids")) {
|
|
129
|
+
db.run("ALTER TABLE model_cache ADD COLUMN unrestorable_header_model_ids TEXT NOT NULL DEFAULT '[]'");
|
|
130
|
+
}
|
|
131
|
+
if (!columns.some(column => column.name === "header_restore_version")) {
|
|
132
|
+
// Existing v10 rows get 0, distinguishing markers produced by the
|
|
133
|
+
// old id-only header matcher from rows written after request-model
|
|
134
|
+
// header matching was introduced.
|
|
135
|
+
db.run("ALTER TABLE model_cache ADD COLUMN header_restore_version INTEGER NOT NULL DEFAULT 0");
|
|
136
|
+
}
|
|
137
|
+
} finally {
|
|
138
|
+
stmt.finalize();
|
|
139
|
+
}
|
|
140
|
+
// Delete rows written under any older schema so they cannot be reused. The
|
|
141
|
+
// legacy `UPDATE ... WHERE version = 2` migration silently promoted the very
|
|
142
|
+
// first cache version to whatever the current one is, defeating every
|
|
143
|
+
// subsequent invalidation (see #4146: pre-V2 Codex rows kept the legacy
|
|
144
|
+
// compaction path even after CACHE_SCHEMA_VERSION was bumped).
|
|
145
|
+
db.run("DELETE FROM model_cache WHERE version <> ?", [CACHE_SCHEMA_VERSION]);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function readModelCache<TApi extends Api>(
|
|
149
|
+
providerId: string,
|
|
150
|
+
ttlMs: number,
|
|
151
|
+
now: () => number,
|
|
152
|
+
dbPath?: string,
|
|
153
|
+
): CacheEntry<TApi> | null {
|
|
154
|
+
try {
|
|
155
|
+
return withModelCacheDb(dbPath, db => {
|
|
156
|
+
const stmt = db.query<CacheRow, [string]>("SELECT * FROM model_cache WHERE provider_id = ?");
|
|
157
|
+
try {
|
|
158
|
+
const row = stmt.get(providerId);
|
|
159
|
+
if (!row || row.version !== CACHE_SCHEMA_VERSION) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
const models = JSON.parse(row.models) as ModelSpec<TApi>[];
|
|
163
|
+
const parsedHeaderModelIds: unknown = JSON.parse(row.header_omitted_model_ids);
|
|
164
|
+
const headerOmittedModelIds = Array.isArray(parsedHeaderModelIds)
|
|
165
|
+
? parsedHeaderModelIds.filter((id): id is string => typeof id === "string")
|
|
166
|
+
: [];
|
|
167
|
+
const parsedUnrestorableModelIds: unknown = JSON.parse(row.unrestorable_header_model_ids);
|
|
168
|
+
const unrestorableHeaderModelIds = Array.isArray(parsedUnrestorableModelIds)
|
|
169
|
+
? parsedUnrestorableModelIds.filter((id): id is string => typeof id === "string")
|
|
170
|
+
: [];
|
|
171
|
+
const ageMs = now() - row.updated_at;
|
|
172
|
+
const fresh = Number.isFinite(ageMs) && ageMs >= 0 && ageMs <= ttlMs;
|
|
173
|
+
return {
|
|
174
|
+
models,
|
|
175
|
+
fresh,
|
|
176
|
+
authoritative: row.authoritative === 1,
|
|
177
|
+
updatedAt: row.updated_at,
|
|
178
|
+
headerOmittedModelIds,
|
|
179
|
+
unrestorableHeaderModelIds,
|
|
180
|
+
legacyHeaderRestoreMarkers: row.header_restore_version < HEADER_RESTORE_VERSION,
|
|
181
|
+
staticFingerprint: row.static_fingerprint ?? "",
|
|
182
|
+
};
|
|
183
|
+
} finally {
|
|
184
|
+
stmt.finalize();
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Whether a live model carries at least one request header. */
|
|
193
|
+
function hasModelHeaders(model: Model<Api>): boolean {
|
|
194
|
+
const headers = model.headers;
|
|
195
|
+
if (!headers) return false;
|
|
196
|
+
for (const _key in headers) return true;
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Project a live model to cache-safe metadata.
|
|
202
|
+
*
|
|
203
|
+
* Headers are never persisted: custom/runtime providers may use arbitrary
|
|
204
|
+
* credential header names, so no name-based filter can be complete. The
|
|
205
|
+
* separately persisted model-id list lets the manager restore matching static
|
|
206
|
+
* headers and reject/refetch dynamic-only cached models that need live headers.
|
|
207
|
+
*/
|
|
208
|
+
function toCachedModelSpec<TApi extends Api>(model: Model<TApi>): ModelSpec<TApi> {
|
|
209
|
+
const { headers: _headers, compatConfig, supportsComputerUseConfig, ...rest } = model;
|
|
210
|
+
return { ...rest, supportsComputerUse: supportsComputerUseConfig, compat: compatConfig };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Whether two in-memory header records are byte-for-byte equivalent. */
|
|
214
|
+
function headersEqual(left: Record<string, string> | undefined, right: Record<string, string> | undefined): boolean {
|
|
215
|
+
if (!left || !right) return left === right;
|
|
216
|
+
for (const key in left) {
|
|
217
|
+
if (right[key] !== left[key]) return false;
|
|
218
|
+
}
|
|
219
|
+
for (const key in right) {
|
|
220
|
+
if (!(key in left)) return false;
|
|
221
|
+
}
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function writeModelCache<TApi extends Api>(
|
|
226
|
+
providerId: string,
|
|
227
|
+
updatedAt: number,
|
|
228
|
+
models: Model<TApi>[],
|
|
229
|
+
authoritative: boolean,
|
|
230
|
+
staticFingerprint: string,
|
|
231
|
+
dbPath?: string,
|
|
232
|
+
staticHeaderSources: readonly Model<TApi>[] = [],
|
|
233
|
+
restorableHeaderFallback?: Record<string, string>,
|
|
234
|
+
): void {
|
|
235
|
+
try {
|
|
236
|
+
withModelCacheDb(dbPath, db => {
|
|
237
|
+
const headerOmittedModelIds: string[] = [];
|
|
238
|
+
const unrestorableHeaderModelIds: string[] = [];
|
|
239
|
+
const cachedModels: ModelSpec<TApi>[] = [];
|
|
240
|
+
const staticById = new Map(staticHeaderSources.map(model => [model.id, model]));
|
|
241
|
+
for (const model of models) {
|
|
242
|
+
if (hasModelHeaders(model)) {
|
|
243
|
+
headerOmittedModelIds.push(model.id);
|
|
244
|
+
// Synthesized variants (e.g. Copilot `-1m`) have no same-id static
|
|
245
|
+
// entry; their headers come from the `requestModelId` base. Match
|
|
246
|
+
// against that source too, else they are wrongly flagged
|
|
247
|
+
// unrestorable and dropped on the next offline read (#6037, #6284).
|
|
248
|
+
const staticHeaderSource =
|
|
249
|
+
staticById.get(model.id) ?? (model.requestModelId ? staticById.get(model.requestModelId) : undefined);
|
|
250
|
+
// A model with no static source is still restorable when its live
|
|
251
|
+
// headers equal the provider's trusted constant (a compile-time,
|
|
252
|
+
// non-credential value the reader can reattach by value). This keeps
|
|
253
|
+
// reference-less Copilot models (e.g. claude-opus-5) alive offline.
|
|
254
|
+
const matchesStatic = staticHeaderSource
|
|
255
|
+
? headersEqual(model.headers, staticHeaderSource.headers)
|
|
256
|
+
: headersEqual(model.headers, restorableHeaderFallback);
|
|
257
|
+
if (!matchesStatic) {
|
|
258
|
+
unrestorableHeaderModelIds.push(model.id);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
cachedModels.push(toCachedModelSpec(model));
|
|
262
|
+
}
|
|
263
|
+
db.run(
|
|
264
|
+
`INSERT OR REPLACE INTO model_cache (
|
|
265
|
+
provider_id, version, updated_at, authoritative, static_fingerprint,
|
|
266
|
+
header_omitted_model_ids, unrestorable_header_model_ids,
|
|
267
|
+
header_restore_version, models
|
|
268
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
269
|
+
[
|
|
270
|
+
providerId,
|
|
271
|
+
CACHE_SCHEMA_VERSION,
|
|
272
|
+
updatedAt,
|
|
273
|
+
authoritative ? 1 : 0,
|
|
274
|
+
staticFingerprint,
|
|
275
|
+
JSON.stringify(headerOmittedModelIds),
|
|
276
|
+
JSON.stringify(unrestorableHeaderModelIds),
|
|
277
|
+
HEADER_RESTORE_VERSION,
|
|
278
|
+
JSON.stringify(cachedModels),
|
|
279
|
+
],
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
} catch {
|
|
283
|
+
// Cache writes are best-effort; failures should not break model resolution.
|
|
284
|
+
}
|
|
285
|
+
}
|