agent-accelerator 0.1.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1220 -0
  3. package/SYSTEM_PROMPT.md +14 -0
  4. package/SYSTEM_PROMPT_AGENT.md +34 -0
  5. package/SYSTEM_PROMPT_TOOLS.md +9 -0
  6. package/bunfig.toml +2 -0
  7. package/package.json +59 -0
  8. package/src/agent/agent.ts +615 -0
  9. package/src/agent/context.ts +161 -0
  10. package/src/agent/delegation.ts +481 -0
  11. package/src/agent/loop.ts +569 -0
  12. package/src/agent/subagent.ts +83 -0
  13. package/src/ai-sdk/converters.ts +342 -0
  14. package/src/ai-sdk/errors.ts +122 -0
  15. package/src/ai-sdk/executor.ts +454 -0
  16. package/src/ai-sdk/index.ts +55 -0
  17. package/src/ai-sdk/model-provider.ts +303 -0
  18. package/src/ai-sdk/options.ts +306 -0
  19. package/src/ai-sdk/provider.ts +415 -0
  20. package/src/ai-sdk/registry.ts +416 -0
  21. package/src/data/README.md +84 -0
  22. package/src/index.ts +190 -0
  23. package/src/models/catalog-cache.ts +273 -0
  24. package/src/models/catalog.ts +503 -0
  25. package/src/streaming/event-stream.ts +211 -0
  26. package/src/streaming/sse-parser.ts +97 -0
  27. package/src/tokens/counter.ts +136 -0
  28. package/src/tools/executor.ts +365 -0
  29. package/src/tools/schema.ts +221 -0
  30. package/src/tools/tool.ts +101 -0
  31. package/src/types/agent.ts +87 -0
  32. package/src/types/core.ts +86 -0
  33. package/src/types/message.ts +115 -0
  34. package/src/types/model.ts +212 -0
  35. package/src/types/provider-payloads.ts +434 -0
  36. package/src/types/response.ts +158 -0
  37. package/src/types/tool.ts +61 -0
  38. package/src/utils/base64.ts +27 -0
  39. package/src/utils/cache.ts +146 -0
  40. package/src/utils/env.ts +78 -0
  41. package/src/utils/headers.ts +110 -0
  42. package/src/utils/media.ts +137 -0
  43. package/src/utils/serialization.ts +91 -0
  44. package/src/utils/session.ts +26 -0
  45. package/src/utils/thought-signature.ts +27 -0
  46. package/tsconfig.json +31 -0
@@ -0,0 +1,503 @@
1
+ /**
2
+ * Model catalog — battle-tested single source of truth (models.dev)
3
+ * Dynamically loaded and synchronized via ~/.cache with ~12h TTL.
4
+ * All context length, pricing, limits, reasoning, modalities come from here.
5
+ * Providers' hardcoded GOOGLE_MODELS etc. are DEPRECATED fallbacks only.
6
+ */
7
+
8
+ import {
9
+ getActiveCatalog,
10
+ registerCatalogUpdateListener,
11
+ refreshModelCatalog,
12
+ ensureModelCatalogFresh,
13
+ getCatalogStatus,
14
+ setCatalogTTL,
15
+ getCatalogTTL,
16
+ DEFAULT_CATALOG_TTL_MS,
17
+ type CatalogStatus,
18
+ type RefreshCatalogOptions,
19
+ } from "./catalog-cache.ts";
20
+ import type { ModelSpec, ProviderId, ModelLimit, ModelCost, ModelModalities } from "../types/model.ts";
21
+ import type { ThinkingLevel } from "../types/core.ts";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Provider aliases — maps our internal ids to models.dev keys
25
+ // battle-tested: opencode covers zen/go, google covers vertex variants
26
+ // ---------------------------------------------------------------------------
27
+ const PROVIDER_ALIASES: Record<string, string[]> = {
28
+ google: ["google", "google-vertex", "google-vertex-anthropic"],
29
+ opencode: ["opencode", "opencode-go"],
30
+ "opencode-go": ["opencode-go", "opencode"],
31
+ openrouter: ["openrouter"],
32
+ openai: ["openai"],
33
+ };
34
+
35
+ // Verified modality corrections — models.dev lags on some inputs (e.g. gpt-4o
36
+ // accepts audio). Keyed `provider/model`, merged over upstream data so
37
+ // `npm run update-models` never wipes them. Add entries here, not in the JSON.
38
+ const MODALITY_OVERRIDES: Record<string, { input?: string[]; output?: string[] }> = {
39
+ "openai/gpt-4o": { input: ["text", "image", "audio", "pdf"] },
40
+ "openai/gpt-4o-mini": { input: ["text", "image", "audio", "pdf"] },
41
+ // OpenRouter routes this free model to an endpoint that parses PDFs,
42
+ // verified end-to-end via examples/08. If routing changes, the runtime
43
+ // 404 still surfaces as a one-line error.
44
+ "openrouter/nvidia/nemotron-3.5-lightning:free": { input: ["text", "pdf"] },
45
+ };
46
+
47
+ function applyModalityOverrides(provider: string, modelId: string, base: ModelModalities): ModelModalities {
48
+ const hit =
49
+ MODALITY_OVERRIDES[`${provider}/${modelId}`] ??
50
+ MODALITY_OVERRIDES[`${provider}/${normalizeModelIdForLookup(modelId)}`];
51
+ if (!hit) return base;
52
+ return {
53
+ input: ((hit.input ?? base.input) as ModelModalities["input"]),
54
+ output: ((hit.output ?? base.output) as ModelModalities["output"]),
55
+ };
56
+ }
57
+
58
+ function normalizeModelIdForLookup(modelId: string): string {
59
+ if (modelId.includes("/")) {
60
+ const parts = modelId.split("/");
61
+ return parts.slice(1).join("/");
62
+ }
63
+ return modelId;
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Internal cache — battle-tested: avoid repeated global scans (7487 models)
68
+ // ---------------------------------------------------------------------------
69
+ const lookupCache = new Map<string, ModelSpec | undefined>();
70
+ const providerModelsCache = new Map<string, ModelSpec[]>();
71
+
72
+ function mapCapabilities(raw: any, cost: ModelCost, reasoning: boolean, toolCall: boolean, modalities: ModelModalities) {
73
+ return {
74
+ supportsThinking: reasoning,
75
+ supportsThinkingLevel: !!raw.reasoning_options,
76
+ supportsThinkingBudget: Array.isArray(raw.reasoning_options) && raw.reasoning_options.some((o: any) => o.type === "budget_tokens"),
77
+ supportsImplicitCaching: cost.cache_read !== undefined || cost.cache_write !== undefined,
78
+ supportsLongCacheRetention: cost.cache_read !== undefined,
79
+ supportsExplicitCaching: cost.cache_read !== undefined,
80
+ supportsParallelToolCalls: toolCall,
81
+ supportsStreaming: true,
82
+ supportsReasoningToggle: Array.isArray(raw.reasoning_options) && raw.reasoning_options.some((o: any) => o.type === "toggle"),
83
+ supportsReasoningEffort: Array.isArray(raw.reasoning_options) && raw.reasoning_options.some((o: any) => o.type === "effort"),
84
+ modalities: [...(modalities.input || []), ...(modalities.output || [])].filter((v: any, i: any, a: any) => a.indexOf(v) === i),
85
+ };
86
+ }
87
+
88
+ function mapModelSpec(provider: string, modelId: string, raw: any): ModelSpec {
89
+ const limit: ModelLimit = raw.limit || { context: 128000, output: 8192 };
90
+ const cost: ModelCost = raw.cost || {};
91
+ const modalities: ModelModalities = applyModalityOverrides(
92
+ provider,
93
+ modelId,
94
+ raw.modalities || { input: ["text"], output: ["text"] }
95
+ );
96
+ const reasoning = !!raw.reasoning;
97
+ const toolCall = !!raw.tool_call;
98
+
99
+ // Ensure limit.context / limit.output exist
100
+ const contextWindow = limit.context ?? limit.input ?? 128000;
101
+ const maxOutputTokens = limit.output ?? 8192;
102
+
103
+ const mergedLimit: ModelLimit = Object.assign({ context: contextWindow, output: maxOutputTokens }, limit);
104
+ return {
105
+ id: raw.id || modelId,
106
+ provider: provider as ProviderId,
107
+ name: raw.name || modelId,
108
+ description: raw.description,
109
+ family: raw.family,
110
+ api: raw.provider?.api || raw.api,
111
+ contextWindow,
112
+ maxOutputTokens,
113
+ // Battle-tested full fields
114
+ limit: mergedLimit,
115
+ cost,
116
+ modalities,
117
+ reasoning: raw.reasoning,
118
+ reasoning_options: raw.reasoning_options,
119
+ tool_call: raw.tool_call,
120
+ attachment: raw.attachment,
121
+ knowledge: raw.knowledge,
122
+ release_date: raw.release_date,
123
+ last_updated: raw.last_updated,
124
+ capabilities: mapCapabilities(raw, cost, reasoning, toolCall, modalities),
125
+ pricing: {
126
+ inputPerMillion: cost.input,
127
+ outputPerMillion: cost.output,
128
+ cacheReadPerMillion: cost.cache_read,
129
+ cacheWritePerMillion: cost.cache_write,
130
+ inputAudioPerMillion: cost.input_audio,
131
+ },
132
+ raw,
133
+ };
134
+ }
135
+
136
+ interface IndexEntry {
137
+ provider: ProviderId;
138
+ key: string;
139
+ raw: any;
140
+ }
141
+
142
+ let globalIndex: Map<string, IndexEntry> | null = null;
143
+
144
+ function getGlobalIndex(): Map<string, IndexEntry> {
145
+ if (globalIndex) return globalIndex;
146
+ globalIndex = new Map();
147
+ const catalog = getActiveCatalog();
148
+ for (const [p, providerData] of Object.entries(catalog as Record<string, any>)) {
149
+ const models = (providerData as any)?.models;
150
+ if (!models) continue;
151
+ for (const [key, raw] of Object.entries(models as Record<string, any>)) {
152
+ const entry: IndexEntry = { provider: p as ProviderId, key, raw };
153
+ const lowerKey = key.toLowerCase();
154
+ if (!globalIndex.has(lowerKey)) globalIndex.set(lowerKey, entry);
155
+ const stripped = normalizeModelIdForLookup(key).toLowerCase();
156
+ if (!globalIndex.has(stripped)) globalIndex.set(stripped, entry);
157
+ const withSlash = `${p}/${stripped}`.toLowerCase();
158
+ if (!globalIndex.has(withSlash)) globalIndex.set(withSlash, entry);
159
+ }
160
+ }
161
+ return globalIndex;
162
+ }
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // Public: getModelFromCatalog — battle-tested, cached, alias-aware
166
+ // ---------------------------------------------------------------------------
167
+ /**
168
+ * Looks up one model using provider aliases, normalized IDs, and the global catalog index.
169
+ *
170
+ * @example `const model = getModelFromCatalog("google", "gemini-3.5-flash-lite");`
171
+ */
172
+ export function getModelFromCatalog(providerInput: string, modelIdInput: string): ModelSpec | undefined {
173
+ const cacheKey = `${providerInput}::${modelIdInput}`;
174
+ if (lookupCache.has(cacheKey)) return lookupCache.get(cacheKey);
175
+
176
+ const provider = providerInput.toLowerCase();
177
+ const modelId = modelIdInput.trim();
178
+ let result: ModelSpec | undefined;
179
+
180
+ const aliasProviders = PROVIDER_ALIASES[provider] || [provider];
181
+ const tryProviders = [...new Set([...aliasProviders, provider, provider.replace("-zen", ""), provider.replace("-go", "")])];
182
+ const catalog = getActiveCatalog();
183
+
184
+ for (const p of tryProviders) {
185
+ const providerData: any = (catalog as any)[p];
186
+ if (!providerData?.models) continue;
187
+ const candidates = [
188
+ modelId,
189
+ normalizeModelIdForLookup(modelId),
190
+ `${p}/${normalizeModelIdForLookup(modelId)}`,
191
+ `${p}/${modelId}`,
192
+ ];
193
+ for (const cand of candidates) {
194
+ const raw = providerData.models[cand];
195
+ if (raw) {
196
+ result = mapModelSpec(p as ProviderId, cand, raw);
197
+ lookupCache.set(cacheKey, result);
198
+ return result;
199
+ }
200
+ }
201
+ for (const [key, raw] of Object.entries(providerData.models as Record<string, any>)) {
202
+ if (key === modelId || key.endsWith(`/${modelId}`) || modelId.endsWith(key) || normalizeModelIdForLookup(key) === normalizeModelIdForLookup(modelId)) {
203
+ result = mapModelSpec(p as ProviderId, key, raw as any);
204
+ lookupCache.set(cacheKey, result);
205
+ return result;
206
+ }
207
+ }
208
+ }
209
+
210
+ // Global O(1) fallback via indexed catalog
211
+ const index = getGlobalIndex();
212
+ const lowerModelId = modelId.toLowerCase();
213
+ const strippedLower = normalizeModelIdForLookup(modelId).toLowerCase();
214
+ const hit = index.get(lowerModelId) || index.get(strippedLower);
215
+ if (hit) {
216
+ result = mapModelSpec(hit.provider, hit.key, hit.raw);
217
+ lookupCache.set(cacheKey, result);
218
+ return result;
219
+ }
220
+
221
+ lookupCache.set(cacheKey, undefined);
222
+ return undefined;
223
+ }
224
+
225
+ /** Thinking capabilities and allowed levels for a catalog model. */
226
+ export interface ModelThinkingInfo {
227
+ supportsThinking: boolean;
228
+ reasoningOptions?: any[];
229
+ allowedLevels: string[];
230
+ supportsDisable: boolean;
231
+ description: string;
232
+ }
233
+
234
+ /**
235
+ * Returns reasoning support and permitted ThinkingLevel values for a model.
236
+ *
237
+ * @example `const info = getModelThinkingInfo("google", "gemini-3.5-flash-lite");`
238
+ */
239
+ export function getModelThinkingInfo(provider: string, modelId: string): ModelThinkingInfo {
240
+ const spec = getModelFromCatalog(provider, modelId) || getModelFromCatalog(modelId, modelId);
241
+ if (!spec) {
242
+ return {
243
+ supportsThinking: true,
244
+ allowedLevels: ["none", "minimal", "low", "medium", "high", "xhigh", "dynamic"],
245
+ supportsDisable: true,
246
+ description: "Model not found in catalog; generic thinking levels allowed.",
247
+ };
248
+ }
249
+
250
+ const supportsThinking = !!spec.reasoning || spec.capabilities.supportsThinking;
251
+ const reasoningOptions = spec.reasoning_options || [];
252
+
253
+ if (!supportsThinking) {
254
+ return {
255
+ supportsThinking: false,
256
+ reasoningOptions: [],
257
+ allowedLevels: ["none"],
258
+ supportsDisable: true,
259
+ description: `Model "${spec.id}" does not support thinking/reasoning.`,
260
+ };
261
+ }
262
+
263
+ // Model supports thinking — inspect reasoning_options from models.dev.json
264
+ const effortOpt: any = reasoningOptions.find((o: any) => o.type === "effort");
265
+ const toggleOpt: any = reasoningOptions.find((o: any) => o.type === "toggle");
266
+ const budgetOpt: any = reasoningOptions.find((o: any) => o.type === "budget_tokens");
267
+
268
+ const allowed = new Set<string>();
269
+
270
+ if (effortOpt && Array.isArray(effortOpt.values) && effortOpt.values.length > 0) {
271
+ for (const val of effortOpt.values) {
272
+ allowed.add(String(val).toLowerCase());
273
+ }
274
+ }
275
+
276
+ if (toggleOpt) {
277
+ allowed.add("none");
278
+ allowed.add("low");
279
+ allowed.add("medium");
280
+ allowed.add("high");
281
+ allowed.add("dynamic");
282
+ allowed.add("minimal");
283
+ }
284
+
285
+ if (budgetOpt) {
286
+ if (budgetOpt.min === undefined || budgetOpt.min <= 0) {
287
+ allowed.add("none");
288
+ }
289
+ allowed.add("dynamic");
290
+ allowed.add("minimal");
291
+ allowed.add("low");
292
+ allowed.add("medium");
293
+ allowed.add("high");
294
+ allowed.add("xhigh");
295
+ }
296
+
297
+ // Fixed reasoning model (no reasoning_options) e.g. DeepSeek-R1, QwQ-32B
298
+ if (reasoningOptions.length === 0) {
299
+ return {
300
+ supportsThinking: true,
301
+ reasoningOptions: [],
302
+ allowedLevels: [],
303
+ supportsDisable: false,
304
+ description: `Model "${spec.id}" is a fixed-reasoning model and does not support configurable thinking levels.`,
305
+ };
306
+ }
307
+
308
+ const allowedLevels = Array.from(allowed);
309
+ const supportsDisable = allowed.has("none") || !!toggleOpt || Boolean(budgetOpt && (budgetOpt.min === undefined || budgetOpt.min <= 0));
310
+
311
+ return {
312
+ supportsThinking: true,
313
+ reasoningOptions,
314
+ allowedLevels,
315
+ supportsDisable,
316
+ description: `Supported thinking levels for "${spec.id}": [${allowedLevels.map((l) => `"${l}"`).join(", ")}]`,
317
+ };
318
+ }
319
+
320
+ /** Thrown when a requested ThinkingLevel is unsupported by the selected model. */
321
+ export class ThinkingLevelError extends Error {
322
+ readonly provider: string;
323
+ readonly modelId: string;
324
+ readonly requestedLevel: string;
325
+ readonly allowedLevels: string[];
326
+ readonly supportsThinking: boolean;
327
+
328
+ /**
329
+ * Creates a descriptive validation error for an unsupported thinking level.
330
+ *
331
+ * @param opts Provider/model, requested level, supported levels, and remedy.
332
+ */
333
+ constructor(opts: {
334
+ provider: string;
335
+ modelId: string;
336
+ requestedLevel: string;
337
+ allowedLevels: string[];
338
+ supportsThinking: boolean;
339
+ reason: string;
340
+ remedy?: string;
341
+ }) {
342
+ const remedyLines = opts.remedy
343
+ ? `\n\n \x1b[36m💡 How to fix:\x1b[0m\n ${opts.remedy}`
344
+ : "";
345
+ const allowedLine = opts.allowedLevels.length > 0
346
+ ? `\n \x1b[1mAllowed Levels:\x1b[0m [${opts.allowedLevels.map((l) => `"${l}"`).join(", ")}]`
347
+ : "";
348
+
349
+ const formattedMessage =
350
+ `\x1b[31m[Agent Accelerator] ThinkingLevel Mismatch for "${opts.provider}/${opts.modelId}":\x1b[0m\n` +
351
+ ` \x1b[1mRequested Level:\x1b[0m "${opts.requestedLevel}"\n` +
352
+ ` \x1b[1mIssue:\x1b[0m ${opts.reason}` +
353
+ allowedLine +
354
+ remedyLines;
355
+
356
+ super(formattedMessage);
357
+ this.name = "ThinkingLevelError";
358
+ this.provider = opts.provider;
359
+ this.modelId = opts.modelId;
360
+ this.requestedLevel = opts.requestedLevel;
361
+ this.allowedLevels = opts.allowedLevels;
362
+ this.supportsThinking = opts.supportsThinking;
363
+
364
+ if (Error.captureStackTrace) {
365
+ Error.captureStackTrace(this, ThinkingLevelError);
366
+ }
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Validates a ThinkingLevel against catalog metadata and throws ThinkingLevelError on mismatch.
372
+ *
373
+ * @example `validateModelThinking("google", "gemini-3.5-flash-lite", "medium");`
374
+ */
375
+ export function validateModelThinking(provider: string, modelId: string, requestedLevel?: ThinkingLevel | string): void {
376
+ if (!requestedLevel) return;
377
+ const level = String(requestedLevel).toLowerCase().trim();
378
+ const info = getModelThinkingInfo(provider, modelId);
379
+
380
+ if (!info.supportsThinking) {
381
+ if (level !== "none") {
382
+ throw new ThinkingLevelError({
383
+ provider,
384
+ modelId,
385
+ requestedLevel: String(requestedLevel),
386
+ allowedLevels: [],
387
+ supportsThinking: false,
388
+ reason: `Model "${modelId}" does not support thinking/reasoning.`,
389
+ remedy: `Omit thinkingLevel or set thinkingLevel: "none".`,
390
+ });
391
+ }
392
+ return;
393
+ }
394
+
395
+ // Fixed reasoning model
396
+ if (info.allowedLevels.length === 0) {
397
+ if (level === "none") {
398
+ throw new ThinkingLevelError({
399
+ provider,
400
+ modelId,
401
+ requestedLevel: String(requestedLevel),
402
+ allowedLevels: [],
403
+ supportsThinking: true,
404
+ reason: `Model "${modelId}" is a fixed-reasoning model and cannot have thinking disabled ("none").`,
405
+ remedy: `Omit thinkingLevel to use the model's default reasoning.`,
406
+ });
407
+ }
408
+ return;
409
+ }
410
+
411
+ // If user requested "none" on a model that does not allow disabling
412
+ if (level === "none" && !info.supportsDisable) {
413
+ throw new ThinkingLevelError({
414
+ provider,
415
+ modelId,
416
+ requestedLevel: String(requestedLevel),
417
+ allowedLevels: info.allowedLevels,
418
+ supportsThinking: true,
419
+ reason: `Model "${modelId}" requires thinking and does not support disabling it ("none").`,
420
+ remedy: `Set thinkingLevel to one of: ${info.allowedLevels.map((l) => `"${l}"`).join(" | ")}, or omit thinkingLevel to use the model default.`,
421
+ });
422
+ }
423
+
424
+ // If level is not in allowed levels
425
+ if (!info.allowedLevels.includes(level)) {
426
+ throw new ThinkingLevelError({
427
+ provider,
428
+ modelId,
429
+ requestedLevel: String(requestedLevel),
430
+ allowedLevels: info.allowedLevels,
431
+ supportsThinking: true,
432
+ reason: `Invalid thinking level "${requestedLevel}" for model "${modelId}".`,
433
+ remedy: `Set thinkingLevel to one of: ${info.allowedLevels.map((l) => `"${l}"`).join(" | ")}, or omit thinkingLevel to use the model default.`,
434
+ });
435
+ }
436
+ }
437
+
438
+ // ---------------------------------------------------------------------------
439
+ // getModelsForProvider — battle-tested view for BaseProvider.models
440
+ // Opencode total context length now comes from catalog, not stale hardcoded file.
441
+ // ---------------------------------------------------------------------------
442
+ /**
443
+ * Returns normalized catalog specs for a provider and its configured aliases.
444
+ *
445
+ * @example `const googleModels = getModelsForProvider("google");`
446
+ */
447
+ export function getModelsForProvider(provider: string): ModelSpec[] {
448
+ if (providerModelsCache.has(provider)) return providerModelsCache.get(provider)!;
449
+ const aliases = PROVIDER_ALIASES[provider] || [provider];
450
+ const seen = new Set<string>();
451
+ const out: ModelSpec[] = [];
452
+ const catalog = getActiveCatalog();
453
+ for (const alias of aliases) {
454
+ const data: any = (catalog as any)[alias];
455
+ if (!data?.models) continue;
456
+ for (const [key, raw] of Object.entries(data.models as Record<string, any>)) {
457
+ if (seen.has(key)) continue;
458
+ seen.add(key);
459
+ out.push(mapModelSpec(alias as ProviderId, key, raw));
460
+ }
461
+ }
462
+ // Also include our internal provider id mapping
463
+ providerModelsCache.set(provider, out);
464
+ return out;
465
+ }
466
+
467
+ export const GOOGLE_MODELS: ModelSpec[] = [...getModelsForProvider("google")];
468
+ export const OPENAI_MODELS: ModelSpec[] = [...getModelsForProvider("openai")];
469
+ export const OPENCODE_MODELS: ModelSpec[] = [...getModelsForProvider("opencode")];
470
+ export const OPENROUTER_MODELS: ModelSpec[] = [...getModelsForProvider("openrouter")];
471
+
472
+ function refreshExportedModels(): void {
473
+ GOOGLE_MODELS.length = 0;
474
+ GOOGLE_MODELS.push(...getModelsForProvider("google"));
475
+ OPENAI_MODELS.length = 0;
476
+ OPENAI_MODELS.push(...getModelsForProvider("openai"));
477
+ OPENCODE_MODELS.length = 0;
478
+ OPENCODE_MODELS.push(...getModelsForProvider("opencode"));
479
+ OPENROUTER_MODELS.length = 0;
480
+ OPENROUTER_MODELS.push(...getModelsForProvider("openrouter"));
481
+ }
482
+
483
+ // Invalidate caches and refresh exported models whenever the catalog updates
484
+ registerCatalogUpdateListener(() => {
485
+ lookupCache.clear();
486
+ providerModelsCache.clear();
487
+ globalIndex = null;
488
+ refreshExportedModels();
489
+ });
490
+
491
+ // Re-export cache management utilities
492
+ export {
493
+ refreshModelCatalog,
494
+ ensureModelCatalogFresh,
495
+ getCatalogStatus,
496
+ setCatalogTTL,
497
+ getCatalogTTL,
498
+ getCacheDir,
499
+ getCacheFilePath,
500
+ DEFAULT_CATALOG_TTL_MS,
501
+ type CatalogStatus,
502
+ type RefreshCatalogOptions,
503
+ } from "./catalog-cache.ts";