@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/costs-models-dev.ts
CHANGED
|
@@ -1,173 +1,173 @@
|
|
|
1
|
-
import type { ModelEntry } from "./catalog.js";
|
|
2
|
-
|
|
3
|
-
export const MODELS_DEV_URL = "https://models.dev/api.json";
|
|
4
|
-
export const FREE_COST = { input: 0, output: 0 } as const;
|
|
5
|
-
export const TEXT_ONLY_MODALITIES = { input: ["text"], output: ["text"] } as const;
|
|
6
|
-
|
|
7
|
-
export type ModelsDevRow = {
|
|
8
|
-
id: string;
|
|
9
|
-
name: string;
|
|
10
|
-
cost: { input: number; output: number; cache_read?: number; cache_write?: number };
|
|
11
|
-
attachment?: boolean;
|
|
12
|
-
modalities?: { input: string[]; output: string[] };
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
type ModelsDevModel = {
|
|
16
|
-
id?: string;
|
|
17
|
-
name?: string;
|
|
18
|
-
cost?: { input?: number; output?: number; cache_read?: number; cache_write?: number };
|
|
19
|
-
attachment?: boolean;
|
|
20
|
-
modalities?: { input?: string[]; output?: string[] };
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
type ModelsDevProvider = { models?: Record<string, ModelsDevModel> };
|
|
24
|
-
|
|
25
|
-
function lastSegment(id: string): string {
|
|
26
|
-
const i = id.lastIndexOf("/");
|
|
27
|
-
return i >= 0 ? id.slice(i + 1) : id;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function isFreeSku(model: { id: string; name: string }): boolean {
|
|
31
|
-
if (/-free$/i.test(model.id)) return true;
|
|
32
|
-
return /\bfree\b/i.test(`${model.id} ${model.name}`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function parseModelsDev(json: string): ModelsDevRow[] {
|
|
36
|
-
const data = JSON.parse(json) as Record<string, ModelsDevProvider>;
|
|
37
|
-
const rows: ModelsDevRow[] = [];
|
|
38
|
-
const seen = new Set<string>();
|
|
39
|
-
for (const provider of Object.keys(data).sort()) {
|
|
40
|
-
const models = data[provider]?.models ?? {};
|
|
41
|
-
for (const model of Object.values(models)) {
|
|
42
|
-
if (!model?.id || model.cost?.input === undefined || model.cost?.output === undefined)
|
|
43
|
-
continue;
|
|
44
|
-
const key = model.id.toLowerCase();
|
|
45
|
-
if (seen.has(key)) continue;
|
|
46
|
-
seen.add(key);
|
|
47
|
-
const cost: ModelsDevRow["cost"] = { input: model.cost.input, output: model.cost.output };
|
|
48
|
-
if (model.cost.cache_read !== undefined) cost.cache_read = model.cost.cache_read;
|
|
49
|
-
if (model.cost.cache_write !== undefined) cost.cache_write = model.cost.cache_write;
|
|
50
|
-
const row: ModelsDevRow = { id: model.id, name: model.name ?? model.id, cost };
|
|
51
|
-
if (typeof model.attachment === "boolean") row.attachment = model.attachment;
|
|
52
|
-
const input = model.modalities?.input?.filter((x) => typeof x === "string");
|
|
53
|
-
const output = model.modalities?.output?.filter((x) => typeof x === "string");
|
|
54
|
-
if (input?.length || output?.length) {
|
|
55
|
-
row.modalities = {
|
|
56
|
-
input: input?.length ? input : [...TEXT_ONLY_MODALITIES.input],
|
|
57
|
-
output: output?.length ? output : [...TEXT_ONLY_MODALITIES.output],
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
rows.push(row);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return rows;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function indexRows(rows: ModelsDevRow[]) {
|
|
67
|
-
const byId = new Map<string, ModelsDevRow>();
|
|
68
|
-
const bySegment = new Map<string, ModelsDevRow>();
|
|
69
|
-
const byName = new Map<string, ModelsDevRow>();
|
|
70
|
-
for (const row of rows) {
|
|
71
|
-
const idKey = row.id.toLowerCase();
|
|
72
|
-
if (!byId.has(idKey)) byId.set(idKey, row);
|
|
73
|
-
const segment = lastSegment(row.id).toLowerCase();
|
|
74
|
-
if (!bySegment.has(segment)) bySegment.set(segment, row);
|
|
75
|
-
const nameKey = row.name.toLowerCase();
|
|
76
|
-
if (!byName.has(nameKey)) byName.set(nameKey, row);
|
|
77
|
-
}
|
|
78
|
-
return { byId, bySegment, byName };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function findRow(model: ModelEntry, index: ReturnType<typeof indexRows>): ModelsDevRow | undefined {
|
|
82
|
-
return (
|
|
83
|
-
index.byId.get(model.id.toLowerCase()) ??
|
|
84
|
-
index.bySegment.get(lastSegment(model.id).toLowerCase()) ??
|
|
85
|
-
index.byName.get(model.name.toLowerCase())
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function applyFreeCosts(
|
|
90
|
-
models: ModelEntry[],
|
|
91
|
-
skipIds: Set<string>,
|
|
92
|
-
filledIds?: Set<string>,
|
|
93
|
-
): number {
|
|
94
|
-
let filled = 0;
|
|
95
|
-
for (const model of models) {
|
|
96
|
-
if (skipIds.has(model.id) || !isFreeSku(model)) continue;
|
|
97
|
-
model.cost = { ...FREE_COST };
|
|
98
|
-
filledIds?.add(model.id);
|
|
99
|
-
filled++;
|
|
100
|
-
}
|
|
101
|
-
return filled;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function textOnly(): { input: string[]; output: string[] } {
|
|
105
|
-
return { input: [...TEXT_ONLY_MODALITIES.input], output: [...TEXT_ONLY_MODALITIES.output] };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function applyModelsDevModalities(models: ModelEntry[], rows: ModelsDevRow[]): number {
|
|
109
|
-
const index = indexRows(rows);
|
|
110
|
-
let filled = 0;
|
|
111
|
-
for (const model of models) {
|
|
112
|
-
const row = findRow(model, index);
|
|
113
|
-
const current = model.modalities;
|
|
114
|
-
if (current && model.attachment !== undefined) {
|
|
115
|
-
const extra = row?.modalities?.input?.filter((x) => !current.input.includes(x)) ?? [];
|
|
116
|
-
if (extra.length > 0) {
|
|
117
|
-
model.modalities = {
|
|
118
|
-
input: [...current.input, ...extra],
|
|
119
|
-
output: [...current.output],
|
|
120
|
-
};
|
|
121
|
-
if (model.modalities.input.includes("image")) model.attachment = true;
|
|
122
|
-
filled++;
|
|
123
|
-
}
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (row && (row.modalities || row.attachment !== undefined)) {
|
|
127
|
-
const modalities = row.modalities
|
|
128
|
-
? { input: [...row.modalities.input], output: [...row.modalities.output] }
|
|
129
|
-
: textOnly();
|
|
130
|
-
model.modalities = modalities;
|
|
131
|
-
model.attachment = row.attachment ?? modalities.input.includes("image");
|
|
132
|
-
filled++;
|
|
133
|
-
} else {
|
|
134
|
-
model.attachment = false;
|
|
135
|
-
model.modalities = textOnly();
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return filled;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function applyModelsDevCosts(
|
|
142
|
-
models: ModelEntry[],
|
|
143
|
-
rows: ModelsDevRow[],
|
|
144
|
-
skipIds: Set<string>,
|
|
145
|
-
filledIds?: Set<string>,
|
|
146
|
-
): number {
|
|
147
|
-
const index = indexRows(rows);
|
|
148
|
-
let filled = 0;
|
|
149
|
-
for (const model of models) {
|
|
150
|
-
if (skipIds.has(model.id) || isFreeSku(model)) continue;
|
|
151
|
-
const row = findRow(model, index);
|
|
152
|
-
if (!row) continue;
|
|
153
|
-
model.cost = { input: row.cost.input, output: row.cost.output };
|
|
154
|
-
if (row.cost.cache_read !== undefined) model.cost.cache_read = row.cost.cache_read;
|
|
155
|
-
if (row.cost.cache_write !== undefined) model.cost.cache_write = row.cost.cache_write;
|
|
156
|
-
filledIds?.add(model.id);
|
|
157
|
-
filled++;
|
|
158
|
-
}
|
|
159
|
-
return filled;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export async function fetchModelsDevJson(): Promise<string> {
|
|
163
|
-
const resp = await fetch(MODELS_DEV_URL, {
|
|
164
|
-
headers: {
|
|
165
|
-
// ponytail: models.dev returns 403 without a browser-like UA; upgrade if they add a real API token
|
|
166
|
-
"User-Agent":
|
|
167
|
-
"Mozilla/5.0 (compatible; opencode-commandcode/0.5; +https://github.com/BrainerVirus/opencode-commandcode)",
|
|
168
|
-
Accept: "application/json",
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
if (!resp.ok) throw new Error(`models.dev returned ${resp.status}`);
|
|
172
|
-
return resp.text();
|
|
173
|
-
}
|
|
1
|
+
import type { ModelEntry } from "./catalog.js";
|
|
2
|
+
|
|
3
|
+
export const MODELS_DEV_URL = "https://models.dev/api.json";
|
|
4
|
+
export const FREE_COST = { input: 0, output: 0 } as const;
|
|
5
|
+
export const TEXT_ONLY_MODALITIES = { input: ["text"], output: ["text"] } as const;
|
|
6
|
+
|
|
7
|
+
export type ModelsDevRow = {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
cost: { input: number; output: number; cache_read?: number; cache_write?: number };
|
|
11
|
+
attachment?: boolean;
|
|
12
|
+
modalities?: { input: string[]; output: string[] };
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type ModelsDevModel = {
|
|
16
|
+
id?: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
cost?: { input?: number; output?: number; cache_read?: number; cache_write?: number };
|
|
19
|
+
attachment?: boolean;
|
|
20
|
+
modalities?: { input?: string[]; output?: string[] };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type ModelsDevProvider = { models?: Record<string, ModelsDevModel> };
|
|
24
|
+
|
|
25
|
+
function lastSegment(id: string): string {
|
|
26
|
+
const i = id.lastIndexOf("/");
|
|
27
|
+
return i >= 0 ? id.slice(i + 1) : id;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isFreeSku(model: { id: string; name: string }): boolean {
|
|
31
|
+
if (/-free$/i.test(model.id)) return true;
|
|
32
|
+
return /\bfree\b/i.test(`${model.id} ${model.name}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function parseModelsDev(json: string): ModelsDevRow[] {
|
|
36
|
+
const data = JSON.parse(json) as Record<string, ModelsDevProvider>;
|
|
37
|
+
const rows: ModelsDevRow[] = [];
|
|
38
|
+
const seen = new Set<string>();
|
|
39
|
+
for (const provider of Object.keys(data).sort()) {
|
|
40
|
+
const models = data[provider]?.models ?? {};
|
|
41
|
+
for (const model of Object.values(models)) {
|
|
42
|
+
if (!model?.id || model.cost?.input === undefined || model.cost?.output === undefined)
|
|
43
|
+
continue;
|
|
44
|
+
const key = model.id.toLowerCase();
|
|
45
|
+
if (seen.has(key)) continue;
|
|
46
|
+
seen.add(key);
|
|
47
|
+
const cost: ModelsDevRow["cost"] = { input: model.cost.input, output: model.cost.output };
|
|
48
|
+
if (model.cost.cache_read !== undefined) cost.cache_read = model.cost.cache_read;
|
|
49
|
+
if (model.cost.cache_write !== undefined) cost.cache_write = model.cost.cache_write;
|
|
50
|
+
const row: ModelsDevRow = { id: model.id, name: model.name ?? model.id, cost };
|
|
51
|
+
if (typeof model.attachment === "boolean") row.attachment = model.attachment;
|
|
52
|
+
const input = model.modalities?.input?.filter((x) => typeof x === "string");
|
|
53
|
+
const output = model.modalities?.output?.filter((x) => typeof x === "string");
|
|
54
|
+
if (input?.length || output?.length) {
|
|
55
|
+
row.modalities = {
|
|
56
|
+
input: input?.length ? input : [...TEXT_ONLY_MODALITIES.input],
|
|
57
|
+
output: output?.length ? output : [...TEXT_ONLY_MODALITIES.output],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
rows.push(row);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return rows;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function indexRows(rows: ModelsDevRow[]) {
|
|
67
|
+
const byId = new Map<string, ModelsDevRow>();
|
|
68
|
+
const bySegment = new Map<string, ModelsDevRow>();
|
|
69
|
+
const byName = new Map<string, ModelsDevRow>();
|
|
70
|
+
for (const row of rows) {
|
|
71
|
+
const idKey = row.id.toLowerCase();
|
|
72
|
+
if (!byId.has(idKey)) byId.set(idKey, row);
|
|
73
|
+
const segment = lastSegment(row.id).toLowerCase();
|
|
74
|
+
if (!bySegment.has(segment)) bySegment.set(segment, row);
|
|
75
|
+
const nameKey = row.name.toLowerCase();
|
|
76
|
+
if (!byName.has(nameKey)) byName.set(nameKey, row);
|
|
77
|
+
}
|
|
78
|
+
return { byId, bySegment, byName };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function findRow(model: ModelEntry, index: ReturnType<typeof indexRows>): ModelsDevRow | undefined {
|
|
82
|
+
return (
|
|
83
|
+
index.byId.get(model.id.toLowerCase()) ??
|
|
84
|
+
index.bySegment.get(lastSegment(model.id).toLowerCase()) ??
|
|
85
|
+
index.byName.get(model.name.toLowerCase())
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function applyFreeCosts(
|
|
90
|
+
models: ModelEntry[],
|
|
91
|
+
skipIds: Set<string>,
|
|
92
|
+
filledIds?: Set<string>,
|
|
93
|
+
): number {
|
|
94
|
+
let filled = 0;
|
|
95
|
+
for (const model of models) {
|
|
96
|
+
if (skipIds.has(model.id) || !isFreeSku(model)) continue;
|
|
97
|
+
model.cost = { ...FREE_COST };
|
|
98
|
+
filledIds?.add(model.id);
|
|
99
|
+
filled++;
|
|
100
|
+
}
|
|
101
|
+
return filled;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function textOnly(): { input: string[]; output: string[] } {
|
|
105
|
+
return { input: [...TEXT_ONLY_MODALITIES.input], output: [...TEXT_ONLY_MODALITIES.output] };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function applyModelsDevModalities(models: ModelEntry[], rows: ModelsDevRow[]): number {
|
|
109
|
+
const index = indexRows(rows);
|
|
110
|
+
let filled = 0;
|
|
111
|
+
for (const model of models) {
|
|
112
|
+
const row = findRow(model, index);
|
|
113
|
+
const current = model.modalities;
|
|
114
|
+
if (current && model.attachment !== undefined) {
|
|
115
|
+
const extra = row?.modalities?.input?.filter((x) => !current.input.includes(x)) ?? [];
|
|
116
|
+
if (extra.length > 0) {
|
|
117
|
+
model.modalities = {
|
|
118
|
+
input: [...current.input, ...extra],
|
|
119
|
+
output: [...current.output],
|
|
120
|
+
};
|
|
121
|
+
if (model.modalities.input.includes("image")) model.attachment = true;
|
|
122
|
+
filled++;
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (row && (row.modalities || row.attachment !== undefined)) {
|
|
127
|
+
const modalities = row.modalities
|
|
128
|
+
? { input: [...row.modalities.input], output: [...row.modalities.output] }
|
|
129
|
+
: textOnly();
|
|
130
|
+
model.modalities = modalities;
|
|
131
|
+
model.attachment = row.attachment ?? modalities.input.includes("image");
|
|
132
|
+
filled++;
|
|
133
|
+
} else {
|
|
134
|
+
model.attachment = false;
|
|
135
|
+
model.modalities = textOnly();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return filled;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function applyModelsDevCosts(
|
|
142
|
+
models: ModelEntry[],
|
|
143
|
+
rows: ModelsDevRow[],
|
|
144
|
+
skipIds: Set<string>,
|
|
145
|
+
filledIds?: Set<string>,
|
|
146
|
+
): number {
|
|
147
|
+
const index = indexRows(rows);
|
|
148
|
+
let filled = 0;
|
|
149
|
+
for (const model of models) {
|
|
150
|
+
if (skipIds.has(model.id) || isFreeSku(model)) continue;
|
|
151
|
+
const row = findRow(model, index);
|
|
152
|
+
if (!row) continue;
|
|
153
|
+
model.cost = { input: row.cost.input, output: row.cost.output };
|
|
154
|
+
if (row.cost.cache_read !== undefined) model.cost.cache_read = row.cost.cache_read;
|
|
155
|
+
if (row.cost.cache_write !== undefined) model.cost.cache_write = row.cost.cache_write;
|
|
156
|
+
filledIds?.add(model.id);
|
|
157
|
+
filled++;
|
|
158
|
+
}
|
|
159
|
+
return filled;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function fetchModelsDevJson(): Promise<string> {
|
|
163
|
+
const resp = await fetch(MODELS_DEV_URL, {
|
|
164
|
+
headers: {
|
|
165
|
+
// ponytail: models.dev returns 403 without a browser-like UA; upgrade if they add a real API token
|
|
166
|
+
"User-Agent":
|
|
167
|
+
"Mozilla/5.0 (compatible; opencode-commandcode/0.5; +https://github.com/BrainerVirus/opencode-commandcode)",
|
|
168
|
+
Accept: "application/json",
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
if (!resp.ok) throw new Error(`models.dev returned ${resp.status}`);
|
|
172
|
+
return resp.text();
|
|
173
|
+
}
|
package/src/manifest.ts
CHANGED
|
@@ -1,134 +1,134 @@
|
|
|
1
|
-
import { writeFileSync } from "fs";
|
|
2
|
-
|
|
3
|
-
export type CatalogStatus = "healthy" | "degraded" | "broken";
|
|
4
|
-
export type CostCatalogBest = "cli" | "docs" | "thirdParty" | "free" | "fallback" | "missing";
|
|
5
|
-
|
|
6
|
-
export type CostSources = {
|
|
7
|
-
cli: number;
|
|
8
|
-
officialDocs: number;
|
|
9
|
-
thirdParty: number;
|
|
10
|
-
free: number;
|
|
11
|
-
fallback: number;
|
|
12
|
-
unmatched: number;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export type CatalogReview = {
|
|
16
|
-
thirdParty: string[];
|
|
17
|
-
free: string[];
|
|
18
|
-
unmatched: string[];
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export type CatalogManifest = {
|
|
22
|
-
schemaVersion: 1;
|
|
23
|
-
generatedAt: string;
|
|
24
|
-
pluginVersion: string;
|
|
25
|
-
commandCodeVersion: string;
|
|
26
|
-
commandCodeTarball: string;
|
|
27
|
-
modelCount: number;
|
|
28
|
-
reasoningModelCount: number;
|
|
29
|
-
extraction: {
|
|
30
|
-
modelCatalog: "ok" | "failed";
|
|
31
|
-
costCatalog: CostCatalogBest;
|
|
32
|
-
costCatalogError: string | null;
|
|
33
|
-
};
|
|
34
|
-
costSources: CostSources;
|
|
35
|
-
review?: CatalogReview;
|
|
36
|
-
status: CatalogStatus;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export type BuildManifestInput = {
|
|
40
|
-
pluginVersion: string;
|
|
41
|
-
commandCodeVersion: string;
|
|
42
|
-
commandCodeTarball: string;
|
|
43
|
-
modelCount: number;
|
|
44
|
-
reasoningModelCount: number;
|
|
45
|
-
modelCatalogOk: boolean;
|
|
46
|
-
costSources: CostSources;
|
|
47
|
-
review?: CatalogReview;
|
|
48
|
-
generatedAt: string;
|
|
49
|
-
costCatalogError?: string | null;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export function meetsModelCountFloor(modelCount: number, lastSuccessful: number | null): boolean {
|
|
53
|
-
const floor = Math.max(20, Math.floor((lastSuccessful ?? 20) * 0.5));
|
|
54
|
-
// ponytail: when there is no prior catalog, lastSuccessful is null and the floor is 20
|
|
55
|
-
return modelCount >= (lastSuccessful === null ? 20 : floor);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function bestCostCatalog(sources: CostSources): CostCatalogBest {
|
|
59
|
-
if (sources.cli > 0) return "cli";
|
|
60
|
-
if (sources.officialDocs > 0) return "docs";
|
|
61
|
-
if (sources.thirdParty > 0) return "thirdParty";
|
|
62
|
-
if (sources.free > 0) return "free";
|
|
63
|
-
if (sources.fallback > 0) return "fallback";
|
|
64
|
-
return "missing";
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function catalogStatus(modelCatalogOk: boolean, sources: CostSources): CatalogStatus {
|
|
68
|
-
if (!modelCatalogOk) return "broken";
|
|
69
|
-
if (sources.unmatched > 0) return "degraded";
|
|
70
|
-
return "healthy";
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function commandCodeTarballUrl(version: string): string {
|
|
74
|
-
return `https://registry.npmjs.org/command-code/-/command-code-${version}.tgz`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function buildManifest(input: BuildManifestInput): CatalogManifest {
|
|
78
|
-
const status = catalogStatus(input.modelCatalogOk, input.costSources);
|
|
79
|
-
return {
|
|
80
|
-
schemaVersion: 1,
|
|
81
|
-
generatedAt: input.generatedAt,
|
|
82
|
-
pluginVersion: input.pluginVersion,
|
|
83
|
-
commandCodeVersion: input.commandCodeVersion,
|
|
84
|
-
commandCodeTarball: input.commandCodeTarball,
|
|
85
|
-
modelCount: input.modelCount,
|
|
86
|
-
reasoningModelCount: input.reasoningModelCount,
|
|
87
|
-
extraction: {
|
|
88
|
-
modelCatalog: input.modelCatalogOk ? "ok" : "failed",
|
|
89
|
-
costCatalog: bestCostCatalog(input.costSources),
|
|
90
|
-
costCatalogError: input.costCatalogError ?? null,
|
|
91
|
-
},
|
|
92
|
-
costSources: { ...input.costSources },
|
|
93
|
-
...(input.review ? { review: input.review } : {}),
|
|
94
|
-
status,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function writeManifest(path: string, manifest: CatalogManifest): void {
|
|
99
|
-
writeFileSync(path, `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function lastSuccessfulModelCount(manifest: CatalogManifest | null): number | null {
|
|
103
|
-
if (!manifest) return null;
|
|
104
|
-
if (manifest.status === "broken") return null;
|
|
105
|
-
return manifest.modelCount;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function countCostSources(input: {
|
|
109
|
-
modelIds: string[];
|
|
110
|
-
cliIds: Set<string>;
|
|
111
|
-
officialDocIds: Set<string>;
|
|
112
|
-
thirdPartyIds: Set<string>;
|
|
113
|
-
freeIds: Set<string>;
|
|
114
|
-
fallbackIds?: Set<string>;
|
|
115
|
-
}): CostSources {
|
|
116
|
-
const fallbackIds = input.fallbackIds ?? new Set<string>();
|
|
117
|
-
const sources: CostSources = {
|
|
118
|
-
cli: 0,
|
|
119
|
-
officialDocs: 0,
|
|
120
|
-
thirdParty: 0,
|
|
121
|
-
free: 0,
|
|
122
|
-
fallback: 0,
|
|
123
|
-
unmatched: 0,
|
|
124
|
-
};
|
|
125
|
-
for (const id of input.modelIds) {
|
|
126
|
-
if (input.cliIds.has(id)) sources.cli++;
|
|
127
|
-
else if (input.officialDocIds.has(id)) sources.officialDocs++;
|
|
128
|
-
else if (input.thirdPartyIds.has(id)) sources.thirdParty++;
|
|
129
|
-
else if (input.freeIds.has(id)) sources.free++;
|
|
130
|
-
else if (fallbackIds.has(id)) sources.fallback++;
|
|
131
|
-
else sources.unmatched++;
|
|
132
|
-
}
|
|
133
|
-
return sources;
|
|
134
|
-
}
|
|
1
|
+
import { writeFileSync } from "fs";
|
|
2
|
+
|
|
3
|
+
export type CatalogStatus = "healthy" | "degraded" | "broken";
|
|
4
|
+
export type CostCatalogBest = "cli" | "docs" | "thirdParty" | "free" | "fallback" | "missing";
|
|
5
|
+
|
|
6
|
+
export type CostSources = {
|
|
7
|
+
cli: number;
|
|
8
|
+
officialDocs: number;
|
|
9
|
+
thirdParty: number;
|
|
10
|
+
free: number;
|
|
11
|
+
fallback: number;
|
|
12
|
+
unmatched: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type CatalogReview = {
|
|
16
|
+
thirdParty: string[];
|
|
17
|
+
free: string[];
|
|
18
|
+
unmatched: string[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type CatalogManifest = {
|
|
22
|
+
schemaVersion: 1;
|
|
23
|
+
generatedAt: string;
|
|
24
|
+
pluginVersion: string;
|
|
25
|
+
commandCodeVersion: string;
|
|
26
|
+
commandCodeTarball: string;
|
|
27
|
+
modelCount: number;
|
|
28
|
+
reasoningModelCount: number;
|
|
29
|
+
extraction: {
|
|
30
|
+
modelCatalog: "ok" | "failed";
|
|
31
|
+
costCatalog: CostCatalogBest;
|
|
32
|
+
costCatalogError: string | null;
|
|
33
|
+
};
|
|
34
|
+
costSources: CostSources;
|
|
35
|
+
review?: CatalogReview;
|
|
36
|
+
status: CatalogStatus;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type BuildManifestInput = {
|
|
40
|
+
pluginVersion: string;
|
|
41
|
+
commandCodeVersion: string;
|
|
42
|
+
commandCodeTarball: string;
|
|
43
|
+
modelCount: number;
|
|
44
|
+
reasoningModelCount: number;
|
|
45
|
+
modelCatalogOk: boolean;
|
|
46
|
+
costSources: CostSources;
|
|
47
|
+
review?: CatalogReview;
|
|
48
|
+
generatedAt: string;
|
|
49
|
+
costCatalogError?: string | null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export function meetsModelCountFloor(modelCount: number, lastSuccessful: number | null): boolean {
|
|
53
|
+
const floor = Math.max(20, Math.floor((lastSuccessful ?? 20) * 0.5));
|
|
54
|
+
// ponytail: when there is no prior catalog, lastSuccessful is null and the floor is 20
|
|
55
|
+
return modelCount >= (lastSuccessful === null ? 20 : floor);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function bestCostCatalog(sources: CostSources): CostCatalogBest {
|
|
59
|
+
if (sources.cli > 0) return "cli";
|
|
60
|
+
if (sources.officialDocs > 0) return "docs";
|
|
61
|
+
if (sources.thirdParty > 0) return "thirdParty";
|
|
62
|
+
if (sources.free > 0) return "free";
|
|
63
|
+
if (sources.fallback > 0) return "fallback";
|
|
64
|
+
return "missing";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function catalogStatus(modelCatalogOk: boolean, sources: CostSources): CatalogStatus {
|
|
68
|
+
if (!modelCatalogOk) return "broken";
|
|
69
|
+
if (sources.unmatched > 0) return "degraded";
|
|
70
|
+
return "healthy";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function commandCodeTarballUrl(version: string): string {
|
|
74
|
+
return `https://registry.npmjs.org/command-code/-/command-code-${version}.tgz`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function buildManifest(input: BuildManifestInput): CatalogManifest {
|
|
78
|
+
const status = catalogStatus(input.modelCatalogOk, input.costSources);
|
|
79
|
+
return {
|
|
80
|
+
schemaVersion: 1,
|
|
81
|
+
generatedAt: input.generatedAt,
|
|
82
|
+
pluginVersion: input.pluginVersion,
|
|
83
|
+
commandCodeVersion: input.commandCodeVersion,
|
|
84
|
+
commandCodeTarball: input.commandCodeTarball,
|
|
85
|
+
modelCount: input.modelCount,
|
|
86
|
+
reasoningModelCount: input.reasoningModelCount,
|
|
87
|
+
extraction: {
|
|
88
|
+
modelCatalog: input.modelCatalogOk ? "ok" : "failed",
|
|
89
|
+
costCatalog: bestCostCatalog(input.costSources),
|
|
90
|
+
costCatalogError: input.costCatalogError ?? null,
|
|
91
|
+
},
|
|
92
|
+
costSources: { ...input.costSources },
|
|
93
|
+
...(input.review ? { review: input.review } : {}),
|
|
94
|
+
status,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function writeManifest(path: string, manifest: CatalogManifest): void {
|
|
99
|
+
writeFileSync(path, `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function lastSuccessfulModelCount(manifest: CatalogManifest | null): number | null {
|
|
103
|
+
if (!manifest) return null;
|
|
104
|
+
if (manifest.status === "broken") return null;
|
|
105
|
+
return manifest.modelCount;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function countCostSources(input: {
|
|
109
|
+
modelIds: string[];
|
|
110
|
+
cliIds: Set<string>;
|
|
111
|
+
officialDocIds: Set<string>;
|
|
112
|
+
thirdPartyIds: Set<string>;
|
|
113
|
+
freeIds: Set<string>;
|
|
114
|
+
fallbackIds?: Set<string>;
|
|
115
|
+
}): CostSources {
|
|
116
|
+
const fallbackIds = input.fallbackIds ?? new Set<string>();
|
|
117
|
+
const sources: CostSources = {
|
|
118
|
+
cli: 0,
|
|
119
|
+
officialDocs: 0,
|
|
120
|
+
thirdParty: 0,
|
|
121
|
+
free: 0,
|
|
122
|
+
fallback: 0,
|
|
123
|
+
unmatched: 0,
|
|
124
|
+
};
|
|
125
|
+
for (const id of input.modelIds) {
|
|
126
|
+
if (input.cliIds.has(id)) sources.cli++;
|
|
127
|
+
else if (input.officialDocIds.has(id)) sources.officialDocs++;
|
|
128
|
+
else if (input.thirdPartyIds.has(id)) sources.thirdParty++;
|
|
129
|
+
else if (input.freeIds.has(id)) sources.free++;
|
|
130
|
+
else if (fallbackIds.has(id)) sources.fallback++;
|
|
131
|
+
else sources.unmatched++;
|
|
132
|
+
}
|
|
133
|
+
return sources;
|
|
134
|
+
}
|