@jeffusion/bungee-core 5.10.0 → 5.11.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/dist/main.js +31344 -17334
- package/dist/plugins/ai-transformer/index.js.map +2 -2
- package/dist/plugins/anthropic-request-sanitizer/index.js.map +2 -2
- package/dist/plugins/anthropic-tool-name-transformer/index.js.map +2 -2
- package/dist/plugins/chatgpt-oauth/control.js +513 -1
- package/dist/plugins/chatgpt-oauth/control.js.map +12 -4
- package/dist/plugins/chatgpt-oauth/index.js +511 -4
- package/dist/plugins/chatgpt-oauth/index.js.map +13 -5
- package/dist/plugins/chatgpt-oauth/manifest.json +19 -0
- package/dist/plugins/deepseek-reasoning-fix/index.js.map +2 -2
- package/dist/plugins/key-access/index.js.map +2 -2
- package/dist/plugins/key-rate-limit/index.js.map +2 -2
- package/dist/plugins/local-accounts/control.js.map +2 -2
- package/dist/plugins/local-accounts/index.js.map +2 -2
- package/dist/plugins/model-mapping/control.js +72 -17741
- package/dist/plugins/model-mapping/control.js.map +6 -58
- package/dist/plugins/model-mapping/index.js +32 -17433
- package/dist/plugins/model-mapping/index.js.map +6 -58
- package/dist/plugins/model-mapping/manifest.json +30 -14
- package/dist/plugins/models-dev/control.js +1042 -0
- package/dist/plugins/models-dev/control.js.map +16 -0
- package/dist/plugins/models-dev/index.js +15418 -0
- package/dist/plugins/models-dev/index.js.map +134 -0
- package/dist/plugins/models-dev/manifest.json +199 -0
- package/dist/plugins/models-dev/ui/ModelsDevSettings.svelte +120 -0
- package/dist/plugins/openai-messages-to-chat/index.js.map +2 -2
- package/dist/plugins/signature-repair/index.js.map +2 -2
- package/dist/plugins/token-budget/control.js +524 -9
- package/dist/plugins/token-budget/control.js.map +12 -4
- package/dist/plugins/token-budget/index.js +519 -12
- package/dist/plugins/token-budget/index.js.map +13 -6
- package/dist/plugins/token-budget/manifest.json +16 -2
- package/dist/plugins/token-metering/index.js +93 -2
- package/dist/plugins/token-metering/index.js.map +12 -6
- package/dist/plugins/token-stats/control.js +174 -312
- package/dist/plugins/token-stats/control.js.map +7 -7
- package/dist/plugins/token-stats/index.js +283 -486
- package/dist/plugins/token-stats/index.js.map +16 -12
- package/dist/plugins/token-stats/manifest.json +99 -12
- package/dist/plugins/token-stats/ui/ClientModelPicker.svelte +76 -0
- package/dist/plugins/token-stats/ui/PriceModelPicker.svelte +87 -0
- package/dist/plugins/token-stats/ui/TokenStatsSettings.svelte +136 -80
- package/dist/plugins/token-stats/ui/client-model-search.test.ts +58 -0
- package/dist/plugins/token-stats/ui/client-model-search.ts +16 -0
- package/dist/plugins/token-stats/ui/model-search.ts +45 -0
- package/dist/plugins/token-stats/ui/pricing-catalog.test.ts +114 -0
- package/dist/plugins/token-stats/ui/pricing-catalog.ts +17 -0
- package/package.json +2 -2
|
@@ -0,0 +1,1042 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// plugins/models-dev/contract.ts
|
|
3
|
+
var MODELS_DEV_SOURCE_URL = "https://models.dev/api.json";
|
|
4
|
+
var MODELS_DEV_CATALOG_SERVICE_ID = "models-dev.catalog.v1";
|
|
5
|
+
var MODELS_DEV_CATALOG_CONTRACT_VERSION = 1;
|
|
6
|
+
var MODELS_DEV_CATALOG_SNAPSHOT_ID = "models-dev.catalog.snapshot.v1";
|
|
7
|
+
var MODELS_DEV_CATALOG_SNAPSHOT_CONTRACT_VERSION = 1;
|
|
8
|
+
var MODELS_DEV_CATALOG_SNAPSHOT_SCHEMA = Object.freeze({
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
version: { type: "number", integer: true, minimum: 1 },
|
|
12
|
+
fetchedAt: { type: "number", minimum: 0 },
|
|
13
|
+
catalog: { type: "json", maxBytes: 32 * 1024 * 1024 }
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
var MODELS_DEV_CATALOG_SNAPSHOT_CONTRACT = Object.freeze({
|
|
17
|
+
id: MODELS_DEV_CATALOG_SNAPSHOT_ID,
|
|
18
|
+
version: MODELS_DEV_CATALOG_SNAPSHOT_CONTRACT_VERSION,
|
|
19
|
+
content: MODELS_DEV_CATALOG_SNAPSHOT_SCHEMA
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// plugins/models-dev/server/model-names.ts
|
|
23
|
+
function resourceName(name) {
|
|
24
|
+
return name.replace(/^(?:projects\/[^/]+\/locations\/[^/]+\/)?publishers\/google\/models\//, "").replace(/^models\//, "");
|
|
25
|
+
}
|
|
26
|
+
function claudeVersion(name) {
|
|
27
|
+
return name.replace(/^(claude-(?:(?:opus|sonnet|haiku)-)?\d+)-(\d+)(?=-|$)/, "$1.$2");
|
|
28
|
+
}
|
|
29
|
+
function withoutDate(name) {
|
|
30
|
+
return name.replace(/-(?:\d{8}|\d{4}-\d{2}-\d{2})$/, "");
|
|
31
|
+
}
|
|
32
|
+
function withoutEffort(name) {
|
|
33
|
+
return name.replace(/-(?:minimal|low|medium|high|xhigh)$/, "");
|
|
34
|
+
}
|
|
35
|
+
function withoutPreview(name) {
|
|
36
|
+
return name.replace(/-(?:latest|preview|exp)$/, "");
|
|
37
|
+
}
|
|
38
|
+
function estimateNames(model) {
|
|
39
|
+
const names = new Set([model.trim().toLowerCase()]);
|
|
40
|
+
const transforms = [resourceName, claudeVersion, withoutDate, withoutEffort, withoutPreview];
|
|
41
|
+
for (const name of names) {
|
|
42
|
+
for (const transform of transforms) {
|
|
43
|
+
const candidate = transform(name);
|
|
44
|
+
if (candidate && names.size < 32)
|
|
45
|
+
names.add(candidate);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return [...names];
|
|
49
|
+
}
|
|
50
|
+
function canonicalModelName(model) {
|
|
51
|
+
return claudeVersion(resourceName(model.trim().toLowerCase()));
|
|
52
|
+
}
|
|
53
|
+
function modelFamily(model) {
|
|
54
|
+
let name = canonicalModelName(model);
|
|
55
|
+
for (;; ) {
|
|
56
|
+
const next = withoutPreview(withoutEffort(withoutDate(name)));
|
|
57
|
+
if (next === name)
|
|
58
|
+
return name;
|
|
59
|
+
name = next;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function modelDate(model) {
|
|
63
|
+
return model.toLowerCase().match(/-(\d{8}|\d{4}-\d{2}-\d{2})(?:-(?:minimal|low|medium|high|xhigh|latest|preview|exp))*$/)?.[1]?.replaceAll("-", "") ?? "";
|
|
64
|
+
}
|
|
65
|
+
function originalLab(model) {
|
|
66
|
+
const name = resourceName(model.trim().toLowerCase());
|
|
67
|
+
if (/^(?:gpt-|o\d+(?:-|$))/.test(name))
|
|
68
|
+
return "openai";
|
|
69
|
+
if (name.startsWith("claude-"))
|
|
70
|
+
return "anthropic";
|
|
71
|
+
if (name.startsWith("gemini-"))
|
|
72
|
+
return "google";
|
|
73
|
+
if (name.startsWith("grok-"))
|
|
74
|
+
return "xai";
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// plugins/models-dev/server/catalog.ts
|
|
79
|
+
var MAX_PROVIDERS = 4096;
|
|
80
|
+
var MAX_MODELS_PER_PROVIDER = 16384;
|
|
81
|
+
var MAX_MODEL_OPTION_PAGE_SIZE = 100;
|
|
82
|
+
var MAX_RESOLVE_CANDIDATES = 20000;
|
|
83
|
+
function isRecord(value) {
|
|
84
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
85
|
+
}
|
|
86
|
+
function finiteNonNegative(value) {
|
|
87
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
88
|
+
}
|
|
89
|
+
function contextTier(cost, minimumInputTokens) {
|
|
90
|
+
if (!isRecord(cost) || !Number.isSafeInteger(minimumInputTokens) || minimumInputTokens < 0)
|
|
91
|
+
return null;
|
|
92
|
+
const input = finiteNonNegative(cost.input);
|
|
93
|
+
const output = finiteNonNegative(cost.output);
|
|
94
|
+
if (input === null || output === null)
|
|
95
|
+
return null;
|
|
96
|
+
return Object.freeze({
|
|
97
|
+
minimumInputTokens,
|
|
98
|
+
input,
|
|
99
|
+
output,
|
|
100
|
+
cacheRead: finiteNonNegative(cost.cache_read),
|
|
101
|
+
cacheWrite: finiteNonNegative(cost.cache_write)
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function contextTiers(cost) {
|
|
105
|
+
if (Array.isArray(cost.tiers) && cost.tiers.length > 0) {
|
|
106
|
+
const tiers = [];
|
|
107
|
+
const thresholds = new Set;
|
|
108
|
+
for (const value of cost.tiers) {
|
|
109
|
+
const tier = isRecord(value) && isRecord(value.tier) && (value.tier.type === undefined || value.tier.type === "context") ? contextTier(value, value.tier.size) : null;
|
|
110
|
+
if (tier === null || thresholds.has(tier.minimumInputTokens)) {
|
|
111
|
+
tiers.length = 0;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
thresholds.add(tier.minimumInputTokens);
|
|
115
|
+
tiers.push(tier);
|
|
116
|
+
}
|
|
117
|
+
if (tiers.length > 0)
|
|
118
|
+
return Object.freeze(tiers.sort((a, b) => a.minimumInputTokens - b.minimumInputTokens));
|
|
119
|
+
}
|
|
120
|
+
const legacy = contextTier(cost.context_over_200k, 200001);
|
|
121
|
+
return Object.freeze(legacy === null ? [] : [legacy]);
|
|
122
|
+
}
|
|
123
|
+
function normalizePrice(cost) {
|
|
124
|
+
if (!isRecord(cost))
|
|
125
|
+
return null;
|
|
126
|
+
const input = finiteNonNegative(cost.input);
|
|
127
|
+
const output = finiteNonNegative(cost.output);
|
|
128
|
+
if (input === null || output === null)
|
|
129
|
+
return null;
|
|
130
|
+
const tiered = "tiers" in cost || "context_over_200k" in cost;
|
|
131
|
+
return Object.freeze({
|
|
132
|
+
input,
|
|
133
|
+
output,
|
|
134
|
+
cacheRead: finiteNonNegative(cost.cache_read),
|
|
135
|
+
cacheWrite: finiteNonNegative(cost.cache_write),
|
|
136
|
+
tiered,
|
|
137
|
+
contextTiers: contextTiers(cost)
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function parseApiBase(api) {
|
|
141
|
+
if (typeof api !== "string" || api.length === 0 || api.length > 2048)
|
|
142
|
+
return null;
|
|
143
|
+
let parsed;
|
|
144
|
+
try {
|
|
145
|
+
parsed = new URL(api.replace(/\$\{[^}]*\}/g, "wildcard"));
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const host = parsed.hostname.toLowerCase();
|
|
150
|
+
if (!host)
|
|
151
|
+
return null;
|
|
152
|
+
const pathSegments = parsed.pathname.split("/").filter(Boolean).map((segment) => segment.toLowerCase());
|
|
153
|
+
return { host, pathSegments };
|
|
154
|
+
}
|
|
155
|
+
function pathMatches(template, request) {
|
|
156
|
+
if (template.length > request.length)
|
|
157
|
+
return false;
|
|
158
|
+
for (let index = 0;index < template.length; index += 1) {
|
|
159
|
+
if (template[index] !== "*" && template[index] !== request[index])
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
function nameIndexes(models) {
|
|
165
|
+
const lowerModels = new Map;
|
|
166
|
+
const familyModels = new Map;
|
|
167
|
+
for (const model of models.values()) {
|
|
168
|
+
if (model.price === null)
|
|
169
|
+
continue;
|
|
170
|
+
for (const lower of new Set([model.id.toLowerCase(), canonicalModelName(model.id)])) {
|
|
171
|
+
lowerModels.set(lower, lowerModels.has(lower) ? null : model);
|
|
172
|
+
}
|
|
173
|
+
const family = modelFamily(model.id);
|
|
174
|
+
const current = familyModels.get(family);
|
|
175
|
+
const rank = (entry) => `${canonicalModelName(entry.id) === family ? "1" : "0"}:${modelDate(entry.id)}`;
|
|
176
|
+
if (current === undefined || rank(model) > rank(current) || rank(model) === rank(current) && model.id < current.id)
|
|
177
|
+
familyModels.set(family, model);
|
|
178
|
+
}
|
|
179
|
+
return { lowerModels, familyModels };
|
|
180
|
+
}
|
|
181
|
+
function buildCatalogIndex(record) {
|
|
182
|
+
if (!isRecord(record.catalog))
|
|
183
|
+
throw new Error("models-dev catalog record is invalid");
|
|
184
|
+
const providers = [];
|
|
185
|
+
const byProvider = new Map;
|
|
186
|
+
let modelCount = 0;
|
|
187
|
+
for (const [providerKey, providerValue] of Object.entries(record.catalog)) {
|
|
188
|
+
if (!isRecord(providerValue))
|
|
189
|
+
continue;
|
|
190
|
+
if (providers.length >= MAX_PROVIDERS)
|
|
191
|
+
throw new Error("models-dev catalog exceeds the provider budget");
|
|
192
|
+
const id = typeof providerValue.id === "string" && providerValue.id.trim() ? providerValue.id.trim() : providerKey.trim();
|
|
193
|
+
if (!id || id.length > 256)
|
|
194
|
+
continue;
|
|
195
|
+
const name = typeof providerValue.name === "string" && providerValue.name.trim() ? providerValue.name.trim() : id;
|
|
196
|
+
const parsedApi = parseApiBase(providerValue.api);
|
|
197
|
+
const models = new Map;
|
|
198
|
+
if (isRecord(providerValue.models)) {
|
|
199
|
+
for (const [modelKey, modelValue] of Object.entries(providerValue.models)) {
|
|
200
|
+
if (!isRecord(modelValue))
|
|
201
|
+
continue;
|
|
202
|
+
if (models.size >= MAX_MODELS_PER_PROVIDER)
|
|
203
|
+
throw new Error("models-dev catalog exceeds the model budget");
|
|
204
|
+
const modelId = typeof modelValue.id === "string" && modelValue.id.trim() ? modelValue.id.trim() : modelKey.trim();
|
|
205
|
+
if (!modelId || modelId.length > 512)
|
|
206
|
+
continue;
|
|
207
|
+
const modelName = typeof modelValue.name === "string" && modelValue.name.trim() ? modelValue.name.trim() : modelId;
|
|
208
|
+
models.set(modelId, Object.freeze({ id: modelId, name: modelName, price: normalizePrice(modelValue.cost), raw: modelValue }));
|
|
209
|
+
modelCount += 1;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const provider = Object.freeze({
|
|
213
|
+
id,
|
|
214
|
+
name,
|
|
215
|
+
api: typeof providerValue.api === "string" ? providerValue.api : null,
|
|
216
|
+
apiHost: parsedApi?.host ?? null,
|
|
217
|
+
apiPathSegments: parsedApi === null ? null : Object.freeze(parsedApi.pathSegments.map((segment) => segment.includes("wildcard") ? "*" : segment)),
|
|
218
|
+
models,
|
|
219
|
+
...nameIndexes(models),
|
|
220
|
+
raw: providerValue
|
|
221
|
+
});
|
|
222
|
+
providers.push(provider);
|
|
223
|
+
if (!byProvider.has(id))
|
|
224
|
+
byProvider.set(id, provider);
|
|
225
|
+
}
|
|
226
|
+
providers.sort((left, right) => left.id.localeCompare(right.id));
|
|
227
|
+
const exactModels = new Map;
|
|
228
|
+
const lowerModels = new Map;
|
|
229
|
+
const familyModels = new Map;
|
|
230
|
+
const byApiHost = new Map;
|
|
231
|
+
for (const provider of providers) {
|
|
232
|
+
if (provider.apiHost !== null) {
|
|
233
|
+
const sameHost = byApiHost.get(provider.apiHost) ?? [];
|
|
234
|
+
sameHost.push(provider);
|
|
235
|
+
byApiHost.set(provider.apiHost, sameHost);
|
|
236
|
+
}
|
|
237
|
+
for (const model of provider.models.values()) {
|
|
238
|
+
const match = catalogModelIn(provider, model.id);
|
|
239
|
+
if (match === null)
|
|
240
|
+
continue;
|
|
241
|
+
exactModels.set(model.id, exactModels.has(model.id) ? null : match);
|
|
242
|
+
const lower = model.id.toLowerCase();
|
|
243
|
+
lowerModels.set(lower, lowerModels.has(lower) ? null : match);
|
|
244
|
+
}
|
|
245
|
+
for (const [family, model] of provider.familyModels) {
|
|
246
|
+
familyModels.set(family, familyModels.has(family) ? null : catalogModelIn(provider, model.id));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (const providers of byApiHost.values())
|
|
250
|
+
Object.freeze(providers);
|
|
251
|
+
return Object.freeze({ version: record.version, fetchedAt: record.fetchedAt, providers: Object.freeze(providers), byProvider, modelCount, exactModels, lowerModels, familyModels, byApiHost });
|
|
252
|
+
}
|
|
253
|
+
function emptyStatus() {
|
|
254
|
+
return Object.freeze({ state: "empty", version: null, fetchedAt: null, providerCount: 0, modelCount: 0, error: null });
|
|
255
|
+
}
|
|
256
|
+
function statusOf(index, state, error) {
|
|
257
|
+
if (index === null)
|
|
258
|
+
return Object.freeze({ state: error === null ? "empty" : "failed", version: null, fetchedAt: null, providerCount: 0, modelCount: 0, error });
|
|
259
|
+
return Object.freeze({ state, version: index.version, fetchedAt: index.fetchedAt, providerCount: index.providers.length, modelCount: index.modelCount, error });
|
|
260
|
+
}
|
|
261
|
+
function resolveProviderFromUrl(index, rawUrl) {
|
|
262
|
+
if (index === null || typeof rawUrl !== "string" || rawUrl.length === 0 || rawUrl.length > 8192)
|
|
263
|
+
return null;
|
|
264
|
+
let parsed;
|
|
265
|
+
try {
|
|
266
|
+
parsed = new URL(rawUrl);
|
|
267
|
+
} catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
const host = parsed.hostname.toLowerCase();
|
|
271
|
+
const segments = parsed.pathname.split("/").filter(Boolean).map((segment) => segment.toLowerCase());
|
|
272
|
+
const matches = [];
|
|
273
|
+
for (const provider of index.byApiHost.get(host) ?? []) {
|
|
274
|
+
if (provider.apiHost !== host || provider.apiPathSegments === null)
|
|
275
|
+
continue;
|
|
276
|
+
if (!pathMatches(provider.apiPathSegments, segments))
|
|
277
|
+
continue;
|
|
278
|
+
matches.push(provider.id);
|
|
279
|
+
if (matches.length > 1)
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
return matches.length === 1 ? Object.freeze({ provider: matches[0] }) : null;
|
|
283
|
+
}
|
|
284
|
+
function catalogModelIn(provider, modelId) {
|
|
285
|
+
const model = provider.models.get(modelId);
|
|
286
|
+
if (model === undefined || model.price === null)
|
|
287
|
+
return null;
|
|
288
|
+
return Object.freeze({
|
|
289
|
+
provider: provider.id,
|
|
290
|
+
model: model.id,
|
|
291
|
+
input: model.price.input,
|
|
292
|
+
output: model.price.output,
|
|
293
|
+
cacheRead: model.price.cacheRead,
|
|
294
|
+
cacheWrite: model.price.cacheWrite,
|
|
295
|
+
tiered: model.price.tiered,
|
|
296
|
+
contextTiers: model.price.contextTiers
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
function providerPrefix(model) {
|
|
300
|
+
const colon = model.indexOf(":");
|
|
301
|
+
const slash = model.indexOf("/");
|
|
302
|
+
const index = [colon, slash].filter((value) => value > 0).sort((left, right) => left - right)[0];
|
|
303
|
+
if (index === undefined)
|
|
304
|
+
return null;
|
|
305
|
+
const provider = model.slice(0, index);
|
|
306
|
+
const modelId = model.slice(index + 1);
|
|
307
|
+
return provider && modelId ? { provider, modelId } : null;
|
|
308
|
+
}
|
|
309
|
+
function resolveModelInCatalog(index, input) {
|
|
310
|
+
if (index === null || typeof input.model !== "string" || input.model.length === 0 || input.model.length > 512)
|
|
311
|
+
return null;
|
|
312
|
+
if (input.mode === "estimate")
|
|
313
|
+
return resolveEstimate(index, input);
|
|
314
|
+
const model = input.model;
|
|
315
|
+
const hintProvider = typeof input.pricingProvider === "string" && input.pricingProvider.length > 0 && input.pricingProvider.length <= 512 ? input.pricingProvider : undefined;
|
|
316
|
+
if (hintProvider !== undefined) {
|
|
317
|
+
const direct = index.byProvider.get(hintProvider);
|
|
318
|
+
if (direct !== undefined) {
|
|
319
|
+
return catalogModelIn(direct, model);
|
|
320
|
+
}
|
|
321
|
+
if (hintProvider.includes("://") || hintProvider.includes("/")) {
|
|
322
|
+
const resolved = resolveProviderFromUrl(index, hintProvider);
|
|
323
|
+
return resolved === null ? null : catalogModelIn(index.byProvider.get(resolved.provider), model);
|
|
324
|
+
}
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
if (input.url !== undefined && input.url.length > 0) {
|
|
328
|
+
const resolved = resolveProviderFromUrl(index, input.url);
|
|
329
|
+
if (resolved !== null)
|
|
330
|
+
return catalogModelIn(index.byProvider.get(resolved.provider), model);
|
|
331
|
+
}
|
|
332
|
+
const prefixed = providerPrefix(model);
|
|
333
|
+
if (prefixed !== null) {
|
|
334
|
+
const provider = index.byProvider.get(prefixed.provider);
|
|
335
|
+
if (provider !== undefined) {
|
|
336
|
+
const match = catalogModelIn(provider, prefixed.modelId);
|
|
337
|
+
if (match !== null)
|
|
338
|
+
return match;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return index.exactModels.get(model) ?? null;
|
|
342
|
+
}
|
|
343
|
+
function estimateIn(provider, model) {
|
|
344
|
+
const exact = catalogModelIn(provider, model);
|
|
345
|
+
if (exact !== null)
|
|
346
|
+
return exact;
|
|
347
|
+
for (const name of estimateNames(model)) {
|
|
348
|
+
const candidate = provider.lowerModels.get(name);
|
|
349
|
+
if (candidate)
|
|
350
|
+
return catalogModelIn(provider, candidate.id);
|
|
351
|
+
}
|
|
352
|
+
const family = provider.familyModels.get(modelFamily(model));
|
|
353
|
+
return family === undefined ? null : catalogModelIn(provider, family.id);
|
|
354
|
+
}
|
|
355
|
+
function resolveEstimate(index, input) {
|
|
356
|
+
const model = input.model.trim();
|
|
357
|
+
if (!model)
|
|
358
|
+
return null;
|
|
359
|
+
const hint = input.pricingProvider;
|
|
360
|
+
const direct = hint ? index.byProvider.get(hint) : undefined;
|
|
361
|
+
if (direct !== undefined)
|
|
362
|
+
return estimateIn(direct, model);
|
|
363
|
+
const hintIsUrl = typeof hint === "string" && (hint.includes("://") || hint.includes("/"));
|
|
364
|
+
if (hint && !hintIsUrl) {
|
|
365
|
+
const provider = index.byProvider.get(hint);
|
|
366
|
+
return provider === undefined ? null : estimateIn(provider, model);
|
|
367
|
+
}
|
|
368
|
+
const prefixed = providerPrefix(model);
|
|
369
|
+
if (prefixed !== null) {
|
|
370
|
+
const provider = index.byProvider.get(prefixed.provider.toLowerCase());
|
|
371
|
+
if (provider !== undefined)
|
|
372
|
+
return estimateIn(provider, prefixed.modelId);
|
|
373
|
+
}
|
|
374
|
+
const url = hintIsUrl ? hint : input.url;
|
|
375
|
+
if (url) {
|
|
376
|
+
const resolved = resolveProviderFromUrl(index, url);
|
|
377
|
+
if (resolved !== null) {
|
|
378
|
+
const match = estimateIn(index.byProvider.get(resolved.provider), model);
|
|
379
|
+
if (match !== null)
|
|
380
|
+
return match;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const exact = index.exactModels.get(model);
|
|
384
|
+
if (exact)
|
|
385
|
+
return exact;
|
|
386
|
+
const lower = index.lowerModels.get(model.toLowerCase());
|
|
387
|
+
if (lower)
|
|
388
|
+
return lower;
|
|
389
|
+
const lab = originalLab(model);
|
|
390
|
+
if (lab !== null) {
|
|
391
|
+
const provider = index.byProvider.get(lab);
|
|
392
|
+
return provider === undefined ? null : estimateIn(provider, model);
|
|
393
|
+
}
|
|
394
|
+
for (const name of estimateNames(model)) {
|
|
395
|
+
const match = index.lowerModels.get(name);
|
|
396
|
+
if (match)
|
|
397
|
+
return match;
|
|
398
|
+
}
|
|
399
|
+
return index.familyModels.get(modelFamily(model)) ?? null;
|
|
400
|
+
}
|
|
401
|
+
function providerSummaries(index) {
|
|
402
|
+
if (index === null)
|
|
403
|
+
return Object.freeze([]);
|
|
404
|
+
return Object.freeze(index.providers.map((provider) => Object.freeze({
|
|
405
|
+
provider: provider.id,
|
|
406
|
+
name: provider.name,
|
|
407
|
+
api: provider.api,
|
|
408
|
+
modelCount: provider.models.size
|
|
409
|
+
})));
|
|
410
|
+
}
|
|
411
|
+
function modelOptions(index, input = {}) {
|
|
412
|
+
const pageSize = Math.min(Math.max(1, Math.trunc(input.pageSize ?? 50)), MAX_MODEL_OPTION_PAGE_SIZE);
|
|
413
|
+
const requestedPage = Math.max(1, Math.trunc(input.page ?? 1));
|
|
414
|
+
if (index === null)
|
|
415
|
+
return Object.freeze({ models: Object.freeze([]), total: 0, page: 1, pageSize });
|
|
416
|
+
const providerFilter = input.provider;
|
|
417
|
+
const search = input.search;
|
|
418
|
+
const needle = typeof search === "string" && search.length > 0 ? search.toLocaleLowerCase() : null;
|
|
419
|
+
const providers = providerFilter === undefined ? index.providers : index.providers.filter((provider) => provider.id === providerFilter);
|
|
420
|
+
const matchesSearch = (provider, model) => needle === null || `${model.id}
|
|
421
|
+
${model.name}
|
|
422
|
+
${provider.id}
|
|
423
|
+
${provider.name}`.toLocaleLowerCase().includes(needle);
|
|
424
|
+
let total = 0;
|
|
425
|
+
for (const provider of providers) {
|
|
426
|
+
if (needle === null)
|
|
427
|
+
total += provider.models.size;
|
|
428
|
+
else
|
|
429
|
+
for (const model of provider.models.values())
|
|
430
|
+
if (matchesSearch(provider, model))
|
|
431
|
+
total++;
|
|
432
|
+
if (total > MAX_RESOLVE_CANDIDATES)
|
|
433
|
+
throw new Error("models-dev model option scan exceeded the budget");
|
|
434
|
+
}
|
|
435
|
+
const lastPage = Math.max(1, Math.ceil(total / pageSize));
|
|
436
|
+
const page = Math.min(requestedPage, lastPage);
|
|
437
|
+
let skip = (page - 1) * pageSize;
|
|
438
|
+
const models = [];
|
|
439
|
+
for (const provider of providers) {
|
|
440
|
+
if (needle === null && skip >= provider.models.size) {
|
|
441
|
+
skip -= provider.models.size;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
for (const model of provider.models.values()) {
|
|
445
|
+
if (!matchesSearch(provider, model))
|
|
446
|
+
continue;
|
|
447
|
+
if (skip > 0) {
|
|
448
|
+
skip--;
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
models.push({ provider: provider.id, providerName: provider.name, model: model.id, name: model.name });
|
|
452
|
+
if (models.length === pageSize)
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
if (models.length === pageSize)
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
return Object.freeze({ models: Object.freeze(models), total, page, pageSize });
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// plugins/models-dev/server/download.ts
|
|
462
|
+
var MAX_CATALOG_BODY_BYTES = 16 * 1024 * 1024;
|
|
463
|
+
async function readBoundedBody(response, maxBytes, signal) {
|
|
464
|
+
if (signal?.aborted)
|
|
465
|
+
throw new Error("catalog download cancelled");
|
|
466
|
+
const contentLength = Number(response.headers.get("content-length"));
|
|
467
|
+
if (Number.isFinite(contentLength) && contentLength > maxBytes) {
|
|
468
|
+
await response.body?.cancel();
|
|
469
|
+
throw new Error("catalog body limit exceeded");
|
|
470
|
+
}
|
|
471
|
+
if (response.body === null)
|
|
472
|
+
return "";
|
|
473
|
+
const reader = response.body.getReader();
|
|
474
|
+
const chunks = [];
|
|
475
|
+
let total = 0;
|
|
476
|
+
try {
|
|
477
|
+
for (;; ) {
|
|
478
|
+
if (signal?.aborted)
|
|
479
|
+
throw new Error("catalog download cancelled");
|
|
480
|
+
const part = await reader.read();
|
|
481
|
+
if (part.done)
|
|
482
|
+
break;
|
|
483
|
+
total += part.value.byteLength;
|
|
484
|
+
if (total > maxBytes) {
|
|
485
|
+
await reader.cancel();
|
|
486
|
+
throw new Error("catalog body limit exceeded");
|
|
487
|
+
}
|
|
488
|
+
chunks.push(part.value);
|
|
489
|
+
}
|
|
490
|
+
} finally {
|
|
491
|
+
reader.releaseLock();
|
|
492
|
+
}
|
|
493
|
+
const body = new Uint8Array(total);
|
|
494
|
+
let offset = 0;
|
|
495
|
+
for (const chunk of chunks) {
|
|
496
|
+
body.set(chunk, offset);
|
|
497
|
+
offset += chunk.byteLength;
|
|
498
|
+
}
|
|
499
|
+
return new TextDecoder().decode(body);
|
|
500
|
+
}
|
|
501
|
+
async function downloadModelsDevCatalog(input = {}) {
|
|
502
|
+
const url = input.url ?? MODELS_DEV_SOURCE_URL;
|
|
503
|
+
const maxBytes = input.maxBytes ?? MAX_CATALOG_BODY_BYTES;
|
|
504
|
+
const response = await (input.fetch ?? globalThis.fetch)(url, {
|
|
505
|
+
...input.signal === undefined ? {} : { signal: input.signal },
|
|
506
|
+
headers: { accept: "application/json" }
|
|
507
|
+
});
|
|
508
|
+
if (!response.ok)
|
|
509
|
+
throw new Error(`catalog download failed with status ${response.status}`);
|
|
510
|
+
const body = await readBoundedBody(response, maxBytes, input.signal);
|
|
511
|
+
if (input.signal?.aborted)
|
|
512
|
+
throw new Error("catalog download cancelled");
|
|
513
|
+
const parsed = JSON.parse(body);
|
|
514
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
515
|
+
throw new Error("catalog payload is invalid");
|
|
516
|
+
return parsed;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// plugins/models-dev/server/local.ts
|
|
520
|
+
import { createHash } from "crypto";
|
|
521
|
+
|
|
522
|
+
// plugins/models-dev/server/store.ts
|
|
523
|
+
var MODELS_DEV_SETTINGS_KEY = "catalog:settings:v2";
|
|
524
|
+
var MODELS_DEV_CATALOG_KEY = "catalog:v1";
|
|
525
|
+
var MODELS_DEV_STATUS_KEY = "catalog:status:v1";
|
|
526
|
+
var DEFAULT_MODELS_DEV_SETTINGS = { autoRefresh: true, intervalHours: 24, timeoutSeconds: 15 };
|
|
527
|
+
function parseModelsDevSettings(value) {
|
|
528
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
529
|
+
throw new Error("invalid_input");
|
|
530
|
+
const record = value;
|
|
531
|
+
if (Object.keys(record).some((key) => !["autoRefresh", "intervalHours", "timeoutSeconds"].includes(key)) || typeof record.autoRefresh !== "boolean" || !Number.isInteger(record.intervalHours) || record.intervalHours < 1 || record.intervalHours > 24 || !Number.isInteger(record.timeoutSeconds) || record.timeoutSeconds < 5 || record.timeoutSeconds > 120) {
|
|
532
|
+
throw new Error("invalid_input");
|
|
533
|
+
}
|
|
534
|
+
return { autoRefresh: record.autoRefresh, intervalHours: record.intervalHours, timeoutSeconds: record.timeoutSeconds };
|
|
535
|
+
}
|
|
536
|
+
function parsePersistedStatus(value) {
|
|
537
|
+
const empty = { lastAttemptAt: null, lastSuccessAt: null, lastError: null, consecutiveFailures: 0 };
|
|
538
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
539
|
+
return empty;
|
|
540
|
+
const record = value;
|
|
541
|
+
const numberOrNull = (candidate) => typeof candidate === "number" && Number.isFinite(candidate) && candidate >= 0 ? candidate : null;
|
|
542
|
+
return {
|
|
543
|
+
lastAttemptAt: numberOrNull(record.lastAttemptAt),
|
|
544
|
+
lastSuccessAt: numberOrNull(record.lastSuccessAt),
|
|
545
|
+
lastError: typeof record.lastError === "string" && record.lastError.length > 0 && record.lastError.length <= 64 ? record.lastError : null,
|
|
546
|
+
consecutiveFailures: Number.isSafeInteger(record.consecutiveFailures) && record.consecutiveFailures >= 0 ? record.consecutiveFailures : 0
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
async function strictRead(storage, key) {
|
|
550
|
+
if (typeof storage.readStrict === "function") {
|
|
551
|
+
const result = await storage.readStrict(key);
|
|
552
|
+
return result.found ? result.value : undefined;
|
|
553
|
+
}
|
|
554
|
+
const value = await storage.get(key);
|
|
555
|
+
return value === null ? undefined : value;
|
|
556
|
+
}
|
|
557
|
+
async function readModelsDevSettings(storage) {
|
|
558
|
+
const stored = await strictRead(storage, MODELS_DEV_SETTINGS_KEY);
|
|
559
|
+
if (stored === undefined)
|
|
560
|
+
return { ...DEFAULT_MODELS_DEV_SETTINGS };
|
|
561
|
+
return parseModelsDevSettings(stored);
|
|
562
|
+
}
|
|
563
|
+
async function writeModelsDevSettings(storage, settings) {
|
|
564
|
+
await storage.set(MODELS_DEV_SETTINGS_KEY, settings);
|
|
565
|
+
}
|
|
566
|
+
async function readCatalogRecord(storage) {
|
|
567
|
+
if (typeof storage.readStrict !== "function")
|
|
568
|
+
throw new Error("models-dev requires strict plugin storage reads");
|
|
569
|
+
const result = await storage.readStrict(MODELS_DEV_CATALOG_KEY);
|
|
570
|
+
if (!result.found)
|
|
571
|
+
return null;
|
|
572
|
+
return parseCatalogRecord(result.value);
|
|
573
|
+
}
|
|
574
|
+
function parseCatalogRecord(value) {
|
|
575
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
576
|
+
throw new Error("models-dev catalog record is invalid");
|
|
577
|
+
const record = value;
|
|
578
|
+
if (!Number.isSafeInteger(record.version) || record.version < 1 || typeof record.fetchedAt !== "number" || !Number.isFinite(record.fetchedAt) || record.fetchedAt < 0 || record.catalog === null || typeof record.catalog !== "object" || Array.isArray(record.catalog)) {
|
|
579
|
+
throw new Error("models-dev catalog record is invalid");
|
|
580
|
+
}
|
|
581
|
+
return { version: record.version, fetchedAt: record.fetchedAt, catalog: record.catalog };
|
|
582
|
+
}
|
|
583
|
+
async function readPersistedStatus(storage) {
|
|
584
|
+
const stored = await strictRead(storage, MODELS_DEV_STATUS_KEY);
|
|
585
|
+
return stored === undefined ? parsePersistedStatus(undefined) : parsePersistedStatus(stored);
|
|
586
|
+
}
|
|
587
|
+
async function writePersistedStatus(storage, status) {
|
|
588
|
+
await (storage.uncached?.() ?? storage).set(MODELS_DEV_STATUS_KEY, status);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// plugins/models-dev/server/local.ts
|
|
592
|
+
class CatalogView {
|
|
593
|
+
#index = null;
|
|
594
|
+
#state = "empty";
|
|
595
|
+
#error = null;
|
|
596
|
+
apply(index, state = "ready", error = null) {
|
|
597
|
+
this.#index = index;
|
|
598
|
+
this.#state = state;
|
|
599
|
+
this.#error = error;
|
|
600
|
+
}
|
|
601
|
+
fail(error) {
|
|
602
|
+
this.#error = error;
|
|
603
|
+
this.#state = this.#index === null ? "failed" : "stale";
|
|
604
|
+
}
|
|
605
|
+
loaded() {
|
|
606
|
+
return this.#index !== null;
|
|
607
|
+
}
|
|
608
|
+
status() {
|
|
609
|
+
return this.#index === null && this.#state === "empty" ? emptyStatus() : statusOf(this.#index, this.#state === "empty" ? "ready" : this.#state, this.#error);
|
|
610
|
+
}
|
|
611
|
+
providers() {
|
|
612
|
+
return providerSummaries(this.#index);
|
|
613
|
+
}
|
|
614
|
+
modelOptions(input) {
|
|
615
|
+
return modelOptions(this.#index, input);
|
|
616
|
+
}
|
|
617
|
+
resolveModel(input) {
|
|
618
|
+
return resolveModelInCatalog(this.#index, input);
|
|
619
|
+
}
|
|
620
|
+
resolveProvider(input) {
|
|
621
|
+
return resolveProviderFromUrl(this.#index, input.url);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function catalogServiceOf(view) {
|
|
625
|
+
return {
|
|
626
|
+
status: () => view.status(),
|
|
627
|
+
providers: () => view.providers(),
|
|
628
|
+
modelOptions: (input) => view.modelOptions(input),
|
|
629
|
+
resolveModel: (input) => view.resolveModel(input),
|
|
630
|
+
resolveProvider: (input) => view.resolveProvider(input)
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function decodeCatalogSnapshot(bytes, version) {
|
|
634
|
+
const record = parseCatalogRecord(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)));
|
|
635
|
+
if (record.version !== version)
|
|
636
|
+
throw new Error("catalog_version_mismatch");
|
|
637
|
+
return record;
|
|
638
|
+
}
|
|
639
|
+
async function readCatalogSnapshot(source) {
|
|
640
|
+
const { size, chunkBytes, digest, version } = source.descriptor;
|
|
641
|
+
if (!Number.isSafeInteger(size) || size < 1 || size > 32 * 1024 * 1024 || !Number.isSafeInteger(chunkBytes) || chunkBytes < 1)
|
|
642
|
+
throw new Error("catalog_snapshot_size");
|
|
643
|
+
source.retain?.();
|
|
644
|
+
try {
|
|
645
|
+
const bytes = new Uint8Array(size);
|
|
646
|
+
for (let offset = 0;offset < size; offset += chunkBytes) {
|
|
647
|
+
const length = Math.min(chunkBytes, size - offset);
|
|
648
|
+
const chunk = await source.read(offset, length);
|
|
649
|
+
if (chunk.byteLength !== length)
|
|
650
|
+
throw new Error("catalog_snapshot_truncated");
|
|
651
|
+
bytes.set(chunk, offset);
|
|
652
|
+
}
|
|
653
|
+
if (`sha256:${createHash("sha256").update(bytes).digest("hex")}` !== digest)
|
|
654
|
+
throw new Error("catalog_snapshot_digest");
|
|
655
|
+
return decodeCatalogSnapshot(bytes, version);
|
|
656
|
+
} finally {
|
|
657
|
+
source.release?.();
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// plugins/models-dev/server/control.ts
|
|
662
|
+
var MAX_RESPONSE_BYTES = 256 * 1024;
|
|
663
|
+
var MAX_PAGE_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
664
|
+
var MAX_QUERY_BYTES = 512;
|
|
665
|
+
var MAX_PAGE = 400;
|
|
666
|
+
|
|
667
|
+
class ModelsDevCatalogManager {
|
|
668
|
+
storage;
|
|
669
|
+
publisher;
|
|
670
|
+
options;
|
|
671
|
+
view = new CatalogView;
|
|
672
|
+
settings = { ...DEFAULT_MODELS_DEV_SETTINGS };
|
|
673
|
+
status = { lastAttemptAt: null, lastSuccessAt: null, lastError: null, consecutiveFailures: 0 };
|
|
674
|
+
refreshing = false;
|
|
675
|
+
nextRefreshAt = null;
|
|
676
|
+
timer;
|
|
677
|
+
controller;
|
|
678
|
+
inFlight;
|
|
679
|
+
initialization;
|
|
680
|
+
stopped = false;
|
|
681
|
+
now;
|
|
682
|
+
constructor(storage, publisher, options = {}) {
|
|
683
|
+
this.storage = storage;
|
|
684
|
+
this.publisher = publisher;
|
|
685
|
+
this.options = options;
|
|
686
|
+
this.now = options.now ?? Date.now;
|
|
687
|
+
}
|
|
688
|
+
async start() {
|
|
689
|
+
await (this.initialization ??= this.restore());
|
|
690
|
+
}
|
|
691
|
+
statusSnapshot() {
|
|
692
|
+
const catalog = this.view.status();
|
|
693
|
+
return {
|
|
694
|
+
source: MODELS_DEV_SOURCE_URL,
|
|
695
|
+
state: catalog.state,
|
|
696
|
+
settings: { ...this.settings },
|
|
697
|
+
refreshing: this.refreshing,
|
|
698
|
+
lastAttemptAt: this.status.lastAttemptAt,
|
|
699
|
+
lastSuccessAt: this.status.lastSuccessAt,
|
|
700
|
+
nextRefreshAt: this.nextRefreshAt,
|
|
701
|
+
lastError: this.status.lastError,
|
|
702
|
+
consecutiveFailures: this.status.consecutiveFailures,
|
|
703
|
+
modelCount: catalog.modelCount,
|
|
704
|
+
providerCount: catalog.providerCount,
|
|
705
|
+
version: catalog.version
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
async configure(settings) {
|
|
709
|
+
await this.start();
|
|
710
|
+
if (this.stopped)
|
|
711
|
+
throw new Error("disposed");
|
|
712
|
+
const parsed = parseModelsDevSettings(settings);
|
|
713
|
+
await writeModelsDevSettings(this.storage, parsed);
|
|
714
|
+
this.settings = parsed;
|
|
715
|
+
this.schedule();
|
|
716
|
+
return this.statusSnapshot();
|
|
717
|
+
}
|
|
718
|
+
refresh() {
|
|
719
|
+
if (this.stopped)
|
|
720
|
+
return Promise.resolve();
|
|
721
|
+
if (this.inFlight)
|
|
722
|
+
return this.inFlight;
|
|
723
|
+
this.clearTimer();
|
|
724
|
+
this.nextRefreshAt = null;
|
|
725
|
+
this.refreshing = true;
|
|
726
|
+
this.status.lastAttemptAt = this.now();
|
|
727
|
+
const task = this.load().finally(() => {
|
|
728
|
+
if (this.inFlight === task)
|
|
729
|
+
this.inFlight = undefined;
|
|
730
|
+
});
|
|
731
|
+
this.inFlight = task;
|
|
732
|
+
return task;
|
|
733
|
+
}
|
|
734
|
+
async stop() {
|
|
735
|
+
this.stopped = true;
|
|
736
|
+
this.clearTimer();
|
|
737
|
+
this.controller?.abort();
|
|
738
|
+
await Promise.allSettled([this.initialization, this.inFlight].filter((task) => task !== undefined));
|
|
739
|
+
}
|
|
740
|
+
async restore() {
|
|
741
|
+
try {
|
|
742
|
+
this.settings = await readModelsDevSettings(this.storage);
|
|
743
|
+
} catch {
|
|
744
|
+
this.status.lastError = "storage";
|
|
745
|
+
}
|
|
746
|
+
try {
|
|
747
|
+
this.status = { ...this.status, ...await readPersistedStatus(this.storage) };
|
|
748
|
+
} catch {
|
|
749
|
+
this.status.lastError = "storage";
|
|
750
|
+
}
|
|
751
|
+
try {
|
|
752
|
+
let source = this.publisher.current();
|
|
753
|
+
if (source === null) {
|
|
754
|
+
const legacy = await readCatalogRecord(this.storage);
|
|
755
|
+
if (legacy !== null) {
|
|
756
|
+
buildCatalogIndex(legacy);
|
|
757
|
+
this.publisher.publish(legacy.version, new TextEncoder().encode(JSON.stringify(legacy)));
|
|
758
|
+
source = this.publisher.current();
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
const record = source === null ? null : await readCatalogSnapshot(source);
|
|
762
|
+
if (record !== null) {
|
|
763
|
+
const index = buildCatalogIndex(record);
|
|
764
|
+
this.view.apply(index, this.status.lastError === null ? "ready" : "stale", this.status.lastError);
|
|
765
|
+
this.status.lastSuccessAt = record.fetchedAt;
|
|
766
|
+
} else {
|
|
767
|
+
this.status.lastSuccessAt = null;
|
|
768
|
+
}
|
|
769
|
+
} catch {
|
|
770
|
+
this.view.fail("snapshot");
|
|
771
|
+
this.status.lastError = "snapshot";
|
|
772
|
+
}
|
|
773
|
+
if (this.stopped)
|
|
774
|
+
return;
|
|
775
|
+
this.schedule();
|
|
776
|
+
}
|
|
777
|
+
async load() {
|
|
778
|
+
const controller = new AbortController;
|
|
779
|
+
this.controller = controller;
|
|
780
|
+
let timeout;
|
|
781
|
+
let onAbort;
|
|
782
|
+
let timedOut = false;
|
|
783
|
+
let phase = "storage";
|
|
784
|
+
try {
|
|
785
|
+
try {
|
|
786
|
+
await writePersistedStatus(this.storage, this.status);
|
|
787
|
+
} catch {
|
|
788
|
+
this.status.lastError = "storage";
|
|
789
|
+
}
|
|
790
|
+
if (this.stopped)
|
|
791
|
+
return;
|
|
792
|
+
phase = "network";
|
|
793
|
+
const failure = new Promise((_resolve, reject) => {
|
|
794
|
+
onAbort = () => reject(new Error("aborted"));
|
|
795
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
796
|
+
timeout = (this.options.schedule ?? setTimeout)(() => {
|
|
797
|
+
timedOut = true;
|
|
798
|
+
controller.abort();
|
|
799
|
+
}, this.settings.timeoutSeconds * 1000);
|
|
800
|
+
});
|
|
801
|
+
const catalog = await Promise.race([
|
|
802
|
+
downloadModelsDevCatalog({ ...this.options.fetch ? { fetch: this.options.fetch } : {}, signal: controller.signal }),
|
|
803
|
+
failure
|
|
804
|
+
]);
|
|
805
|
+
if (this.stopped)
|
|
806
|
+
return;
|
|
807
|
+
phase = "invalid_catalog";
|
|
808
|
+
const version = (this.publisher.current()?.descriptor.version ?? 0) + 1;
|
|
809
|
+
if (!Number.isSafeInteger(version))
|
|
810
|
+
throw new Error("catalog_version_overflow");
|
|
811
|
+
const fetchedAt = Math.max(this.now(), (this.status.lastSuccessAt ?? -1) + 1);
|
|
812
|
+
const record = { version, fetchedAt, catalog };
|
|
813
|
+
const index = buildCatalogIndex(record);
|
|
814
|
+
phase = "snapshot";
|
|
815
|
+
this.publisher.publish(version, new TextEncoder().encode(JSON.stringify(record)));
|
|
816
|
+
if (this.stopped)
|
|
817
|
+
return;
|
|
818
|
+
this.view.apply(index, "ready", null);
|
|
819
|
+
this.status.lastSuccessAt = fetchedAt;
|
|
820
|
+
this.status.lastError = null;
|
|
821
|
+
this.status.consecutiveFailures = 0;
|
|
822
|
+
} catch (error) {
|
|
823
|
+
if (this.stopped)
|
|
824
|
+
return;
|
|
825
|
+
this.status.lastError = timedOut ? "timeout" : phase === "invalid_catalog" ? "invalid_catalog" : phase;
|
|
826
|
+
this.status.consecutiveFailures += 1;
|
|
827
|
+
this.view.fail(this.status.lastError);
|
|
828
|
+
} finally {
|
|
829
|
+
if (timeout !== undefined)
|
|
830
|
+
(this.options.cancel ?? clearTimeout)(timeout);
|
|
831
|
+
if (onAbort)
|
|
832
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
833
|
+
if (this.controller === controller)
|
|
834
|
+
this.controller = undefined;
|
|
835
|
+
this.refreshing = false;
|
|
836
|
+
if (!this.stopped) {
|
|
837
|
+
this.schedule();
|
|
838
|
+
try {
|
|
839
|
+
await writePersistedStatus(this.storage, this.status);
|
|
840
|
+
} catch {
|
|
841
|
+
this.status.lastError = "storage";
|
|
842
|
+
this.view.fail("storage");
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
clearTimer() {
|
|
848
|
+
if (this.timer !== undefined)
|
|
849
|
+
(this.options.cancel ?? clearTimeout)(this.timer);
|
|
850
|
+
this.timer = undefined;
|
|
851
|
+
}
|
|
852
|
+
schedule() {
|
|
853
|
+
this.clearTimer();
|
|
854
|
+
this.nextRefreshAt = null;
|
|
855
|
+
if (this.stopped || this.refreshing || !this.settings.autoRefresh)
|
|
856
|
+
return;
|
|
857
|
+
const interval = this.settings.intervalHours * 60 * 60 * 1000;
|
|
858
|
+
const due = this.status.consecutiveFailures ? this.now() + Math.min(interval, 60000 * Math.min(15, 2 ** Math.min(this.status.consecutiveFailures - 1, 4))) : this.status.lastSuccessAt === null ? this.now() : Math.max(this.now(), this.status.lastSuccessAt + interval);
|
|
859
|
+
this.nextRefreshAt = due;
|
|
860
|
+
this.timer = (this.options.schedule ?? setTimeout)(() => {
|
|
861
|
+
this.refresh();
|
|
862
|
+
}, Math.max(0, due - this.now()));
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
function jsonResponse(value, status = 200, maxBytes = MAX_RESPONSE_BYTES) {
|
|
866
|
+
const body = JSON.stringify(value);
|
|
867
|
+
if (new TextEncoder().encode(body).byteLength > maxBytes) {
|
|
868
|
+
return new Response(JSON.stringify({ error: "response_limit" }), { status: 500, headers: { "content-type": "application/json; charset=utf-8" } });
|
|
869
|
+
}
|
|
870
|
+
return new Response(body, { status, headers: { "content-type": "application/json; charset=utf-8" } });
|
|
871
|
+
}
|
|
872
|
+
async function readBody(request, maxBytes) {
|
|
873
|
+
const reader = request.body?.getReader();
|
|
874
|
+
if (!reader)
|
|
875
|
+
throw new Error("invalid_input");
|
|
876
|
+
const chunks = [];
|
|
877
|
+
let size = 0;
|
|
878
|
+
try {
|
|
879
|
+
for (;; ) {
|
|
880
|
+
const { done, value } = await reader.read();
|
|
881
|
+
if (done)
|
|
882
|
+
break;
|
|
883
|
+
size += value.byteLength;
|
|
884
|
+
if (size > maxBytes) {
|
|
885
|
+
await reader.cancel();
|
|
886
|
+
throw new Error("invalid_input");
|
|
887
|
+
}
|
|
888
|
+
chunks.push(value);
|
|
889
|
+
}
|
|
890
|
+
const bytes = new Uint8Array(size);
|
|
891
|
+
let offset = 0;
|
|
892
|
+
for (const chunk of chunks) {
|
|
893
|
+
bytes.set(chunk, offset);
|
|
894
|
+
offset += chunk.byteLength;
|
|
895
|
+
}
|
|
896
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
897
|
+
} finally {
|
|
898
|
+
reader.releaseLock();
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
class ModelsDevControl {
|
|
903
|
+
host;
|
|
904
|
+
api;
|
|
905
|
+
rpc = [];
|
|
906
|
+
disposed = false;
|
|
907
|
+
manager;
|
|
908
|
+
store;
|
|
909
|
+
abortListener;
|
|
910
|
+
constructor(host, options) {
|
|
911
|
+
this.host = host;
|
|
912
|
+
const snapshot = host.services?.snapshot;
|
|
913
|
+
if (snapshot === undefined || typeof snapshot.store !== "function") {
|
|
914
|
+
throw new Error("models-dev requires a durable host snapshot store capability");
|
|
915
|
+
}
|
|
916
|
+
const store = snapshot.store({ id: "models-dev.catalog.v1", schemaVersion: 1, maxVersions: 3, maxBytes: 32 * 1024 * 1024 });
|
|
917
|
+
if (store === null)
|
|
918
|
+
throw new Error("models-dev requires a durable host snapshot store capability");
|
|
919
|
+
this.store = store;
|
|
920
|
+
this.manager = new ModelsDevCatalogManager(host.storage.uncached?.() ?? host.storage, {
|
|
921
|
+
publish: (version, bytes) => {
|
|
922
|
+
store.publish(version, bytes);
|
|
923
|
+
},
|
|
924
|
+
current: () => store.current()
|
|
925
|
+
}, options);
|
|
926
|
+
this.abortListener = () => {
|
|
927
|
+
this.dispose();
|
|
928
|
+
};
|
|
929
|
+
if (host.signal.aborted)
|
|
930
|
+
this.disposed = true;
|
|
931
|
+
else
|
|
932
|
+
host.signal.addEventListener("abort", this.abortListener, { once: true });
|
|
933
|
+
snapshot.provide(MODELS_DEV_CATALOG_SNAPSHOT_CONTRACT, {
|
|
934
|
+
current: () => this.store.current(),
|
|
935
|
+
version: (version) => this.store.version(version)
|
|
936
|
+
});
|
|
937
|
+
if (host.services !== undefined) {
|
|
938
|
+
host.services.publish(MODELS_DEV_CATALOG_SERVICE_ID, MODELS_DEV_CATALOG_CONTRACT_VERSION, catalogServiceOf(this.manager.view));
|
|
939
|
+
}
|
|
940
|
+
this.api = this.buildApi();
|
|
941
|
+
}
|
|
942
|
+
assertAlive(signal) {
|
|
943
|
+
if (this.disposed || this.host.signal.aborted)
|
|
944
|
+
throw new Error("disposed");
|
|
945
|
+
if (signal?.aborted)
|
|
946
|
+
throw new Error("request_cancelled");
|
|
947
|
+
}
|
|
948
|
+
buildApi() {
|
|
949
|
+
const invoke = (handler) => async (context) => {
|
|
950
|
+
try {
|
|
951
|
+
this.assertAlive(context.requestSignal);
|
|
952
|
+
return await handler(context);
|
|
953
|
+
} catch (error) {
|
|
954
|
+
return jsonResponse({ error: error instanceof Error && error.message === "invalid_input" ? "invalid_input" : "internal_error" }, 500);
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
return [{
|
|
958
|
+
path: "/catalog/status",
|
|
959
|
+
methods: ["GET"],
|
|
960
|
+
handler: "getCatalogStatus",
|
|
961
|
+
invoke: invoke(async () => {
|
|
962
|
+
await this.manager.start();
|
|
963
|
+
return jsonResponse(this.manager.statusSnapshot());
|
|
964
|
+
})
|
|
965
|
+
}, {
|
|
966
|
+
path: "/catalog/providers",
|
|
967
|
+
methods: ["GET"],
|
|
968
|
+
handler: "getCatalogProviders",
|
|
969
|
+
invoke: invoke(async () => jsonResponse({ providers: this.manager.view.providers() }, 200, MAX_PAGE_RESPONSE_BYTES))
|
|
970
|
+
}, {
|
|
971
|
+
path: "/catalog/models",
|
|
972
|
+
methods: ["GET"],
|
|
973
|
+
handler: "getCatalogModels",
|
|
974
|
+
invoke: invoke(async (context) => {
|
|
975
|
+
const query = parseModelQuery(context.request);
|
|
976
|
+
if (query === null)
|
|
977
|
+
return jsonResponse({ error: "invalid_query" }, 400);
|
|
978
|
+
await this.manager.start();
|
|
979
|
+
return jsonResponse(this.manager.view.modelOptions(query), 200, MAX_PAGE_RESPONSE_BYTES);
|
|
980
|
+
})
|
|
981
|
+
}, {
|
|
982
|
+
path: "/catalog/settings",
|
|
983
|
+
methods: ["PUT"],
|
|
984
|
+
handler: "configureCatalog",
|
|
985
|
+
invoke: invoke(async (context) => {
|
|
986
|
+
let settings;
|
|
987
|
+
try {
|
|
988
|
+
settings = parseModelsDevSettings(await readBody(context.request, 4096));
|
|
989
|
+
} catch {
|
|
990
|
+
return jsonResponse({ error: "invalid_input" }, 400);
|
|
991
|
+
}
|
|
992
|
+
return jsonResponse(await this.manager.configure(settings));
|
|
993
|
+
})
|
|
994
|
+
}, {
|
|
995
|
+
path: "/catalog/refresh",
|
|
996
|
+
methods: ["POST"],
|
|
997
|
+
handler: "refreshCatalog",
|
|
998
|
+
invoke: invoke(async () => {
|
|
999
|
+
await this.manager.start();
|
|
1000
|
+
this.manager.refresh();
|
|
1001
|
+
return jsonResponse(this.manager.statusSnapshot(), 202);
|
|
1002
|
+
})
|
|
1003
|
+
}];
|
|
1004
|
+
}
|
|
1005
|
+
async start() {
|
|
1006
|
+
this.assertAlive();
|
|
1007
|
+
await this.manager.start();
|
|
1008
|
+
}
|
|
1009
|
+
async dispose() {
|
|
1010
|
+
if (this.disposed)
|
|
1011
|
+
return;
|
|
1012
|
+
this.disposed = true;
|
|
1013
|
+
const stopped = this.manager.stop();
|
|
1014
|
+
this.host.signal.removeEventListener("abort", this.abortListener);
|
|
1015
|
+
await stopped;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
function parseModelQuery(request) {
|
|
1019
|
+
const params = new URL(request.url).searchParams;
|
|
1020
|
+
const encoder = new TextEncoder;
|
|
1021
|
+
for (const key of ["provider", "search", "page"])
|
|
1022
|
+
if (params.getAll(key).length > 1)
|
|
1023
|
+
return null;
|
|
1024
|
+
const provider = params.get("provider") ?? undefined;
|
|
1025
|
+
const search = params.get("search") ?? undefined;
|
|
1026
|
+
const rawPage = params.get("page");
|
|
1027
|
+
const page = rawPage === null ? 1 : Number(rawPage);
|
|
1028
|
+
if (provider !== undefined && encoder.encode(provider).byteLength > MAX_QUERY_BYTES || search !== undefined && encoder.encode(search).byteLength > MAX_QUERY_BYTES || !Number.isSafeInteger(page) || page < 1 || page > MAX_PAGE)
|
|
1029
|
+
return null;
|
|
1030
|
+
return { provider, search, page };
|
|
1031
|
+
}
|
|
1032
|
+
function createControl(context, options) {
|
|
1033
|
+
return new ModelsDevControl(context, options);
|
|
1034
|
+
}
|
|
1035
|
+
var control_default = { createControl };
|
|
1036
|
+
export {
|
|
1037
|
+
ModelsDevCatalogManager,
|
|
1038
|
+
createControl,
|
|
1039
|
+
control_default as default
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
//# debugId=0376174D0A69321964756E2164756E21
|