@herouucn/opencode-commandcode 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/_version.txt +1 -1
- package/index.ts +32 -32
- package/manifest.json +34 -34
- package/models.json +2182 -2182
- package/package.json +3 -3
- package/src/auth.ts +43 -43
- package/src/catalog-break.ts +41 -41
- package/src/catalog.ts +769 -769
- package/src/convert.ts +242 -242
- package/src/costs-docs.ts +179 -179
- package/src/costs-models-dev.ts +173 -173
- package/src/manifest.ts +134 -134
- package/src/model.ts +168 -168
- package/src/startup.ts +43 -43
- package/src/stream.ts +237 -237
package/src/catalog.ts
CHANGED
|
@@ -1,769 +1,769 @@
|
|
|
1
|
-
import { createRequire } from "module";
|
|
2
|
-
import { execSync } from "child_process";
|
|
3
|
-
import { existsSync, readFileSync, statSync } from "fs";
|
|
4
|
-
import { dirname, join, resolve as resolvePath } from "path";
|
|
5
|
-
|
|
6
|
-
export const NPM_PACKAGE = "command-code";
|
|
7
|
-
|
|
8
|
-
export interface ModelEntry {
|
|
9
|
-
id: string;
|
|
10
|
-
name: string;
|
|
11
|
-
tier: "premium" | "open-source";
|
|
12
|
-
reasoning: boolean;
|
|
13
|
-
reasoningEfforts?: string[];
|
|
14
|
-
tool_call: boolean;
|
|
15
|
-
cost: { input: number; output: number; cache_read?: number; cache_write?: number };
|
|
16
|
-
limit: { context: number; output: number };
|
|
17
|
-
attachment?: boolean;
|
|
18
|
-
modalities?: { input: string[]; output: string[] };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface CostEntry {
|
|
22
|
-
id: string;
|
|
23
|
-
provider: string;
|
|
24
|
-
category: string;
|
|
25
|
-
promptCost: number;
|
|
26
|
-
completionCost: number;
|
|
27
|
-
cacheWrite5mCost: number;
|
|
28
|
-
cacheWrite1hCost: number;
|
|
29
|
-
cacheHitCost: number;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface SnEntry {
|
|
33
|
-
id: string;
|
|
34
|
-
provider: string;
|
|
35
|
-
spec: string;
|
|
36
|
-
label: string;
|
|
37
|
-
name: string;
|
|
38
|
-
description: string;
|
|
39
|
-
reasoning?: boolean;
|
|
40
|
-
reasoningEfforts?: string[];
|
|
41
|
-
contextWindow?: number;
|
|
42
|
-
inputModalities?: string[];
|
|
43
|
-
maxOutputTokens?: number;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface ResolvedCommandCodePackage {
|
|
47
|
-
root: string;
|
|
48
|
-
bundlePath: string;
|
|
49
|
-
version: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface LocalCatalogResult {
|
|
53
|
-
models: ModelEntry[];
|
|
54
|
-
version: string;
|
|
55
|
-
root: string;
|
|
56
|
-
bundleSource: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export const FALLBACK_LIMITS: Record<string, { context: number; output: number }> = {
|
|
60
|
-
"claude-haiku-4-5-20251001": { context: 200000, output: 8192 },
|
|
61
|
-
"claude-opus-4-6": { context: 200000, output: 32000 },
|
|
62
|
-
"claude-opus-4-7": { context: 200000, output: 32000 },
|
|
63
|
-
"claude-sonnet-4-6": { context: 200000, output: 16000 },
|
|
64
|
-
"gpt-5.5": { context: 256000, output: 128000 },
|
|
65
|
-
"gpt-5.4": { context: 256000, output: 128000 },
|
|
66
|
-
"gpt-5.3-codex": { context: 256000, output: 128000 },
|
|
67
|
-
"gpt-5.4-mini": { context: 256000, output: 128000 },
|
|
68
|
-
"moonshotai/Kimi-K2.6": { context: 262144, output: 131072 },
|
|
69
|
-
"moonshotai/Kimi-K2.5": { context: 262144, output: 131072 },
|
|
70
|
-
"zai-org/GLM-5": { context: 200000, output: 131072 },
|
|
71
|
-
"zai-org/GLM-5.1": { context: 200000, output: 131072 },
|
|
72
|
-
"MiniMaxAI/MiniMax-M2.5": { context: 1000000, output: 131072 },
|
|
73
|
-
"MiniMaxAI/MiniMax-M2.7": { context: 1000000, output: 131072 },
|
|
74
|
-
"deepseek/deepseek-v4-pro": { context: 1000000, output: 384000 },
|
|
75
|
-
"deepseek/deepseek-v4-flash": { context: 1000000, output: 384000 },
|
|
76
|
-
"Qwen/Qwen3.6-Max-Preview": { context: 1000000, output: 131072 },
|
|
77
|
-
"Qwen/Qwen3.6-Plus": { context: 1000000, output: 131072 },
|
|
78
|
-
"Qwen/Qwen3.7-Max": { context: 1000000, output: 131072 },
|
|
79
|
-
"stepfun/Step-3.5-Flash": { context: 1000000, output: 131072 },
|
|
80
|
-
"google/gemini-3.5-flash": { context: 1000000, output: 65536 },
|
|
81
|
-
"google/gemini-3.1-flash-lite": { context: 1000000, output: 65536 },
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
export const HARDCODED_EXTRAS: SnEntry[] = [
|
|
85
|
-
{
|
|
86
|
-
id: "Qwen/Qwen3.7-Max",
|
|
87
|
-
provider: "vercel-ai-gateway",
|
|
88
|
-
spec: "chatComplete",
|
|
89
|
-
label: "Qwen 3.7 Max",
|
|
90
|
-
name: "Qwen 3.7 Max",
|
|
91
|
-
description: "latest Qwen Max model",
|
|
92
|
-
reasoning: true,
|
|
93
|
-
},
|
|
94
|
-
];
|
|
95
|
-
|
|
96
|
-
export const TIER_MAP: Record<string, "premium" | "open-source"> = {
|
|
97
|
-
anthropic: "premium",
|
|
98
|
-
openai: "premium",
|
|
99
|
-
baseten: "open-source",
|
|
100
|
-
"vercel-ai-gateway": "open-source",
|
|
101
|
-
openrouter: "open-source",
|
|
102
|
-
"cloudflare-ai-gateway": "open-source",
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const CATALOG_ANCHOR = 'SONNET_4_6:{id:"claude-sonnet-4-6"';
|
|
106
|
-
const COST_ANCHORS = [
|
|
107
|
-
'{id:"anthropic:claude-sonnet-4-',
|
|
108
|
-
'{id:"anthropic:claude-sonnet-5"',
|
|
109
|
-
'{id:"anthropic:claude-sonnet-4-6"',
|
|
110
|
-
] as const;
|
|
111
|
-
|
|
112
|
-
/** Object after `(` (legacy IIFE / enum form): `(Wt={...})` */
|
|
113
|
-
export function findBalancedObject(source: string, anchor: string): string {
|
|
114
|
-
const anchorIdx = source.indexOf(anchor);
|
|
115
|
-
if (anchorIdx < 0) throw new Error(`Anchor not found: ${anchor}`);
|
|
116
|
-
|
|
117
|
-
let parenIdx = anchorIdx - 1;
|
|
118
|
-
while (parenIdx >= 0 && source[parenIdx] !== "(") parenIdx--;
|
|
119
|
-
if (parenIdx < 0) throw new Error(`Could not find opening ( before anchor: ${anchor}`);
|
|
120
|
-
|
|
121
|
-
const braceStart = source.indexOf("{", parenIdx);
|
|
122
|
-
if (braceStart < 0) throw new Error(`Could not find { after opening (`);
|
|
123
|
-
|
|
124
|
-
let depth = 0;
|
|
125
|
-
let end = braceStart;
|
|
126
|
-
for (; end < source.length; end++) {
|
|
127
|
-
if (source[end] === "{") depth++;
|
|
128
|
-
else if (source[end] === "}") {
|
|
129
|
-
depth--;
|
|
130
|
-
if (depth === 0) break;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return source.slice(braceStart, end + 1);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** All balanced `{...}` spans that contain the anchor, innermost → outermost. */
|
|
138
|
-
export function findEnclosingObjectCandidates(
|
|
139
|
-
source: string,
|
|
140
|
-
anchor: string,
|
|
141
|
-
fromIndex = 0,
|
|
142
|
-
): string[] {
|
|
143
|
-
const anchorIdx = source.indexOf(anchor, fromIndex);
|
|
144
|
-
if (anchorIdx < 0) throw new Error(`Anchor not found: ${anchor}`);
|
|
145
|
-
|
|
146
|
-
const stack: number[] = [];
|
|
147
|
-
for (let i = 0; i <= anchorIdx; i++) {
|
|
148
|
-
const ch = source[i];
|
|
149
|
-
if (ch === "{") stack.push(i);
|
|
150
|
-
else if (ch === "}") stack.pop();
|
|
151
|
-
}
|
|
152
|
-
if (stack.length === 0) throw new Error(`Could not find enclosing { for anchor: ${anchor}`);
|
|
153
|
-
|
|
154
|
-
const MAX = 2_000_000;
|
|
155
|
-
const out: string[] = [];
|
|
156
|
-
// innermost first
|
|
157
|
-
for (let s = stack.length - 1; s >= 0; s--) {
|
|
158
|
-
const start = stack[s];
|
|
159
|
-
if (start === undefined) continue;
|
|
160
|
-
let depth = 0;
|
|
161
|
-
let end = start;
|
|
162
|
-
for (; end < source.length; end++) {
|
|
163
|
-
if (source[end] === "{") depth++;
|
|
164
|
-
else if (source[end] === "}") {
|
|
165
|
-
depth--;
|
|
166
|
-
if (depth === 0) break;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
if (end >= source.length || end < anchorIdx) continue;
|
|
170
|
-
if (end + 1 - start > MAX) continue;
|
|
171
|
-
out.push(source.slice(start, end + 1));
|
|
172
|
-
}
|
|
173
|
-
if (out.length === 0) throw new Error(`Could not close enclosing { for anchor: ${anchor}`);
|
|
174
|
-
return out;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** Default: innermost enclosing object (good for plain catalogs). */
|
|
178
|
-
export function findEnclosingObject(source: string, anchor: string, fromIndex = 0): string {
|
|
179
|
-
const first = findEnclosingObjectCandidates(source, anchor, fromIndex)[0];
|
|
180
|
-
if (!first) throw new Error(`Could not find enclosing object for anchor: ${anchor}`);
|
|
181
|
-
return first;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export function evaluateWithContext(code: string, context: Record<string, unknown>): any {
|
|
185
|
-
const keys = Object.keys(context);
|
|
186
|
-
const values = keys.map((k) => context[k]);
|
|
187
|
-
const fn = Function(...keys, `"use strict"; return (${code})`);
|
|
188
|
-
return fn(...values);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export function normalizeForEval(code: string): string {
|
|
192
|
-
return code
|
|
193
|
-
.replace(/!0/g, "true")
|
|
194
|
-
.replace(/!1/g, "false")
|
|
195
|
-
.replace(/(\d+)e(\d+)/g, (_: string, m: string, e: string) =>
|
|
196
|
-
String(Number(m) * Math.pow(10, Number(e))),
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function getVarNameBefore(source: string, anchorIdx: number): string | null {
|
|
201
|
-
const before = source.slice(Math.max(0, anchorIdx - 50), anchorIdx);
|
|
202
|
-
const match = before.match(/\(([A-Za-z_$]+)=\{$/);
|
|
203
|
-
if (match?.[1]) return match[1];
|
|
204
|
-
const match2 = before.match(/([A-Za-z_$]+)=\{$/);
|
|
205
|
-
if (match2?.[1]) return match2[1];
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/** Prefer provider enums that include gateway/openai keys over UI-only enums. */
|
|
210
|
-
export function extractBestProviderEnum(
|
|
211
|
-
source: string,
|
|
212
|
-
): { name: string; value: Record<string, string> } | null {
|
|
213
|
-
const anchor = 'ANTHROPIC:"anthropic"';
|
|
214
|
-
let from = 0;
|
|
215
|
-
let fallback: { name: string; value: Record<string, string> } | null = null;
|
|
216
|
-
|
|
217
|
-
while (from < source.length) {
|
|
218
|
-
const idx = source.indexOf(anchor, from);
|
|
219
|
-
if (idx < 0) break;
|
|
220
|
-
try {
|
|
221
|
-
let parenIdx = idx - 1;
|
|
222
|
-
while (parenIdx >= 0 && source[parenIdx] !== "(") parenIdx--;
|
|
223
|
-
if (parenIdx < 0) {
|
|
224
|
-
from = idx + 1;
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
const braceStart = source.indexOf("{", parenIdx);
|
|
228
|
-
if (braceStart < 0 || braceStart > idx) {
|
|
229
|
-
from = idx + 1;
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
let depth = 0;
|
|
233
|
-
let end = braceStart;
|
|
234
|
-
for (; end < source.length; end++) {
|
|
235
|
-
if (source[end] === "{") depth++;
|
|
236
|
-
else if (source[end] === "}") {
|
|
237
|
-
depth--;
|
|
238
|
-
if (depth === 0) break;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
const obj = source.slice(braceStart, end + 1);
|
|
242
|
-
const value = evaluateWithContext(normalizeForEval(obj), {}) as Record<string, string>;
|
|
243
|
-
const name = getVarNameBefore(source, idx) || "Wt";
|
|
244
|
-
const rich =
|
|
245
|
-
"OPENAI" in value ||
|
|
246
|
-
"BASETEN" in value ||
|
|
247
|
-
"VERCEL_AI_GATEWAY" in value ||
|
|
248
|
-
Object.values(value).includes("openai");
|
|
249
|
-
const hit = { name, value };
|
|
250
|
-
if (rich) return hit;
|
|
251
|
-
if (!fallback) fallback = hit;
|
|
252
|
-
} catch {
|
|
253
|
-
// try next occurrence
|
|
254
|
-
}
|
|
255
|
-
from = idx + 1;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return fallback;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/** Collect nearby `name="value"` bindings and simple aliases (`tI=Qx`). */
|
|
262
|
-
export function extractStringBindings(
|
|
263
|
-
source: string,
|
|
264
|
-
endIdx: number,
|
|
265
|
-
window = 12000,
|
|
266
|
-
): Record<string, string> {
|
|
267
|
-
const before = source.slice(Math.max(0, endIdx - window), endIdx);
|
|
268
|
-
const bindings: Record<string, string> = {};
|
|
269
|
-
// \b does not fire before "$" (not a word char); minified bundles use names like
|
|
270
|
-
// $R="vercel-ai-gateway". A lookbehind boundary handles both $ and letter names.
|
|
271
|
-
const strRe = /(?<![A-Za-z0-9_$])([A-Za-z_$][\w$]*)="([^"]*)"/g;
|
|
272
|
-
let m: RegExpExecArray | null;
|
|
273
|
-
while ((m = strRe.exec(before))) {
|
|
274
|
-
const name = m[1];
|
|
275
|
-
const value = m[2];
|
|
276
|
-
if (name !== undefined && value !== undefined) bindings[name] = value;
|
|
277
|
-
}
|
|
278
|
-
for (let pass = 0; pass < 4; pass++) {
|
|
279
|
-
const aliasRe = /(?<![A-Za-z0-9_$])([A-Za-z_$][\w$]*)=([A-Za-z_$][\w$]*)(?![A-Za-z0-9_$])/g;
|
|
280
|
-
while ((m = aliasRe.exec(before))) {
|
|
281
|
-
const alias = m[1];
|
|
282
|
-
const target = m[2];
|
|
283
|
-
if (alias === undefined || target === undefined || bindings[alias] !== undefined) continue;
|
|
284
|
-
const resolved = bindings[target];
|
|
285
|
-
if (resolved !== undefined) bindings[alias] = resolved;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
return bindings;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
export function extractSpecConstants(source: string): {
|
|
292
|
-
chatComplete: string;
|
|
293
|
-
responses: string;
|
|
294
|
-
qt: string;
|
|
295
|
-
} {
|
|
296
|
-
const anchorIdx = source.indexOf(CATALOG_ANCHOR);
|
|
297
|
-
if (anchorIdx < 0) throw new Error("Could not find model catalog anchor");
|
|
298
|
-
|
|
299
|
-
const before = source.slice(Math.max(0, anchorIdx - 5000), anchorIdx);
|
|
300
|
-
|
|
301
|
-
const chatMatch = before.match(/([A-Za-z_$]+)="chatComplete"/);
|
|
302
|
-
const respMatch = before.match(/([A-Za-z_$]+)="responses"/);
|
|
303
|
-
const chatName = chatMatch?.[1];
|
|
304
|
-
const respName = respMatch?.[1];
|
|
305
|
-
if (!chatName || !respName) throw new Error("Could not find spec constants");
|
|
306
|
-
|
|
307
|
-
const qtMatch = before.match(/([A-Za-z_$]+)=Vt\[0\]/);
|
|
308
|
-
const qtVar = qtMatch?.[1] ?? "";
|
|
309
|
-
|
|
310
|
-
return {
|
|
311
|
-
chatComplete: chatName,
|
|
312
|
-
responses: respName,
|
|
313
|
-
qt: qtVar,
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function buildEvalContext(source: string, catalogIdx: number): Record<string, unknown> {
|
|
318
|
-
const ctx: Record<string, unknown> = {
|
|
319
|
-
...extractStringBindings(source, catalogIdx),
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
const spec = extractSpecConstants(source);
|
|
323
|
-
ctx[spec.chatComplete] = ctx[spec.chatComplete] ?? "chatComplete";
|
|
324
|
-
ctx[spec.responses] = ctx[spec.responses] ?? "responses";
|
|
325
|
-
|
|
326
|
-
const providerEnum = extractBestProviderEnum(source);
|
|
327
|
-
if (providerEnum) {
|
|
328
|
-
ctx[providerEnum.name] = providerEnum.value;
|
|
329
|
-
if (spec.qt && providerEnum.value.VERCEL_AI_GATEWAY) {
|
|
330
|
-
ctx[spec.qt] = providerEnum.value.VERCEL_AI_GATEWAY;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
return ctx;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function isModelCatalog(value: unknown): value is Record<string, SnEntry> {
|
|
338
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
339
|
-
const vals = Object.values(value as Record<string, unknown>);
|
|
340
|
-
if (vals.length < 2) return false;
|
|
341
|
-
let modelish = 0;
|
|
342
|
-
for (const v of vals) {
|
|
343
|
-
if (
|
|
344
|
-
v &&
|
|
345
|
-
typeof v === "object" &&
|
|
346
|
-
!Array.isArray(v) &&
|
|
347
|
-
typeof (v as SnEntry).id === "string" &&
|
|
348
|
-
typeof (v as SnEntry).name === "string"
|
|
349
|
-
) {
|
|
350
|
-
modelish++;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
return modelish >= 2;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
export function extractModelCatalog(
|
|
357
|
-
source: string,
|
|
358
|
-
wt?: Record<string, string>,
|
|
359
|
-
wtName?: string,
|
|
360
|
-
spec?: ReturnType<typeof extractSpecConstants>,
|
|
361
|
-
): Record<string, SnEntry> {
|
|
362
|
-
const catalogIdx = source.indexOf(CATALOG_ANCHOR);
|
|
363
|
-
if (catalogIdx < 0) throw new Error("Could not find model catalog anchor");
|
|
364
|
-
|
|
365
|
-
const ctx = buildEvalContext(source, catalogIdx);
|
|
366
|
-
if (wt && wtName) ctx[wtName] = wt;
|
|
367
|
-
if (spec) {
|
|
368
|
-
ctx[spec.chatComplete] = "chatComplete";
|
|
369
|
-
ctx[spec.responses] = "responses";
|
|
370
|
-
if (spec.qt && wt?.VERCEL_AI_GATEWAY) ctx[spec.qt] = wt.VERCEL_AI_GATEWAY;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const candidates: string[] = [];
|
|
374
|
-
try {
|
|
375
|
-
candidates.push(...findEnclosingObjectCandidates(source, CATALOG_ANCHOR, catalogIdx));
|
|
376
|
-
} catch {
|
|
377
|
-
// ignore
|
|
378
|
-
}
|
|
379
|
-
try {
|
|
380
|
-
candidates.push(findBalancedObject(source, CATALOG_ANCHOR));
|
|
381
|
-
} catch {
|
|
382
|
-
// ignore
|
|
383
|
-
}
|
|
384
|
-
if (candidates.length === 0) throw new Error("Could not locate model catalog object");
|
|
385
|
-
|
|
386
|
-
let lastError: unknown = null;
|
|
387
|
-
for (const raw of candidates) {
|
|
388
|
-
try {
|
|
389
|
-
const value = evaluateWithContext(normalizeForEval(raw), ctx);
|
|
390
|
-
if (isModelCatalog(value)) return value;
|
|
391
|
-
} catch (err) {
|
|
392
|
-
// keep the first real failure (e.g. "$R is not defined") — it names the
|
|
393
|
-
// missing binding; later candidates usually fail on the same cause
|
|
394
|
-
lastError ??= err;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const detail = lastError instanceof Error ? `: ${lastError.message}` : "";
|
|
399
|
-
throw new Error(`Could not evaluate model catalog${detail}`);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function isCostMap(value: unknown): value is Record<string, CostEntry[]> {
|
|
403
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
404
|
-
const vals = Object.values(value as Record<string, unknown>);
|
|
405
|
-
if (vals.length === 0) return false;
|
|
406
|
-
// At least one provider bucket should be an array of cost rows
|
|
407
|
-
return vals.some(
|
|
408
|
-
(v) =>
|
|
409
|
-
Array.isArray(v) &&
|
|
410
|
-
v.length > 0 &&
|
|
411
|
-
typeof v[0] === "object" &&
|
|
412
|
-
v[0] !== null &&
|
|
413
|
-
typeof (v[0] as CostEntry).promptCost === "number",
|
|
414
|
-
);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
export function extractCostData(
|
|
418
|
-
source: string,
|
|
419
|
-
wt?: Record<string, string>,
|
|
420
|
-
wtName?: string,
|
|
421
|
-
): Record<string, CostEntry[]> {
|
|
422
|
-
let anchorIdx = -1;
|
|
423
|
-
let usedAnchor: string | null = null;
|
|
424
|
-
for (const anchor of COST_ANCHORS) {
|
|
425
|
-
const idx = source.indexOf(anchor);
|
|
426
|
-
if (idx >= 0) {
|
|
427
|
-
anchorIdx = idx;
|
|
428
|
-
usedAnchor = anchor;
|
|
429
|
-
break;
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
if (anchorIdx < 0 || !usedAnchor) throw new Error("Could not find cost data anchor");
|
|
433
|
-
|
|
434
|
-
const candidates = findEnclosingObjectCandidates(source, usedAnchor, anchorIdx);
|
|
435
|
-
const catalogIdx = source.indexOf(CATALOG_ANCHOR);
|
|
436
|
-
const ctx = buildEvalContext(source, catalogIdx >= 0 ? catalogIdx : anchorIdx);
|
|
437
|
-
if (wt && wtName) ctx[wtName] = wt;
|
|
438
|
-
|
|
439
|
-
for (const raw of candidates) {
|
|
440
|
-
try {
|
|
441
|
-
const value = evaluateWithContext(normalizeForEval(raw), ctx);
|
|
442
|
-
if (isCostMap(value)) return value;
|
|
443
|
-
} catch {
|
|
444
|
-
// try next enclosing span
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
throw new Error("Could not evaluate cost data map");
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
export function buildCostMap(costs: Record<string, CostEntry[]>): Map<string, CostEntry> {
|
|
452
|
-
const map = new Map<string, CostEntry>();
|
|
453
|
-
for (const arr of Object.values(costs)) {
|
|
454
|
-
for (const entry of arr) {
|
|
455
|
-
const colonIdx = entry.id.indexOf(":");
|
|
456
|
-
const bareId = colonIdx >= 0 ? entry.id.slice(colonIdx + 1) : entry.id;
|
|
457
|
-
map.set(bareId, entry);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
return map;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
export function buildModelEntry(
|
|
464
|
-
entry: SnEntry,
|
|
465
|
-
costMap: Map<string, CostEntry>,
|
|
466
|
-
): ModelEntry | null {
|
|
467
|
-
const provider = entry.provider || "unknown";
|
|
468
|
-
const tier = TIER_MAP[provider] ?? "open-source";
|
|
469
|
-
|
|
470
|
-
const costEntry = costMap.get(entry.id);
|
|
471
|
-
let cost: { input: number; output: number; cache_read?: number; cache_write?: number };
|
|
472
|
-
if (costEntry) {
|
|
473
|
-
cost = {
|
|
474
|
-
input: costEntry.promptCost,
|
|
475
|
-
output: costEntry.completionCost,
|
|
476
|
-
};
|
|
477
|
-
if (costEntry.cacheHitCost > 0) cost.cache_read = costEntry.cacheHitCost;
|
|
478
|
-
if (costEntry.cacheWrite5mCost > 0) cost.cache_write = costEntry.cacheWrite5mCost;
|
|
479
|
-
} else {
|
|
480
|
-
// ponytail: sync fills real prices (docs → free $0 → models.dev); this is only the unmatched placeholder
|
|
481
|
-
cost = { input: 0.5, output: 2 };
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
const fallback = FALLBACK_LIMITS[entry.id];
|
|
485
|
-
const limit = {
|
|
486
|
-
context: entry.contextWindow ?? fallback?.context ?? 200000,
|
|
487
|
-
output: entry.maxOutputTokens ?? fallback?.output ?? 65536,
|
|
488
|
-
};
|
|
489
|
-
|
|
490
|
-
const efforts = entry.reasoningEfforts?.length ? [...entry.reasoningEfforts] : undefined;
|
|
491
|
-
const input = entry.inputModalities?.filter((x) => typeof x === "string") ?? [];
|
|
492
|
-
const fromCli =
|
|
493
|
-
input.length > 0
|
|
494
|
-
? {
|
|
495
|
-
attachment: input.includes("image"),
|
|
496
|
-
modalities: { input: [...input], output: ["text"] },
|
|
497
|
-
}
|
|
498
|
-
: {};
|
|
499
|
-
|
|
500
|
-
return {
|
|
501
|
-
id: entry.id,
|
|
502
|
-
name: entry.name,
|
|
503
|
-
tier,
|
|
504
|
-
reasoning: entry.reasoning || (efforts?.length ?? 0) > 0,
|
|
505
|
-
...(efforts ? { reasoningEfforts: efforts } : {}),
|
|
506
|
-
tool_call: true,
|
|
507
|
-
cost,
|
|
508
|
-
limit,
|
|
509
|
-
...fromCli,
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
export function disambiguateModelNames(entries: ModelEntry[]): ModelEntry[] {
|
|
514
|
-
const groups = new Map<string, ModelEntry[]>();
|
|
515
|
-
for (const entry of entries) {
|
|
516
|
-
const group = groups.get(entry.name) ?? [];
|
|
517
|
-
group.push(entry);
|
|
518
|
-
groups.set(entry.name, group);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
for (const group of groups.values()) {
|
|
522
|
-
if (group.length < 2) continue;
|
|
523
|
-
for (const entry of group) {
|
|
524
|
-
const suffix = entry.id.split("/").pop() ?? entry.id;
|
|
525
|
-
const normalized = suffix.replace(/[-_]/g, " ");
|
|
526
|
-
if (normalized.toLowerCase().includes("free")) {
|
|
527
|
-
entry.name = `${entry.name} Free`;
|
|
528
|
-
} else if (normalized.toLowerCase() !== entry.name.toLowerCase()) {
|
|
529
|
-
entry.name = `${entry.name} (${suffix})`;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
return entries;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
export function sortModelEntries(entries: ModelEntry[]): ModelEntry[] {
|
|
538
|
-
disambiguateModelNames(entries);
|
|
539
|
-
return entries.sort((a, b) => {
|
|
540
|
-
if (a.tier !== b.tier) return a.tier === "premium" ? -1 : 1;
|
|
541
|
-
return a.name.localeCompare(b.name);
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
export function loadCatalogFromBundle(source: string): ModelEntry[] {
|
|
546
|
-
const models = extractModelCatalog(source);
|
|
547
|
-
let costMap = new Map<string, CostEntry>();
|
|
548
|
-
try {
|
|
549
|
-
costMap = buildCostMap(extractCostData(source));
|
|
550
|
-
} catch {
|
|
551
|
-
// ponytail: CLI cost map is optional; buildModelEntry uses unmatched placeholder until sync fills prices
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
const entries: ModelEntry[] = [];
|
|
555
|
-
for (const model of Object.values(models)) {
|
|
556
|
-
if (!model || typeof model !== "object" || typeof model.id !== "string") continue;
|
|
557
|
-
const entry = buildModelEntry(model, costMap);
|
|
558
|
-
if (entry) entries.push(entry);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
for (const extra of HARDCODED_EXTRAS) {
|
|
562
|
-
if (!entries.some((e) => e.id === extra.id)) {
|
|
563
|
-
const entry = buildModelEntry(extra, costMap);
|
|
564
|
-
if (entry) entries.push(entry);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
return sortModelEntries(entries);
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
function readPackageMeta(root: string): { version: string; bundlePath: string } | null {
|
|
572
|
-
const pkgPath = join(root, "package.json");
|
|
573
|
-
if (!existsSync(pkgPath)) return null;
|
|
574
|
-
try {
|
|
575
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as {
|
|
576
|
-
version?: string;
|
|
577
|
-
module?: string;
|
|
578
|
-
main?: string;
|
|
579
|
-
bin?: string | Record<string, string>;
|
|
580
|
-
exports?: unknown;
|
|
581
|
-
};
|
|
582
|
-
|
|
583
|
-
const binPaths: string[] = [];
|
|
584
|
-
if (typeof pkg.bin === "string") binPaths.push(join(root, pkg.bin));
|
|
585
|
-
else if (pkg.bin && typeof pkg.bin === "object") {
|
|
586
|
-
for (const rel of Object.values(pkg.bin)) {
|
|
587
|
-
if (typeof rel === "string") binPaths.push(join(root, rel));
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// Prefer the full CLI bundle. Newer command-code ships dist/index.mjs as a
|
|
592
|
-
// tiny re-export stub; the catalog lives in dist/cli.mjs (or bin target).
|
|
593
|
-
const candidates = [
|
|
594
|
-
join(root, "dist", "cli.mjs"),
|
|
595
|
-
...binPaths,
|
|
596
|
-
join(root, "dist", "index.mjs"),
|
|
597
|
-
pkg.module ? join(root, pkg.module) : "",
|
|
598
|
-
pkg.main ? join(root, pkg.main) : "",
|
|
599
|
-
].filter(Boolean);
|
|
600
|
-
|
|
601
|
-
let best: { path: string; size: number } | null = null;
|
|
602
|
-
for (const candidate of candidates) {
|
|
603
|
-
if (!existsSync(candidate)) continue;
|
|
604
|
-
try {
|
|
605
|
-
const size = statSync(candidate).size;
|
|
606
|
-
// Skip tiny launcher stubs (< 4KB) that only re-export another file
|
|
607
|
-
if (size < 4096) continue;
|
|
608
|
-
if (!best || size > best.size) best = { path: candidate, size };
|
|
609
|
-
} catch {
|
|
610
|
-
// ignore unreadable candidate
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
if (best) {
|
|
615
|
-
return { version: pkg.version ?? "unknown", bundlePath: best.path };
|
|
616
|
-
}
|
|
617
|
-
} catch {
|
|
618
|
-
return null;
|
|
619
|
-
}
|
|
620
|
-
return null;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
function asPackageRoot(path: string): ResolvedCommandCodePackage | null {
|
|
624
|
-
const resolved = resolvePath(path);
|
|
625
|
-
if (!existsSync(resolved)) return null;
|
|
626
|
-
|
|
627
|
-
// Allow pointing at a file; still prefer the full CLI bundle under the package root
|
|
628
|
-
if (resolved.endsWith(".mjs") || resolved.endsWith(".js")) {
|
|
629
|
-
const root = dirname(dirname(resolved));
|
|
630
|
-
const meta = readPackageMeta(root);
|
|
631
|
-
if (meta) return { root, bundlePath: meta.bundlePath, version: meta.version };
|
|
632
|
-
try {
|
|
633
|
-
if (statSync(resolved).size >= 4096) {
|
|
634
|
-
return { root, bundlePath: resolved, version: "unknown" };
|
|
635
|
-
}
|
|
636
|
-
} catch {
|
|
637
|
-
return null;
|
|
638
|
-
}
|
|
639
|
-
return null;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
const meta = readPackageMeta(resolved);
|
|
643
|
-
if (!meta) return null;
|
|
644
|
-
return { root: resolved, bundlePath: meta.bundlePath, version: meta.version };
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
function tryRequireResolve(): ResolvedCommandCodePackage | null {
|
|
648
|
-
try {
|
|
649
|
-
const require = createRequire(import.meta.url);
|
|
650
|
-
const pkgJson = require.resolve(`${NPM_PACKAGE}/package.json`);
|
|
651
|
-
return asPackageRoot(dirname(pkgJson));
|
|
652
|
-
} catch {
|
|
653
|
-
// try resolving from cwd
|
|
654
|
-
}
|
|
655
|
-
try {
|
|
656
|
-
const require = createRequire(join(process.cwd(), "package.json"));
|
|
657
|
-
const pkgJson = require.resolve(`${NPM_PACKAGE}/package.json`);
|
|
658
|
-
return asPackageRoot(dirname(pkgJson));
|
|
659
|
-
} catch {
|
|
660
|
-
return null;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
function walkNodeModules(startDir: string): ResolvedCommandCodePackage | null {
|
|
665
|
-
let dir = resolvePath(startDir);
|
|
666
|
-
for (let i = 0; i < 12; i++) {
|
|
667
|
-
const candidate = join(dir, "node_modules", NPM_PACKAGE);
|
|
668
|
-
const found = asPackageRoot(candidate);
|
|
669
|
-
if (found) return found;
|
|
670
|
-
const parent = dirname(dir);
|
|
671
|
-
if (parent === dir) break;
|
|
672
|
-
dir = parent;
|
|
673
|
-
}
|
|
674
|
-
return null;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
function tryNpmRootGlobal(): ResolvedCommandCodePackage | null {
|
|
678
|
-
try {
|
|
679
|
-
const root = execSync("npm root -g", {
|
|
680
|
-
encoding: "utf-8",
|
|
681
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
682
|
-
timeout: 3000,
|
|
683
|
-
}).trim();
|
|
684
|
-
if (!root) return null;
|
|
685
|
-
return asPackageRoot(join(root, NPM_PACKAGE));
|
|
686
|
-
} catch {
|
|
687
|
-
return null;
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
function tryWindowsAppData(): ResolvedCommandCodePackage | null {
|
|
692
|
-
const appData = process.env.APPDATA;
|
|
693
|
-
if (!appData) return null;
|
|
694
|
-
return asPackageRoot(join(appData, "npm", "node_modules", NPM_PACKAGE));
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
export function resolveCommandCodePackage(options?: {
|
|
698
|
-
packagePath?: string;
|
|
699
|
-
}): ResolvedCommandCodePackage | null {
|
|
700
|
-
const explicit =
|
|
701
|
-
options?.packagePath?.trim() || process.env.COMMANDCODE_PACKAGE_PATH?.trim() || "";
|
|
702
|
-
|
|
703
|
-
if (explicit) {
|
|
704
|
-
const found = asPackageRoot(explicit);
|
|
705
|
-
if (found) return found;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
return (
|
|
709
|
-
tryRequireResolve() ||
|
|
710
|
-
walkNodeModules(process.cwd()) ||
|
|
711
|
-
tryNpmRootGlobal() ||
|
|
712
|
-
tryWindowsAppData() ||
|
|
713
|
-
null
|
|
714
|
-
);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
export function loadCatalogFromLocalCommandCode(options?: {
|
|
718
|
-
packagePath?: string;
|
|
719
|
-
}): LocalCatalogResult | null {
|
|
720
|
-
try {
|
|
721
|
-
const resolved = resolveCommandCodePackage(options);
|
|
722
|
-
if (!resolved) return null;
|
|
723
|
-
const source = readFileSync(resolved.bundlePath, "utf-8");
|
|
724
|
-
const models = loadCatalogFromBundle(source);
|
|
725
|
-
if (models.length === 0) return null;
|
|
726
|
-
return { models, version: resolved.version, root: resolved.root, bundleSource: source };
|
|
727
|
-
} catch {
|
|
728
|
-
return null;
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
export function toConfigKey(id: string): string {
|
|
733
|
-
const slashIdx = id.indexOf("/");
|
|
734
|
-
const short = slashIdx >= 0 ? id.slice(slashIdx + 1) : id;
|
|
735
|
-
return short.toLowerCase();
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
export function generateOpencodeModels(entries: ModelEntry[]): Record<string, unknown> {
|
|
739
|
-
const models: Record<string, unknown> = {};
|
|
740
|
-
for (const entry of entries) {
|
|
741
|
-
const key = toConfigKey(entry.id);
|
|
742
|
-
const costObj: Record<string, number> = { input: entry.cost.input, output: entry.cost.output };
|
|
743
|
-
if (entry.cost.cache_read !== undefined) costObj.cache_read = entry.cost.cache_read;
|
|
744
|
-
if (entry.cost.cache_write !== undefined) costObj.cache_write = entry.cost.cache_write;
|
|
745
|
-
|
|
746
|
-
const model: Record<string, unknown> = {
|
|
747
|
-
id: entry.id,
|
|
748
|
-
name: entry.name,
|
|
749
|
-
reasoning: entry.reasoning,
|
|
750
|
-
tool_call: entry.tool_call,
|
|
751
|
-
attachment: entry.attachment ?? false,
|
|
752
|
-
modalities: entry.modalities ?? { input: ["text"], output: ["text"] },
|
|
753
|
-
cost: costObj,
|
|
754
|
-
limit: entry.limit,
|
|
755
|
-
};
|
|
756
|
-
|
|
757
|
-
if (entry.reasoningEfforts && entry.reasoningEfforts.length > 0) {
|
|
758
|
-
model.reasoningEfforts = entry.reasoningEfforts;
|
|
759
|
-
const variants: Record<string, Record<string, unknown>> = {};
|
|
760
|
-
for (const effort of entry.reasoningEfforts) {
|
|
761
|
-
variants[effort] = { reasoningEffort: effort };
|
|
762
|
-
}
|
|
763
|
-
model.variants = variants;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
models[key] = model;
|
|
767
|
-
}
|
|
768
|
-
return models;
|
|
769
|
-
}
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
4
|
+
import { dirname, join, resolve as resolvePath } from "path";
|
|
5
|
+
|
|
6
|
+
export const NPM_PACKAGE = "command-code";
|
|
7
|
+
|
|
8
|
+
export interface ModelEntry {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
tier: "premium" | "open-source";
|
|
12
|
+
reasoning: boolean;
|
|
13
|
+
reasoningEfforts?: string[];
|
|
14
|
+
tool_call: boolean;
|
|
15
|
+
cost: { input: number; output: number; cache_read?: number; cache_write?: number };
|
|
16
|
+
limit: { context: number; output: number };
|
|
17
|
+
attachment?: boolean;
|
|
18
|
+
modalities?: { input: string[]; output: string[] };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CostEntry {
|
|
22
|
+
id: string;
|
|
23
|
+
provider: string;
|
|
24
|
+
category: string;
|
|
25
|
+
promptCost: number;
|
|
26
|
+
completionCost: number;
|
|
27
|
+
cacheWrite5mCost: number;
|
|
28
|
+
cacheWrite1hCost: number;
|
|
29
|
+
cacheHitCost: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SnEntry {
|
|
33
|
+
id: string;
|
|
34
|
+
provider: string;
|
|
35
|
+
spec: string;
|
|
36
|
+
label: string;
|
|
37
|
+
name: string;
|
|
38
|
+
description: string;
|
|
39
|
+
reasoning?: boolean;
|
|
40
|
+
reasoningEfforts?: string[];
|
|
41
|
+
contextWindow?: number;
|
|
42
|
+
inputModalities?: string[];
|
|
43
|
+
maxOutputTokens?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ResolvedCommandCodePackage {
|
|
47
|
+
root: string;
|
|
48
|
+
bundlePath: string;
|
|
49
|
+
version: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface LocalCatalogResult {
|
|
53
|
+
models: ModelEntry[];
|
|
54
|
+
version: string;
|
|
55
|
+
root: string;
|
|
56
|
+
bundleSource: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const FALLBACK_LIMITS: Record<string, { context: number; output: number }> = {
|
|
60
|
+
"claude-haiku-4-5-20251001": { context: 200000, output: 8192 },
|
|
61
|
+
"claude-opus-4-6": { context: 200000, output: 32000 },
|
|
62
|
+
"claude-opus-4-7": { context: 200000, output: 32000 },
|
|
63
|
+
"claude-sonnet-4-6": { context: 200000, output: 16000 },
|
|
64
|
+
"gpt-5.5": { context: 256000, output: 128000 },
|
|
65
|
+
"gpt-5.4": { context: 256000, output: 128000 },
|
|
66
|
+
"gpt-5.3-codex": { context: 256000, output: 128000 },
|
|
67
|
+
"gpt-5.4-mini": { context: 256000, output: 128000 },
|
|
68
|
+
"moonshotai/Kimi-K2.6": { context: 262144, output: 131072 },
|
|
69
|
+
"moonshotai/Kimi-K2.5": { context: 262144, output: 131072 },
|
|
70
|
+
"zai-org/GLM-5": { context: 200000, output: 131072 },
|
|
71
|
+
"zai-org/GLM-5.1": { context: 200000, output: 131072 },
|
|
72
|
+
"MiniMaxAI/MiniMax-M2.5": { context: 1000000, output: 131072 },
|
|
73
|
+
"MiniMaxAI/MiniMax-M2.7": { context: 1000000, output: 131072 },
|
|
74
|
+
"deepseek/deepseek-v4-pro": { context: 1000000, output: 384000 },
|
|
75
|
+
"deepseek/deepseek-v4-flash": { context: 1000000, output: 384000 },
|
|
76
|
+
"Qwen/Qwen3.6-Max-Preview": { context: 1000000, output: 131072 },
|
|
77
|
+
"Qwen/Qwen3.6-Plus": { context: 1000000, output: 131072 },
|
|
78
|
+
"Qwen/Qwen3.7-Max": { context: 1000000, output: 131072 },
|
|
79
|
+
"stepfun/Step-3.5-Flash": { context: 1000000, output: 131072 },
|
|
80
|
+
"google/gemini-3.5-flash": { context: 1000000, output: 65536 },
|
|
81
|
+
"google/gemini-3.1-flash-lite": { context: 1000000, output: 65536 },
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const HARDCODED_EXTRAS: SnEntry[] = [
|
|
85
|
+
{
|
|
86
|
+
id: "Qwen/Qwen3.7-Max",
|
|
87
|
+
provider: "vercel-ai-gateway",
|
|
88
|
+
spec: "chatComplete",
|
|
89
|
+
label: "Qwen 3.7 Max",
|
|
90
|
+
name: "Qwen 3.7 Max",
|
|
91
|
+
description: "latest Qwen Max model",
|
|
92
|
+
reasoning: true,
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
export const TIER_MAP: Record<string, "premium" | "open-source"> = {
|
|
97
|
+
anthropic: "premium",
|
|
98
|
+
openai: "premium",
|
|
99
|
+
baseten: "open-source",
|
|
100
|
+
"vercel-ai-gateway": "open-source",
|
|
101
|
+
openrouter: "open-source",
|
|
102
|
+
"cloudflare-ai-gateway": "open-source",
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const CATALOG_ANCHOR = 'SONNET_4_6:{id:"claude-sonnet-4-6"';
|
|
106
|
+
const COST_ANCHORS = [
|
|
107
|
+
'{id:"anthropic:claude-sonnet-4-',
|
|
108
|
+
'{id:"anthropic:claude-sonnet-5"',
|
|
109
|
+
'{id:"anthropic:claude-sonnet-4-6"',
|
|
110
|
+
] as const;
|
|
111
|
+
|
|
112
|
+
/** Object after `(` (legacy IIFE / enum form): `(Wt={...})` */
|
|
113
|
+
export function findBalancedObject(source: string, anchor: string): string {
|
|
114
|
+
const anchorIdx = source.indexOf(anchor);
|
|
115
|
+
if (anchorIdx < 0) throw new Error(`Anchor not found: ${anchor}`);
|
|
116
|
+
|
|
117
|
+
let parenIdx = anchorIdx - 1;
|
|
118
|
+
while (parenIdx >= 0 && source[parenIdx] !== "(") parenIdx--;
|
|
119
|
+
if (parenIdx < 0) throw new Error(`Could not find opening ( before anchor: ${anchor}`);
|
|
120
|
+
|
|
121
|
+
const braceStart = source.indexOf("{", parenIdx);
|
|
122
|
+
if (braceStart < 0) throw new Error(`Could not find { after opening (`);
|
|
123
|
+
|
|
124
|
+
let depth = 0;
|
|
125
|
+
let end = braceStart;
|
|
126
|
+
for (; end < source.length; end++) {
|
|
127
|
+
if (source[end] === "{") depth++;
|
|
128
|
+
else if (source[end] === "}") {
|
|
129
|
+
depth--;
|
|
130
|
+
if (depth === 0) break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return source.slice(braceStart, end + 1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** All balanced `{...}` spans that contain the anchor, innermost → outermost. */
|
|
138
|
+
export function findEnclosingObjectCandidates(
|
|
139
|
+
source: string,
|
|
140
|
+
anchor: string,
|
|
141
|
+
fromIndex = 0,
|
|
142
|
+
): string[] {
|
|
143
|
+
const anchorIdx = source.indexOf(anchor, fromIndex);
|
|
144
|
+
if (anchorIdx < 0) throw new Error(`Anchor not found: ${anchor}`);
|
|
145
|
+
|
|
146
|
+
const stack: number[] = [];
|
|
147
|
+
for (let i = 0; i <= anchorIdx; i++) {
|
|
148
|
+
const ch = source[i];
|
|
149
|
+
if (ch === "{") stack.push(i);
|
|
150
|
+
else if (ch === "}") stack.pop();
|
|
151
|
+
}
|
|
152
|
+
if (stack.length === 0) throw new Error(`Could not find enclosing { for anchor: ${anchor}`);
|
|
153
|
+
|
|
154
|
+
const MAX = 2_000_000;
|
|
155
|
+
const out: string[] = [];
|
|
156
|
+
// innermost first
|
|
157
|
+
for (let s = stack.length - 1; s >= 0; s--) {
|
|
158
|
+
const start = stack[s];
|
|
159
|
+
if (start === undefined) continue;
|
|
160
|
+
let depth = 0;
|
|
161
|
+
let end = start;
|
|
162
|
+
for (; end < source.length; end++) {
|
|
163
|
+
if (source[end] === "{") depth++;
|
|
164
|
+
else if (source[end] === "}") {
|
|
165
|
+
depth--;
|
|
166
|
+
if (depth === 0) break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (end >= source.length || end < anchorIdx) continue;
|
|
170
|
+
if (end + 1 - start > MAX) continue;
|
|
171
|
+
out.push(source.slice(start, end + 1));
|
|
172
|
+
}
|
|
173
|
+
if (out.length === 0) throw new Error(`Could not close enclosing { for anchor: ${anchor}`);
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Default: innermost enclosing object (good for plain catalogs). */
|
|
178
|
+
export function findEnclosingObject(source: string, anchor: string, fromIndex = 0): string {
|
|
179
|
+
const first = findEnclosingObjectCandidates(source, anchor, fromIndex)[0];
|
|
180
|
+
if (!first) throw new Error(`Could not find enclosing object for anchor: ${anchor}`);
|
|
181
|
+
return first;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function evaluateWithContext(code: string, context: Record<string, unknown>): any {
|
|
185
|
+
const keys = Object.keys(context);
|
|
186
|
+
const values = keys.map((k) => context[k]);
|
|
187
|
+
const fn = Function(...keys, `"use strict"; return (${code})`);
|
|
188
|
+
return fn(...values);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function normalizeForEval(code: string): string {
|
|
192
|
+
return code
|
|
193
|
+
.replace(/!0/g, "true")
|
|
194
|
+
.replace(/!1/g, "false")
|
|
195
|
+
.replace(/(\d+)e(\d+)/g, (_: string, m: string, e: string) =>
|
|
196
|
+
String(Number(m) * Math.pow(10, Number(e))),
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function getVarNameBefore(source: string, anchorIdx: number): string | null {
|
|
201
|
+
const before = source.slice(Math.max(0, anchorIdx - 50), anchorIdx);
|
|
202
|
+
const match = before.match(/\(([A-Za-z_$]+)=\{$/);
|
|
203
|
+
if (match?.[1]) return match[1];
|
|
204
|
+
const match2 = before.match(/([A-Za-z_$]+)=\{$/);
|
|
205
|
+
if (match2?.[1]) return match2[1];
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Prefer provider enums that include gateway/openai keys over UI-only enums. */
|
|
210
|
+
export function extractBestProviderEnum(
|
|
211
|
+
source: string,
|
|
212
|
+
): { name: string; value: Record<string, string> } | null {
|
|
213
|
+
const anchor = 'ANTHROPIC:"anthropic"';
|
|
214
|
+
let from = 0;
|
|
215
|
+
let fallback: { name: string; value: Record<string, string> } | null = null;
|
|
216
|
+
|
|
217
|
+
while (from < source.length) {
|
|
218
|
+
const idx = source.indexOf(anchor, from);
|
|
219
|
+
if (idx < 0) break;
|
|
220
|
+
try {
|
|
221
|
+
let parenIdx = idx - 1;
|
|
222
|
+
while (parenIdx >= 0 && source[parenIdx] !== "(") parenIdx--;
|
|
223
|
+
if (parenIdx < 0) {
|
|
224
|
+
from = idx + 1;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const braceStart = source.indexOf("{", parenIdx);
|
|
228
|
+
if (braceStart < 0 || braceStart > idx) {
|
|
229
|
+
from = idx + 1;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
let depth = 0;
|
|
233
|
+
let end = braceStart;
|
|
234
|
+
for (; end < source.length; end++) {
|
|
235
|
+
if (source[end] === "{") depth++;
|
|
236
|
+
else if (source[end] === "}") {
|
|
237
|
+
depth--;
|
|
238
|
+
if (depth === 0) break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const obj = source.slice(braceStart, end + 1);
|
|
242
|
+
const value = evaluateWithContext(normalizeForEval(obj), {}) as Record<string, string>;
|
|
243
|
+
const name = getVarNameBefore(source, idx) || "Wt";
|
|
244
|
+
const rich =
|
|
245
|
+
"OPENAI" in value ||
|
|
246
|
+
"BASETEN" in value ||
|
|
247
|
+
"VERCEL_AI_GATEWAY" in value ||
|
|
248
|
+
Object.values(value).includes("openai");
|
|
249
|
+
const hit = { name, value };
|
|
250
|
+
if (rich) return hit;
|
|
251
|
+
if (!fallback) fallback = hit;
|
|
252
|
+
} catch {
|
|
253
|
+
// try next occurrence
|
|
254
|
+
}
|
|
255
|
+
from = idx + 1;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return fallback;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Collect nearby `name="value"` bindings and simple aliases (`tI=Qx`). */
|
|
262
|
+
export function extractStringBindings(
|
|
263
|
+
source: string,
|
|
264
|
+
endIdx: number,
|
|
265
|
+
window = 12000,
|
|
266
|
+
): Record<string, string> {
|
|
267
|
+
const before = source.slice(Math.max(0, endIdx - window), endIdx);
|
|
268
|
+
const bindings: Record<string, string> = {};
|
|
269
|
+
// \b does not fire before "$" (not a word char); minified bundles use names like
|
|
270
|
+
// $R="vercel-ai-gateway". A lookbehind boundary handles both $ and letter names.
|
|
271
|
+
const strRe = /(?<![A-Za-z0-9_$])([A-Za-z_$][\w$]*)="([^"]*)"/g;
|
|
272
|
+
let m: RegExpExecArray | null;
|
|
273
|
+
while ((m = strRe.exec(before))) {
|
|
274
|
+
const name = m[1];
|
|
275
|
+
const value = m[2];
|
|
276
|
+
if (name !== undefined && value !== undefined) bindings[name] = value;
|
|
277
|
+
}
|
|
278
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
279
|
+
const aliasRe = /(?<![A-Za-z0-9_$])([A-Za-z_$][\w$]*)=([A-Za-z_$][\w$]*)(?![A-Za-z0-9_$])/g;
|
|
280
|
+
while ((m = aliasRe.exec(before))) {
|
|
281
|
+
const alias = m[1];
|
|
282
|
+
const target = m[2];
|
|
283
|
+
if (alias === undefined || target === undefined || bindings[alias] !== undefined) continue;
|
|
284
|
+
const resolved = bindings[target];
|
|
285
|
+
if (resolved !== undefined) bindings[alias] = resolved;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return bindings;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function extractSpecConstants(source: string): {
|
|
292
|
+
chatComplete: string;
|
|
293
|
+
responses: string;
|
|
294
|
+
qt: string;
|
|
295
|
+
} {
|
|
296
|
+
const anchorIdx = source.indexOf(CATALOG_ANCHOR);
|
|
297
|
+
if (anchorIdx < 0) throw new Error("Could not find model catalog anchor");
|
|
298
|
+
|
|
299
|
+
const before = source.slice(Math.max(0, anchorIdx - 5000), anchorIdx);
|
|
300
|
+
|
|
301
|
+
const chatMatch = before.match(/([A-Za-z_$]+)="chatComplete"/);
|
|
302
|
+
const respMatch = before.match(/([A-Za-z_$]+)="responses"/);
|
|
303
|
+
const chatName = chatMatch?.[1];
|
|
304
|
+
const respName = respMatch?.[1];
|
|
305
|
+
if (!chatName || !respName) throw new Error("Could not find spec constants");
|
|
306
|
+
|
|
307
|
+
const qtMatch = before.match(/([A-Za-z_$]+)=Vt\[0\]/);
|
|
308
|
+
const qtVar = qtMatch?.[1] ?? "";
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
chatComplete: chatName,
|
|
312
|
+
responses: respName,
|
|
313
|
+
qt: qtVar,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function buildEvalContext(source: string, catalogIdx: number): Record<string, unknown> {
|
|
318
|
+
const ctx: Record<string, unknown> = {
|
|
319
|
+
...extractStringBindings(source, catalogIdx),
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const spec = extractSpecConstants(source);
|
|
323
|
+
ctx[spec.chatComplete] = ctx[spec.chatComplete] ?? "chatComplete";
|
|
324
|
+
ctx[spec.responses] = ctx[spec.responses] ?? "responses";
|
|
325
|
+
|
|
326
|
+
const providerEnum = extractBestProviderEnum(source);
|
|
327
|
+
if (providerEnum) {
|
|
328
|
+
ctx[providerEnum.name] = providerEnum.value;
|
|
329
|
+
if (spec.qt && providerEnum.value.VERCEL_AI_GATEWAY) {
|
|
330
|
+
ctx[spec.qt] = providerEnum.value.VERCEL_AI_GATEWAY;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return ctx;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function isModelCatalog(value: unknown): value is Record<string, SnEntry> {
|
|
338
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
339
|
+
const vals = Object.values(value as Record<string, unknown>);
|
|
340
|
+
if (vals.length < 2) return false;
|
|
341
|
+
let modelish = 0;
|
|
342
|
+
for (const v of vals) {
|
|
343
|
+
if (
|
|
344
|
+
v &&
|
|
345
|
+
typeof v === "object" &&
|
|
346
|
+
!Array.isArray(v) &&
|
|
347
|
+
typeof (v as SnEntry).id === "string" &&
|
|
348
|
+
typeof (v as SnEntry).name === "string"
|
|
349
|
+
) {
|
|
350
|
+
modelish++;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return modelish >= 2;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function extractModelCatalog(
|
|
357
|
+
source: string,
|
|
358
|
+
wt?: Record<string, string>,
|
|
359
|
+
wtName?: string,
|
|
360
|
+
spec?: ReturnType<typeof extractSpecConstants>,
|
|
361
|
+
): Record<string, SnEntry> {
|
|
362
|
+
const catalogIdx = source.indexOf(CATALOG_ANCHOR);
|
|
363
|
+
if (catalogIdx < 0) throw new Error("Could not find model catalog anchor");
|
|
364
|
+
|
|
365
|
+
const ctx = buildEvalContext(source, catalogIdx);
|
|
366
|
+
if (wt && wtName) ctx[wtName] = wt;
|
|
367
|
+
if (spec) {
|
|
368
|
+
ctx[spec.chatComplete] = "chatComplete";
|
|
369
|
+
ctx[spec.responses] = "responses";
|
|
370
|
+
if (spec.qt && wt?.VERCEL_AI_GATEWAY) ctx[spec.qt] = wt.VERCEL_AI_GATEWAY;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const candidates: string[] = [];
|
|
374
|
+
try {
|
|
375
|
+
candidates.push(...findEnclosingObjectCandidates(source, CATALOG_ANCHOR, catalogIdx));
|
|
376
|
+
} catch {
|
|
377
|
+
// ignore
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
candidates.push(findBalancedObject(source, CATALOG_ANCHOR));
|
|
381
|
+
} catch {
|
|
382
|
+
// ignore
|
|
383
|
+
}
|
|
384
|
+
if (candidates.length === 0) throw new Error("Could not locate model catalog object");
|
|
385
|
+
|
|
386
|
+
let lastError: unknown = null;
|
|
387
|
+
for (const raw of candidates) {
|
|
388
|
+
try {
|
|
389
|
+
const value = evaluateWithContext(normalizeForEval(raw), ctx);
|
|
390
|
+
if (isModelCatalog(value)) return value;
|
|
391
|
+
} catch (err) {
|
|
392
|
+
// keep the first real failure (e.g. "$R is not defined") — it names the
|
|
393
|
+
// missing binding; later candidates usually fail on the same cause
|
|
394
|
+
lastError ??= err;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const detail = lastError instanceof Error ? `: ${lastError.message}` : "";
|
|
399
|
+
throw new Error(`Could not evaluate model catalog${detail}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function isCostMap(value: unknown): value is Record<string, CostEntry[]> {
|
|
403
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
404
|
+
const vals = Object.values(value as Record<string, unknown>);
|
|
405
|
+
if (vals.length === 0) return false;
|
|
406
|
+
// At least one provider bucket should be an array of cost rows
|
|
407
|
+
return vals.some(
|
|
408
|
+
(v) =>
|
|
409
|
+
Array.isArray(v) &&
|
|
410
|
+
v.length > 0 &&
|
|
411
|
+
typeof v[0] === "object" &&
|
|
412
|
+
v[0] !== null &&
|
|
413
|
+
typeof (v[0] as CostEntry).promptCost === "number",
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function extractCostData(
|
|
418
|
+
source: string,
|
|
419
|
+
wt?: Record<string, string>,
|
|
420
|
+
wtName?: string,
|
|
421
|
+
): Record<string, CostEntry[]> {
|
|
422
|
+
let anchorIdx = -1;
|
|
423
|
+
let usedAnchor: string | null = null;
|
|
424
|
+
for (const anchor of COST_ANCHORS) {
|
|
425
|
+
const idx = source.indexOf(anchor);
|
|
426
|
+
if (idx >= 0) {
|
|
427
|
+
anchorIdx = idx;
|
|
428
|
+
usedAnchor = anchor;
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (anchorIdx < 0 || !usedAnchor) throw new Error("Could not find cost data anchor");
|
|
433
|
+
|
|
434
|
+
const candidates = findEnclosingObjectCandidates(source, usedAnchor, anchorIdx);
|
|
435
|
+
const catalogIdx = source.indexOf(CATALOG_ANCHOR);
|
|
436
|
+
const ctx = buildEvalContext(source, catalogIdx >= 0 ? catalogIdx : anchorIdx);
|
|
437
|
+
if (wt && wtName) ctx[wtName] = wt;
|
|
438
|
+
|
|
439
|
+
for (const raw of candidates) {
|
|
440
|
+
try {
|
|
441
|
+
const value = evaluateWithContext(normalizeForEval(raw), ctx);
|
|
442
|
+
if (isCostMap(value)) return value;
|
|
443
|
+
} catch {
|
|
444
|
+
// try next enclosing span
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
throw new Error("Could not evaluate cost data map");
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function buildCostMap(costs: Record<string, CostEntry[]>): Map<string, CostEntry> {
|
|
452
|
+
const map = new Map<string, CostEntry>();
|
|
453
|
+
for (const arr of Object.values(costs)) {
|
|
454
|
+
for (const entry of arr) {
|
|
455
|
+
const colonIdx = entry.id.indexOf(":");
|
|
456
|
+
const bareId = colonIdx >= 0 ? entry.id.slice(colonIdx + 1) : entry.id;
|
|
457
|
+
map.set(bareId, entry);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return map;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function buildModelEntry(
|
|
464
|
+
entry: SnEntry,
|
|
465
|
+
costMap: Map<string, CostEntry>,
|
|
466
|
+
): ModelEntry | null {
|
|
467
|
+
const provider = entry.provider || "unknown";
|
|
468
|
+
const tier = TIER_MAP[provider] ?? "open-source";
|
|
469
|
+
|
|
470
|
+
const costEntry = costMap.get(entry.id);
|
|
471
|
+
let cost: { input: number; output: number; cache_read?: number; cache_write?: number };
|
|
472
|
+
if (costEntry) {
|
|
473
|
+
cost = {
|
|
474
|
+
input: costEntry.promptCost,
|
|
475
|
+
output: costEntry.completionCost,
|
|
476
|
+
};
|
|
477
|
+
if (costEntry.cacheHitCost > 0) cost.cache_read = costEntry.cacheHitCost;
|
|
478
|
+
if (costEntry.cacheWrite5mCost > 0) cost.cache_write = costEntry.cacheWrite5mCost;
|
|
479
|
+
} else {
|
|
480
|
+
// ponytail: sync fills real prices (docs → free $0 → models.dev); this is only the unmatched placeholder
|
|
481
|
+
cost = { input: 0.5, output: 2 };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const fallback = FALLBACK_LIMITS[entry.id];
|
|
485
|
+
const limit = {
|
|
486
|
+
context: entry.contextWindow ?? fallback?.context ?? 200000,
|
|
487
|
+
output: entry.maxOutputTokens ?? fallback?.output ?? 65536,
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const efforts = entry.reasoningEfforts?.length ? [...entry.reasoningEfforts] : undefined;
|
|
491
|
+
const input = entry.inputModalities?.filter((x) => typeof x === "string") ?? [];
|
|
492
|
+
const fromCli =
|
|
493
|
+
input.length > 0
|
|
494
|
+
? {
|
|
495
|
+
attachment: input.includes("image"),
|
|
496
|
+
modalities: { input: [...input], output: ["text"] },
|
|
497
|
+
}
|
|
498
|
+
: {};
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
id: entry.id,
|
|
502
|
+
name: entry.name,
|
|
503
|
+
tier,
|
|
504
|
+
reasoning: entry.reasoning || (efforts?.length ?? 0) > 0,
|
|
505
|
+
...(efforts ? { reasoningEfforts: efforts } : {}),
|
|
506
|
+
tool_call: true,
|
|
507
|
+
cost,
|
|
508
|
+
limit,
|
|
509
|
+
...fromCli,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export function disambiguateModelNames(entries: ModelEntry[]): ModelEntry[] {
|
|
514
|
+
const groups = new Map<string, ModelEntry[]>();
|
|
515
|
+
for (const entry of entries) {
|
|
516
|
+
const group = groups.get(entry.name) ?? [];
|
|
517
|
+
group.push(entry);
|
|
518
|
+
groups.set(entry.name, group);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
for (const group of groups.values()) {
|
|
522
|
+
if (group.length < 2) continue;
|
|
523
|
+
for (const entry of group) {
|
|
524
|
+
const suffix = entry.id.split("/").pop() ?? entry.id;
|
|
525
|
+
const normalized = suffix.replace(/[-_]/g, " ");
|
|
526
|
+
if (normalized.toLowerCase().includes("free")) {
|
|
527
|
+
entry.name = `${entry.name} Free`;
|
|
528
|
+
} else if (normalized.toLowerCase() !== entry.name.toLowerCase()) {
|
|
529
|
+
entry.name = `${entry.name} (${suffix})`;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return entries;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export function sortModelEntries(entries: ModelEntry[]): ModelEntry[] {
|
|
538
|
+
disambiguateModelNames(entries);
|
|
539
|
+
return entries.sort((a, b) => {
|
|
540
|
+
if (a.tier !== b.tier) return a.tier === "premium" ? -1 : 1;
|
|
541
|
+
return a.name.localeCompare(b.name);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export function loadCatalogFromBundle(source: string): ModelEntry[] {
|
|
546
|
+
const models = extractModelCatalog(source);
|
|
547
|
+
let costMap = new Map<string, CostEntry>();
|
|
548
|
+
try {
|
|
549
|
+
costMap = buildCostMap(extractCostData(source));
|
|
550
|
+
} catch {
|
|
551
|
+
// ponytail: CLI cost map is optional; buildModelEntry uses unmatched placeholder until sync fills prices
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const entries: ModelEntry[] = [];
|
|
555
|
+
for (const model of Object.values(models)) {
|
|
556
|
+
if (!model || typeof model !== "object" || typeof model.id !== "string") continue;
|
|
557
|
+
const entry = buildModelEntry(model, costMap);
|
|
558
|
+
if (entry) entries.push(entry);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
for (const extra of HARDCODED_EXTRAS) {
|
|
562
|
+
if (!entries.some((e) => e.id === extra.id)) {
|
|
563
|
+
const entry = buildModelEntry(extra, costMap);
|
|
564
|
+
if (entry) entries.push(entry);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return sortModelEntries(entries);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function readPackageMeta(root: string): { version: string; bundlePath: string } | null {
|
|
572
|
+
const pkgPath = join(root, "package.json");
|
|
573
|
+
if (!existsSync(pkgPath)) return null;
|
|
574
|
+
try {
|
|
575
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as {
|
|
576
|
+
version?: string;
|
|
577
|
+
module?: string;
|
|
578
|
+
main?: string;
|
|
579
|
+
bin?: string | Record<string, string>;
|
|
580
|
+
exports?: unknown;
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
const binPaths: string[] = [];
|
|
584
|
+
if (typeof pkg.bin === "string") binPaths.push(join(root, pkg.bin));
|
|
585
|
+
else if (pkg.bin && typeof pkg.bin === "object") {
|
|
586
|
+
for (const rel of Object.values(pkg.bin)) {
|
|
587
|
+
if (typeof rel === "string") binPaths.push(join(root, rel));
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Prefer the full CLI bundle. Newer command-code ships dist/index.mjs as a
|
|
592
|
+
// tiny re-export stub; the catalog lives in dist/cli.mjs (or bin target).
|
|
593
|
+
const candidates = [
|
|
594
|
+
join(root, "dist", "cli.mjs"),
|
|
595
|
+
...binPaths,
|
|
596
|
+
join(root, "dist", "index.mjs"),
|
|
597
|
+
pkg.module ? join(root, pkg.module) : "",
|
|
598
|
+
pkg.main ? join(root, pkg.main) : "",
|
|
599
|
+
].filter(Boolean);
|
|
600
|
+
|
|
601
|
+
let best: { path: string; size: number } | null = null;
|
|
602
|
+
for (const candidate of candidates) {
|
|
603
|
+
if (!existsSync(candidate)) continue;
|
|
604
|
+
try {
|
|
605
|
+
const size = statSync(candidate).size;
|
|
606
|
+
// Skip tiny launcher stubs (< 4KB) that only re-export another file
|
|
607
|
+
if (size < 4096) continue;
|
|
608
|
+
if (!best || size > best.size) best = { path: candidate, size };
|
|
609
|
+
} catch {
|
|
610
|
+
// ignore unreadable candidate
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (best) {
|
|
615
|
+
return { version: pkg.version ?? "unknown", bundlePath: best.path };
|
|
616
|
+
}
|
|
617
|
+
} catch {
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function asPackageRoot(path: string): ResolvedCommandCodePackage | null {
|
|
624
|
+
const resolved = resolvePath(path);
|
|
625
|
+
if (!existsSync(resolved)) return null;
|
|
626
|
+
|
|
627
|
+
// Allow pointing at a file; still prefer the full CLI bundle under the package root
|
|
628
|
+
if (resolved.endsWith(".mjs") || resolved.endsWith(".js")) {
|
|
629
|
+
const root = dirname(dirname(resolved));
|
|
630
|
+
const meta = readPackageMeta(root);
|
|
631
|
+
if (meta) return { root, bundlePath: meta.bundlePath, version: meta.version };
|
|
632
|
+
try {
|
|
633
|
+
if (statSync(resolved).size >= 4096) {
|
|
634
|
+
return { root, bundlePath: resolved, version: "unknown" };
|
|
635
|
+
}
|
|
636
|
+
} catch {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const meta = readPackageMeta(resolved);
|
|
643
|
+
if (!meta) return null;
|
|
644
|
+
return { root: resolved, bundlePath: meta.bundlePath, version: meta.version };
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function tryRequireResolve(): ResolvedCommandCodePackage | null {
|
|
648
|
+
try {
|
|
649
|
+
const require = createRequire(import.meta.url);
|
|
650
|
+
const pkgJson = require.resolve(`${NPM_PACKAGE}/package.json`);
|
|
651
|
+
return asPackageRoot(dirname(pkgJson));
|
|
652
|
+
} catch {
|
|
653
|
+
// try resolving from cwd
|
|
654
|
+
}
|
|
655
|
+
try {
|
|
656
|
+
const require = createRequire(join(process.cwd(), "package.json"));
|
|
657
|
+
const pkgJson = require.resolve(`${NPM_PACKAGE}/package.json`);
|
|
658
|
+
return asPackageRoot(dirname(pkgJson));
|
|
659
|
+
} catch {
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function walkNodeModules(startDir: string): ResolvedCommandCodePackage | null {
|
|
665
|
+
let dir = resolvePath(startDir);
|
|
666
|
+
for (let i = 0; i < 12; i++) {
|
|
667
|
+
const candidate = join(dir, "node_modules", NPM_PACKAGE);
|
|
668
|
+
const found = asPackageRoot(candidate);
|
|
669
|
+
if (found) return found;
|
|
670
|
+
const parent = dirname(dir);
|
|
671
|
+
if (parent === dir) break;
|
|
672
|
+
dir = parent;
|
|
673
|
+
}
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function tryNpmRootGlobal(): ResolvedCommandCodePackage | null {
|
|
678
|
+
try {
|
|
679
|
+
const root = execSync("npm root -g", {
|
|
680
|
+
encoding: "utf-8",
|
|
681
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
682
|
+
timeout: 3000,
|
|
683
|
+
}).trim();
|
|
684
|
+
if (!root) return null;
|
|
685
|
+
return asPackageRoot(join(root, NPM_PACKAGE));
|
|
686
|
+
} catch {
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function tryWindowsAppData(): ResolvedCommandCodePackage | null {
|
|
692
|
+
const appData = process.env.APPDATA;
|
|
693
|
+
if (!appData) return null;
|
|
694
|
+
return asPackageRoot(join(appData, "npm", "node_modules", NPM_PACKAGE));
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export function resolveCommandCodePackage(options?: {
|
|
698
|
+
packagePath?: string;
|
|
699
|
+
}): ResolvedCommandCodePackage | null {
|
|
700
|
+
const explicit =
|
|
701
|
+
options?.packagePath?.trim() || process.env.COMMANDCODE_PACKAGE_PATH?.trim() || "";
|
|
702
|
+
|
|
703
|
+
if (explicit) {
|
|
704
|
+
const found = asPackageRoot(explicit);
|
|
705
|
+
if (found) return found;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return (
|
|
709
|
+
tryRequireResolve() ||
|
|
710
|
+
walkNodeModules(process.cwd()) ||
|
|
711
|
+
tryNpmRootGlobal() ||
|
|
712
|
+
tryWindowsAppData() ||
|
|
713
|
+
null
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
export function loadCatalogFromLocalCommandCode(options?: {
|
|
718
|
+
packagePath?: string;
|
|
719
|
+
}): LocalCatalogResult | null {
|
|
720
|
+
try {
|
|
721
|
+
const resolved = resolveCommandCodePackage(options);
|
|
722
|
+
if (!resolved) return null;
|
|
723
|
+
const source = readFileSync(resolved.bundlePath, "utf-8");
|
|
724
|
+
const models = loadCatalogFromBundle(source);
|
|
725
|
+
if (models.length === 0) return null;
|
|
726
|
+
return { models, version: resolved.version, root: resolved.root, bundleSource: source };
|
|
727
|
+
} catch {
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
export function toConfigKey(id: string): string {
|
|
733
|
+
const slashIdx = id.indexOf("/");
|
|
734
|
+
const short = slashIdx >= 0 ? id.slice(slashIdx + 1) : id;
|
|
735
|
+
return short.toLowerCase();
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
export function generateOpencodeModels(entries: ModelEntry[]): Record<string, unknown> {
|
|
739
|
+
const models: Record<string, unknown> = {};
|
|
740
|
+
for (const entry of entries) {
|
|
741
|
+
const key = toConfigKey(entry.id);
|
|
742
|
+
const costObj: Record<string, number> = { input: entry.cost.input, output: entry.cost.output };
|
|
743
|
+
if (entry.cost.cache_read !== undefined) costObj.cache_read = entry.cost.cache_read;
|
|
744
|
+
if (entry.cost.cache_write !== undefined) costObj.cache_write = entry.cost.cache_write;
|
|
745
|
+
|
|
746
|
+
const model: Record<string, unknown> = {
|
|
747
|
+
id: entry.id,
|
|
748
|
+
name: entry.name,
|
|
749
|
+
reasoning: entry.reasoning,
|
|
750
|
+
tool_call: entry.tool_call,
|
|
751
|
+
attachment: entry.attachment ?? false,
|
|
752
|
+
modalities: entry.modalities ?? { input: ["text"], output: ["text"] },
|
|
753
|
+
cost: costObj,
|
|
754
|
+
limit: entry.limit,
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
if (entry.reasoningEfforts && entry.reasoningEfforts.length > 0) {
|
|
758
|
+
model.reasoningEfforts = entry.reasoningEfforts;
|
|
759
|
+
const variants: Record<string, Record<string, unknown>> = {};
|
|
760
|
+
for (const effort of entry.reasoningEfforts) {
|
|
761
|
+
variants[effort] = { reasoningEffort: effort };
|
|
762
|
+
}
|
|
763
|
+
model.variants = variants;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
models[key] = model;
|
|
767
|
+
}
|
|
768
|
+
return models;
|
|
769
|
+
}
|