@telsoncc/opencode-commandcode 1.1.0 → 1.2.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/CHANGELOG.md +2 -0
- package/README.md +19 -0
- package/_version.txt +1 -1
- package/manifest.json +4 -4
- package/models.json +1 -1
- package/package.json +1 -1
- package/plugin-opencode2.ts +4 -4
- package/src/catalog.ts +2 -0
- package/src/costs-models-dev.ts +185 -1
- package/src/opencode2.ts +46 -4
- package/src/startup.ts +2 -0
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -24,6 +24,9 @@ Forked and currently maintained by **[telsoncc](https://github.com/telsoncc)** (
|
|
|
24
24
|
- Vision vs text-only comes from the Command Code CLI catalog (`inputModalities` on every SKU). [models.dev](https://models.dev) only adds extra inputs (video/audio/pdf) when it matches.
|
|
25
25
|
- Reasoning effort **variants** on models that declare `reasoningEfforts`.
|
|
26
26
|
- Quiet OpenCode startup (diagnostics go to `startup.json`, not stdout).
|
|
27
|
+
- Live catalog refresh on the 2.0 entrypoint: retired models are pruned and
|
|
28
|
+
new ones are published with [models.dev](https://models.dev) estimates
|
|
29
|
+
(marked `(est.)`) until the next catalog sync replaces them.
|
|
27
30
|
- Native **OpenCode 2.x** entrypoint (`plugin/opencode2`, also served as `server`):
|
|
28
31
|
in-memory provider inventory via `ctx.provider.transform` (no config writes)
|
|
29
32
|
plus a `commandcode` integration for `/connect`. See `docs/opencode-2.md`.
|
|
@@ -100,6 +103,22 @@ Run `/connect` in opencode, search for **Command Code**, and enter your API key,
|
|
|
100
103
|
On 2.x the catalog carries no release dates, so filter the picker by provider
|
|
101
104
|
**Command Code** if the global list looks empty.
|
|
102
105
|
|
|
106
|
+
## Live catalog refresh (2.x)
|
|
107
|
+
|
|
108
|
+
The 2.0 entrypoint re-checks the Provider API (`/models`, no auth) on setup
|
|
109
|
+
and every 6h on a process-shared timer:
|
|
110
|
+
|
|
111
|
+
- ids the API retired are pruned from the inventory;
|
|
112
|
+
- ids missing locally are published immediately with
|
|
113
|
+
[models.dev](https://models.dev) cost/capability estimates, named
|
|
114
|
+
`"<model> (est.)"`, and replaced by exact data on the next catalog sync;
|
|
115
|
+
- ids with no estimable data stay reported (not published) until the sync.
|
|
116
|
+
|
|
117
|
+
Estimates are list prices, close to but not equal to Command Code billing.
|
|
118
|
+
`startup.json` records `catalogSource: "remote"`, `pendingNewCount`, and
|
|
119
|
+
`estimatedCount`. Opt out with `disableModelSync: true` in
|
|
120
|
+
`~/.config/opencode/opencode-commandcode.json`.
|
|
121
|
+
|
|
103
122
|
## Optional local CLI override
|
|
104
123
|
|
|
105
124
|
Maintainers only. OpenCode will scrape a local `command-code` install when `COMMANDCODE_PACKAGE_PATH` or `commandCodePackagePath` in `~/.config/opencode/opencode-commandcode.json` is set.
|
package/_version.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.65.
|
|
1
|
+
1.65.3
|
package/manifest.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-09-
|
|
4
|
-
"pluginVersion": "1.1
|
|
5
|
-
"commandCodeVersion": "1.65.
|
|
6
|
-
"commandCodeTarball": "https://registry.npmjs.org/command-code/-/command-code-1.65.
|
|
3
|
+
"generatedAt": "2026-09-25T21:07:36.120Z",
|
|
4
|
+
"pluginVersion": "1.2.1",
|
|
5
|
+
"commandCodeVersion": "1.65.3",
|
|
6
|
+
"commandCodeTarball": "https://registry.npmjs.org/command-code/-/command-code-1.65.3.tgz",
|
|
7
7
|
"modelCount": 81,
|
|
8
8
|
"reasoningModelCount": 68,
|
|
9
9
|
"extraction": {
|
package/models.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telsoncc/opencode-commandcode",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Command Code API provider for opencode — use Claude, GPT, Gemini, DeepSeek, Qwen, Kimi, GLM, MiniMax, and Step models via Command Code",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
package/plugin-opencode2.ts
CHANGED
|
@@ -71,10 +71,10 @@ export async function setup(ctx: V2SetupContext): Promise<() => Promise<void>> {
|
|
|
71
71
|
// Keep the last-good inventory when the reload fails.
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
// Live availability refresh: prune ids the API retired and
|
|
75
|
-
// ones
|
|
76
|
-
// the offline fallback;
|
|
77
|
-
//
|
|
74
|
+
// Live availability refresh: prune ids the API retired and publish new
|
|
75
|
+
// ones with models.dev cost estimates until a catalog sync replaces them.
|
|
76
|
+
// The bundled catalog stays as the offline fallback; ids with no
|
|
77
|
+
// estimable data stay reported as pending.
|
|
78
78
|
const sameInventory = (next: ModelEntry[]): boolean =>
|
|
79
79
|
next.length === inventory.length && next.every((m, i) => m.id === inventory[i]?.id);
|
|
80
80
|
|
package/src/catalog.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface ModelEntry {
|
|
|
17
17
|
limit: { context: number; output: number };
|
|
18
18
|
attachment?: boolean;
|
|
19
19
|
modalities?: { input: string[]; output: string[] };
|
|
20
|
+
/** Runtime estimate from models.dev — replaced on the next catalog sync. */
|
|
21
|
+
estimated?: boolean;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export interface CostEntry {
|
package/src/costs-models-dev.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { pluginStateDir } from "./startup.js";
|
|
1
4
|
import type { ModelEntry } from "./catalog.js";
|
|
2
5
|
|
|
3
6
|
export const MODELS_DEV_URL = "https://models.dev/api.json";
|
|
7
|
+
export const MODELS_DEV_TIMEOUT_MS = 30_000;
|
|
8
|
+
export const MODELS_DEV_CACHE_TTL_MS = 24 * 3600_000;
|
|
9
|
+
export const MODELS_DEV_CACHE_FILE = "models-dev-cache.json";
|
|
10
|
+
export const ESTIMATED_CONTEXT_TOKENS = 200000;
|
|
11
|
+
export const ESTIMATED_OUTPUT_TOKENS = 65536;
|
|
4
12
|
export const FREE_COST = { input: 0, output: 0 } as const;
|
|
5
13
|
export const TEXT_ONLY_MODALITIES = { input: ["text"], output: ["text"] } as const;
|
|
6
14
|
|
|
@@ -10,6 +18,15 @@ export type ModelsDevRow = {
|
|
|
10
18
|
cost: { input: number; output: number; cache_read?: number; cache_write?: number };
|
|
11
19
|
attachment?: boolean;
|
|
12
20
|
modalities?: { input: string[]; output: string[] };
|
|
21
|
+
reasoning?: boolean;
|
|
22
|
+
reasoningEfforts?: string[];
|
|
23
|
+
toolCall?: boolean;
|
|
24
|
+
limit?: { context: number; output: number };
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type ReasoningOption = {
|
|
28
|
+
type?: string;
|
|
29
|
+
values?: unknown;
|
|
13
30
|
};
|
|
14
31
|
|
|
15
32
|
type ModelsDevModel = {
|
|
@@ -18,6 +35,10 @@ type ModelsDevModel = {
|
|
|
18
35
|
cost?: { input?: number; output?: number; cache_read?: number; cache_write?: number };
|
|
19
36
|
attachment?: boolean;
|
|
20
37
|
modalities?: { input?: string[]; output?: string[] };
|
|
38
|
+
reasoning?: boolean;
|
|
39
|
+
reasoning_options?: ReasoningOption[];
|
|
40
|
+
tool_call?: boolean;
|
|
41
|
+
limit?: { context?: number; output?: number };
|
|
21
42
|
};
|
|
22
43
|
|
|
23
44
|
type ModelsDevProvider = { models?: Record<string, ModelsDevModel> };
|
|
@@ -49,6 +70,12 @@ export function parseModelsDev(json: string): ModelsDevRow[] {
|
|
|
49
70
|
if (model.cost.cache_write !== undefined) cost.cache_write = model.cost.cache_write;
|
|
50
71
|
const row: ModelsDevRow = { id: model.id, name: model.name ?? model.id, cost };
|
|
51
72
|
if (typeof model.attachment === "boolean") row.attachment = model.attachment;
|
|
73
|
+
if (typeof model.reasoning === "boolean") row.reasoning = model.reasoning;
|
|
74
|
+
const efforts = extractEffortValues(model.reasoning_options);
|
|
75
|
+
if (efforts) row.reasoningEfforts = efforts;
|
|
76
|
+
if (typeof model.tool_call === "boolean") row.toolCall = model.tool_call;
|
|
77
|
+
const limit = extractLimit(model.limit);
|
|
78
|
+
if (limit) row.limit = limit;
|
|
52
79
|
const input = model.modalities?.input?.filter((x) => typeof x === "string");
|
|
53
80
|
const output = model.modalities?.output?.filter((x) => typeof x === "string");
|
|
54
81
|
if (input?.length || output?.length) {
|
|
@@ -105,6 +132,162 @@ function textOnly(): { input: string[]; output: string[] } {
|
|
|
105
132
|
return { input: [...TEXT_ONLY_MODALITIES.input], output: [...TEXT_ONLY_MODALITIES.output] };
|
|
106
133
|
}
|
|
107
134
|
|
|
135
|
+
function extractEffortValues(options: ReasoningOption[] | undefined): string[] | undefined {
|
|
136
|
+
if (!Array.isArray(options)) return undefined;
|
|
137
|
+
const values = options.flatMap((o) =>
|
|
138
|
+
o?.type === "effort" && Array.isArray(o.values) ? o.values : [],
|
|
139
|
+
);
|
|
140
|
+
const unique = [
|
|
141
|
+
...new Set(values.filter((v): v is string => typeof v === "string" && v.length > 0)),
|
|
142
|
+
];
|
|
143
|
+
return unique.length > 0 ? unique : undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function extractLimit(
|
|
147
|
+
limit: { context?: number; output?: number } | undefined,
|
|
148
|
+
): { context: number; output: number } | undefined {
|
|
149
|
+
if (!limit || typeof limit.context !== "number" || typeof limit.output !== "number") {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
if (!(limit.context > 0) || !(limit.output > 0)) return undefined;
|
|
153
|
+
return { context: limit.context, output: limit.output };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function prettifyModelId(id: string): string {
|
|
157
|
+
return lastSegment(id)
|
|
158
|
+
.replace(/[-_]+/g, " ")
|
|
159
|
+
.replace(/\s+/g, " ")
|
|
160
|
+
.trim()
|
|
161
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Match a models.dev row by exact id, last path segment, or display name. */
|
|
165
|
+
export function findModelsDevRow(
|
|
166
|
+
rows: ModelsDevRow[],
|
|
167
|
+
id: string,
|
|
168
|
+
name?: string,
|
|
169
|
+
): ModelsDevRow | undefined {
|
|
170
|
+
const index = indexRows(rows);
|
|
171
|
+
return (
|
|
172
|
+
index.byId.get(id.toLowerCase()) ??
|
|
173
|
+
index.bySegment.get(lastSegment(id).toLowerCase()) ??
|
|
174
|
+
(name ? index.byName.get(name.toLowerCase()) : undefined)
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Build a provisional catalog entry for a remote id missing locally.
|
|
180
|
+
* Costs/capabilities come from models.dev (list prices, close to but not
|
|
181
|
+
* equal to Command Code billing); the entry is replaced on the next catalog
|
|
182
|
+
* sync. Returns null when nothing can ground an estimate.
|
|
183
|
+
*/
|
|
184
|
+
export function buildEstimatedEntry(id: string, rows: ModelsDevRow[]): ModelEntry | null {
|
|
185
|
+
const displayName = prettifyModelId(id);
|
|
186
|
+
if (isFreeSku({ id, name: displayName })) {
|
|
187
|
+
return {
|
|
188
|
+
id,
|
|
189
|
+
name: `${displayName} (est.)`,
|
|
190
|
+
tier: "open-source",
|
|
191
|
+
reasoning: false,
|
|
192
|
+
tool_call: true,
|
|
193
|
+
cost: { ...FREE_COST },
|
|
194
|
+
limit: { context: ESTIMATED_CONTEXT_TOKENS, output: ESTIMATED_OUTPUT_TOKENS },
|
|
195
|
+
attachment: false,
|
|
196
|
+
modalities: textOnly(),
|
|
197
|
+
estimated: true,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const row = findModelsDevRow(rows, id);
|
|
201
|
+
if (!row) return null;
|
|
202
|
+
const cost: ModelEntry["cost"] = { input: row.cost.input, output: row.cost.output };
|
|
203
|
+
if (row.cost.cache_read !== undefined) cost.cache_read = row.cost.cache_read;
|
|
204
|
+
if (row.cost.cache_write !== undefined) cost.cache_write = row.cost.cache_write;
|
|
205
|
+
const modalities = row.modalities
|
|
206
|
+
? { input: [...row.modalities.input], output: [...row.modalities.output] }
|
|
207
|
+
: textOnly();
|
|
208
|
+
return {
|
|
209
|
+
id,
|
|
210
|
+
name: `${row.name} (est.)`,
|
|
211
|
+
tier: "open-source",
|
|
212
|
+
reasoning: row.reasoning ?? (row.reasoningEfforts?.length ?? 0) > 0,
|
|
213
|
+
...(row.reasoningEfforts ? { reasoningEfforts: [...row.reasoningEfforts] } : {}),
|
|
214
|
+
tool_call: row.toolCall ?? true,
|
|
215
|
+
cost,
|
|
216
|
+
limit: row.limit ?? {
|
|
217
|
+
context: ESTIMATED_CONTEXT_TOKENS,
|
|
218
|
+
output: ESTIMATED_OUTPUT_TOKENS,
|
|
219
|
+
},
|
|
220
|
+
attachment: row.attachment ?? modalities.input.includes("image"),
|
|
221
|
+
modalities,
|
|
222
|
+
estimated: true,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface ModelsDevCache {
|
|
227
|
+
fetchedAt: number;
|
|
228
|
+
json: string;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function readModelsDevCache(cacheDir: string): ModelsDevCache | null {
|
|
232
|
+
const path = join(cacheDir, MODELS_DEV_CACHE_FILE);
|
|
233
|
+
if (!existsSync(path)) return null;
|
|
234
|
+
try {
|
|
235
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8")) as Partial<ModelsDevCache>;
|
|
236
|
+
if (typeof parsed.json !== "string" || typeof parsed.fetchedAt !== "number") return null;
|
|
237
|
+
return { fetchedAt: parsed.fetchedAt, json: parsed.json };
|
|
238
|
+
} catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function writeModelsDevCache(cacheDir: string, json: string): void {
|
|
244
|
+
try {
|
|
245
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
246
|
+
writeFileSync(
|
|
247
|
+
join(cacheDir, MODELS_DEV_CACHE_FILE),
|
|
248
|
+
JSON.stringify({ fetchedAt: Date.now(), json }),
|
|
249
|
+
"utf-8",
|
|
250
|
+
);
|
|
251
|
+
} catch {
|
|
252
|
+
// ignore cache write
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Load models.dev rows, cached on disk for a day. Falls back to stale cache
|
|
258
|
+
* when the network fails; returns [] only when nothing is available.
|
|
259
|
+
*/
|
|
260
|
+
export async function loadModelsDevRows(
|
|
261
|
+
options: { timeoutMs?: number; maxAgeMs?: number; cacheDir?: string } = {},
|
|
262
|
+
): Promise<ModelsDevRow[]> {
|
|
263
|
+
const cacheDir = options.cacheDir ?? pluginStateDir();
|
|
264
|
+
const maxAgeMs = options.maxAgeMs ?? MODELS_DEV_CACHE_TTL_MS;
|
|
265
|
+
const cached = readModelsDevCache(cacheDir);
|
|
266
|
+
if (cached && Date.now() - cached.fetchedAt < maxAgeMs) {
|
|
267
|
+
try {
|
|
268
|
+
return parseModelsDev(cached.json);
|
|
269
|
+
} catch {
|
|
270
|
+
// fall through to refetch
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
const json = await fetchModelsDevJson(
|
|
275
|
+
AbortSignal.timeout(options.timeoutMs ?? MODELS_DEV_TIMEOUT_MS),
|
|
276
|
+
);
|
|
277
|
+
writeModelsDevCache(cacheDir, json);
|
|
278
|
+
return parseModelsDev(json);
|
|
279
|
+
} catch {
|
|
280
|
+
if (cached) {
|
|
281
|
+
try {
|
|
282
|
+
return parseModelsDev(cached.json);
|
|
283
|
+
} catch {
|
|
284
|
+
// fall through to []
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
108
291
|
export function applyModelsDevModalities(models: ModelEntry[], rows: ModelsDevRow[]): number {
|
|
109
292
|
const index = indexRows(rows);
|
|
110
293
|
let filled = 0;
|
|
@@ -159,7 +342,7 @@ export function applyModelsDevCosts(
|
|
|
159
342
|
return filled;
|
|
160
343
|
}
|
|
161
344
|
|
|
162
|
-
export async function fetchModelsDevJson(): Promise<string> {
|
|
345
|
+
export async function fetchModelsDevJson(signal?: AbortSignal): Promise<string> {
|
|
163
346
|
const resp = await fetch(MODELS_DEV_URL, {
|
|
164
347
|
headers: {
|
|
165
348
|
// ponytail: models.dev returns 403 without a browser-like UA; upgrade if they add a real API token
|
|
@@ -167,6 +350,7 @@ export async function fetchModelsDevJson(): Promise<string> {
|
|
|
167
350
|
"Mozilla/5.0 (compatible; opencode-commandcode/0.5; +https://github.com/telsoncc/opencode-commandcode)",
|
|
168
351
|
Accept: "application/json",
|
|
169
352
|
},
|
|
353
|
+
...(signal ? { signal } : {}),
|
|
170
354
|
});
|
|
171
355
|
if (!resp.ok) throw new Error(`models.dev returned ${resp.status}`);
|
|
172
356
|
return resp.text();
|
package/src/opencode2.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
loadCatalogFromLocalCommandCode,
|
|
11
11
|
type ModelEntry,
|
|
12
12
|
} from "./catalog.js";
|
|
13
|
+
import { buildEstimatedEntry, loadModelsDevRows, type ModelsDevRow } from "./costs-models-dev.js";
|
|
13
14
|
import {
|
|
14
15
|
readCatalogCache,
|
|
15
16
|
writeCatalogCache,
|
|
@@ -350,6 +351,8 @@ export interface RemoteRefreshResult {
|
|
|
350
351
|
unavailable: string[];
|
|
351
352
|
pendingNew: string[];
|
|
352
353
|
remoteCount: number;
|
|
354
|
+
/** Provisional entries published with models.dev estimates. */
|
|
355
|
+
estimated: ModelEntry[];
|
|
353
356
|
}
|
|
354
357
|
|
|
355
358
|
/** Live availability ids from the Provider API. No auth required. */
|
|
@@ -369,8 +372,8 @@ export async function fetchRemoteAvailability(
|
|
|
369
372
|
/**
|
|
370
373
|
* Reconcile a local catalog with live availability: keep the ids the API
|
|
371
374
|
* still serves, report retired ones, and list remote ids missing locally.
|
|
372
|
-
* Missing ids
|
|
373
|
-
*
|
|
375
|
+
* Missing ids stay pending here — refreshCatalogFromRemote tries to publish
|
|
376
|
+
* them with models.dev estimates first.
|
|
374
377
|
*/
|
|
375
378
|
export function applyRemoteAvailability(
|
|
376
379
|
base: ModelEntry[],
|
|
@@ -379,14 +382,52 @@ export function applyRemoteAvailability(
|
|
|
379
382
|
const { retained, unavailable } = filterCatalogByAvailability(base, remoteIds);
|
|
380
383
|
const known = new Set(base.map((m) => m.id));
|
|
381
384
|
const pendingNew = [...new Set(remoteIds)].filter((id) => !known.has(id)).sort();
|
|
382
|
-
return {
|
|
385
|
+
return {
|
|
386
|
+
models: retained,
|
|
387
|
+
unavailable,
|
|
388
|
+
pendingNew,
|
|
389
|
+
remoteCount: remoteIds.length,
|
|
390
|
+
estimated: [],
|
|
391
|
+
};
|
|
383
392
|
}
|
|
384
393
|
|
|
385
394
|
export async function refreshCatalogFromRemote(
|
|
386
395
|
base: ModelEntry[],
|
|
387
396
|
timeoutMs = REMOTE_SYNC_TIMEOUT_MS,
|
|
388
397
|
): Promise<RemoteRefreshResult> {
|
|
389
|
-
|
|
398
|
+
const result = applyRemoteAvailability(base, await fetchRemoteAvailability(timeoutMs));
|
|
399
|
+
if (result.pendingNew.length === 0) return result;
|
|
400
|
+
let rows: ModelsDevRow[] = [];
|
|
401
|
+
try {
|
|
402
|
+
rows = await loadModelsDevRows();
|
|
403
|
+
} catch {
|
|
404
|
+
return result;
|
|
405
|
+
}
|
|
406
|
+
if (rows.length === 0) return result;
|
|
407
|
+
const usedKeys = new Set(base.map((m) => toConfigKey(m.id)));
|
|
408
|
+
const estimated: ModelEntry[] = [];
|
|
409
|
+
const stillPending: string[] = [];
|
|
410
|
+
for (const id of result.pendingNew) {
|
|
411
|
+
const key = toConfigKey(id);
|
|
412
|
+
if (usedKeys.has(key)) {
|
|
413
|
+
stillPending.push(id);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const entry = buildEstimatedEntry(id, rows);
|
|
417
|
+
if (!entry) {
|
|
418
|
+
stillPending.push(id);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
usedKeys.add(key);
|
|
422
|
+
estimated.push(entry);
|
|
423
|
+
}
|
|
424
|
+
return {
|
|
425
|
+
models: [...result.models, ...estimated],
|
|
426
|
+
unavailable: result.unavailable,
|
|
427
|
+
pendingNew: stillPending,
|
|
428
|
+
remoteCount: result.remoteCount,
|
|
429
|
+
estimated,
|
|
430
|
+
};
|
|
390
431
|
}
|
|
391
432
|
|
|
392
433
|
export function isRemoteSyncDisabled(): boolean {
|
|
@@ -416,6 +457,7 @@ export function writeRemoteRefreshSummary(
|
|
|
416
457
|
degradedReason:
|
|
417
458
|
result.models.length === 0 ? "remote availability matched no local models" : null,
|
|
418
459
|
pendingNewCount: result.pendingNew.length,
|
|
460
|
+
estimatedCount: result.estimated.length,
|
|
419
461
|
};
|
|
420
462
|
try {
|
|
421
463
|
writeStartupSummary(pluginStateDir(), summary);
|
package/src/startup.ts
CHANGED
|
@@ -14,6 +14,8 @@ export type StartupSummary = {
|
|
|
14
14
|
degradedReason: string | null;
|
|
15
15
|
/** Remote ids with no local cost data yet — next catalog sync picks them up. */
|
|
16
16
|
pendingNewCount?: number;
|
|
17
|
+
/** Provisional entries published with models.dev estimates. */
|
|
18
|
+
estimatedCount?: number;
|
|
17
19
|
};
|
|
18
20
|
|
|
19
21
|
export function pluginStateDir(): string {
|