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.
- package/LICENSE +21 -0
- package/README.md +1220 -0
- package/SYSTEM_PROMPT.md +14 -0
- package/SYSTEM_PROMPT_AGENT.md +34 -0
- package/SYSTEM_PROMPT_TOOLS.md +9 -0
- package/bunfig.toml +2 -0
- package/package.json +59 -0
- package/src/agent/agent.ts +615 -0
- package/src/agent/context.ts +161 -0
- package/src/agent/delegation.ts +481 -0
- package/src/agent/loop.ts +569 -0
- package/src/agent/subagent.ts +83 -0
- package/src/ai-sdk/converters.ts +342 -0
- package/src/ai-sdk/errors.ts +122 -0
- package/src/ai-sdk/executor.ts +454 -0
- package/src/ai-sdk/index.ts +55 -0
- package/src/ai-sdk/model-provider.ts +303 -0
- package/src/ai-sdk/options.ts +306 -0
- package/src/ai-sdk/provider.ts +415 -0
- package/src/ai-sdk/registry.ts +416 -0
- package/src/data/README.md +84 -0
- package/src/index.ts +190 -0
- package/src/models/catalog-cache.ts +273 -0
- package/src/models/catalog.ts +503 -0
- package/src/streaming/event-stream.ts +211 -0
- package/src/streaming/sse-parser.ts +97 -0
- package/src/tokens/counter.ts +136 -0
- package/src/tools/executor.ts +365 -0
- package/src/tools/schema.ts +221 -0
- package/src/tools/tool.ts +101 -0
- package/src/types/agent.ts +87 -0
- package/src/types/core.ts +86 -0
- package/src/types/message.ts +115 -0
- package/src/types/model.ts +212 -0
- package/src/types/provider-payloads.ts +434 -0
- package/src/types/response.ts +158 -0
- package/src/types/tool.ts +61 -0
- package/src/utils/base64.ts +27 -0
- package/src/utils/cache.ts +146 -0
- package/src/utils/env.ts +78 -0
- package/src/utils/headers.ts +110 -0
- package/src/utils/media.ts +137 -0
- package/src/utils/serialization.ts +91 -0
- package/src/utils/session.ts +26 -0
- package/src/utils/thought-signature.ts +27 -0
- 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";
|