@darthph0enix/commandcode-go-opencode-provider 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugin.ts +12 -1
- package/src/model-sync.ts +128 -28
package/package.json
CHANGED
package/plugin.ts
CHANGED
|
@@ -21,7 +21,18 @@ export default async function commandcodePlugin() {
|
|
|
21
21
|
if (!cc.env) cc.env = ["COMMANDCODE_API_KEY"]
|
|
22
22
|
|
|
23
23
|
if (!cc.models) {
|
|
24
|
-
const
|
|
24
|
+
const options = (cc.options as Record<string, unknown> | undefined) ?? {}
|
|
25
|
+
const baseURL = typeof options.baseURL === "string" ? options.baseURL.replace(/\/+$/, "") : undefined
|
|
26
|
+
const catalogUrl =
|
|
27
|
+
typeof options.catalogUrl === "string"
|
|
28
|
+
? options.catalogUrl
|
|
29
|
+
: baseURL
|
|
30
|
+
? `${baseURL}/catalog.json`
|
|
31
|
+
: undefined
|
|
32
|
+
const { models, source } = await loadSyncedModels({ catalogUrl })
|
|
33
|
+
if (process.env.COMMANDCODE_DEBUG) {
|
|
34
|
+
console.warn(`[commandcode] injected ${models.length} models from ${source}`)
|
|
35
|
+
}
|
|
25
36
|
const modelsObj: Record<string, unknown> = {}
|
|
26
37
|
for (const entry of models) {
|
|
27
38
|
const key = toConfigKey(entry.id)
|
package/src/model-sync.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync, realpathSync, renameSync, writeFileSync } from "fs"
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, renameSync, writeFileSync } from "fs"
|
|
2
2
|
import { dirname, join } from "path"
|
|
3
3
|
import { fileURLToPath } from "url"
|
|
4
4
|
import { resolveApiKey } from "./auth.js"
|
|
@@ -55,6 +55,11 @@ function writeAtomic(file: string, data: string) {
|
|
|
55
55
|
// Locate the model catalog bundled with the installed Command Code CLI.
|
|
56
56
|
// The catalog is the vendor-maintained list for the same API endpoint this
|
|
57
57
|
// provider talks to, so it is the most accurate metadata source available.
|
|
58
|
+
function bundledMdIn(root: string): string | null {
|
|
59
|
+
const candidate = join(root, "command-code", "dist", "bundled", "command-code-knowledge", "reference", "models.md")
|
|
60
|
+
return existsSync(candidate) ? candidate : null
|
|
61
|
+
}
|
|
62
|
+
|
|
58
63
|
function findModelsMd(): string | null {
|
|
59
64
|
const explicit = process.env.COMMANDCODE_MODELS_MD
|
|
60
65
|
if (explicit && existsSync(explicit)) return explicit
|
|
@@ -80,6 +85,28 @@ function findModelsMd(): string | null {
|
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
87
|
}
|
|
88
|
+
|
|
89
|
+
// Common global npm roots — PATH-independent so background services
|
|
90
|
+
// (OpenChamber, launchd/systemd) resolve the catalog even when the npm
|
|
91
|
+
// global bin dir is not exported into their environment.
|
|
92
|
+
const roots: string[] = []
|
|
93
|
+
const home = process.env.HOME
|
|
94
|
+
if (home) {
|
|
95
|
+
for (const base of [join(home, ".local", "share", "nvm"), join(home, ".nvm", "versions", "node"), join(home, ".npm-global", "lib")]) {
|
|
96
|
+
try {
|
|
97
|
+
for (const version of readdirSync(base)) {
|
|
98
|
+
roots.push(join(base, version, "lib", "node_modules"))
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// directory absent — skip
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
roots.push("/usr/local/lib/node_modules", "/usr/lib/node_modules", "/opt/homebrew/lib/node_modules")
|
|
106
|
+
for (const root of roots) {
|
|
107
|
+
const found = bundledMdIn(root)
|
|
108
|
+
if (found) return found
|
|
109
|
+
}
|
|
83
110
|
return null
|
|
84
111
|
}
|
|
85
112
|
|
|
@@ -247,6 +274,34 @@ export function parseModelsMd(text: string, hints: Map<string, ModelEntry>): Mod
|
|
|
247
274
|
return entries
|
|
248
275
|
}
|
|
249
276
|
|
|
277
|
+
// Server-managed master catalog. The server (keypool) serves the same
|
|
278
|
+
// enriched model list this provider builds locally, so clients fetch it at
|
|
279
|
+
// startup and always see current models/capabilities — no plugin update and
|
|
280
|
+
// no `op pull` needed when the catalog changes.
|
|
281
|
+
export async function fetchRemoteCatalog(url: string, timeoutMs = 1500): Promise<ModelEntry[] | null> {
|
|
282
|
+
const controller = new AbortController()
|
|
283
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
284
|
+
try {
|
|
285
|
+
const response = await fetch(url, { signal: controller.signal, headers: { accept: "application/json" } })
|
|
286
|
+
if (!response.ok) return null
|
|
287
|
+
const payload = (await response.json()) as unknown
|
|
288
|
+
const list = Array.isArray(payload)
|
|
289
|
+
? payload
|
|
290
|
+
: payload && typeof payload === "object" && Array.isArray((payload as { models?: unknown }).models)
|
|
291
|
+
? (payload as { models: unknown[] }).models
|
|
292
|
+
: null
|
|
293
|
+
if (!list) return null
|
|
294
|
+
const entries = list.filter(
|
|
295
|
+
(entry): entry is ModelEntry => typeof entry === "object" && entry !== null && typeof (entry as { id?: unknown }).id === "string",
|
|
296
|
+
)
|
|
297
|
+
return entries.length >= 5 ? entries : null
|
|
298
|
+
} catch {
|
|
299
|
+
return null
|
|
300
|
+
} finally {
|
|
301
|
+
clearTimeout(timer)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
250
305
|
async function fetchApiModels(): Promise<ApiModel[] | null> {
|
|
251
306
|
const apiKey = resolveApiKey({})
|
|
252
307
|
const controller = new AbortController()
|
|
@@ -266,7 +321,7 @@ async function fetchApiModels(): Promise<ApiModel[] | null> {
|
|
|
266
321
|
}
|
|
267
322
|
}
|
|
268
323
|
|
|
269
|
-
function mergeApiModels(entries: ModelEntry[], api: ApiModel[]): ModelEntry[] {
|
|
324
|
+
function mergeApiModels(entries: ModelEntry[], api: ApiModel[], hints?: Map<string, ModelEntry>): ModelEntry[] {
|
|
270
325
|
const seen = new Map(entries.map((entry) => [entry.id.toLowerCase(), entry]))
|
|
271
326
|
for (const model of api) {
|
|
272
327
|
if (!model?.id) continue
|
|
@@ -278,15 +333,32 @@ function mergeApiModels(entries: ModelEntry[], api: ApiModel[]): ModelEntry[] {
|
|
|
278
333
|
}
|
|
279
334
|
continue
|
|
280
335
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
336
|
+
// Prefer the bundled snapshot's metadata (plan badge, variants, vision) so
|
|
337
|
+
// machines without the CLI catalog still get full capabilities.
|
|
338
|
+
const hint = hints?.get(key)
|
|
339
|
+
const entry: ModelEntry = hint
|
|
340
|
+
? {
|
|
341
|
+
...hint,
|
|
342
|
+
modalities: hint.modalities ? { input: [...hint.modalities.input], output: [...hint.modalities.output] } : undefined,
|
|
343
|
+
variants: hint.variants ? { ...hint.variants } : undefined,
|
|
344
|
+
reasoningEfforts: hint.reasoningEfforts ? [...hint.reasoningEfforts] : undefined,
|
|
345
|
+
limit: {
|
|
346
|
+
context:
|
|
347
|
+
model.context_length && model.context_length > 0
|
|
348
|
+
? model.context_length
|
|
349
|
+
: hint.limit.context,
|
|
350
|
+
output: hint.limit.output,
|
|
351
|
+
},
|
|
352
|
+
}
|
|
353
|
+
: {
|
|
354
|
+
id: model.id,
|
|
355
|
+
name: model.name?.trim() || model.id.split("/").pop() || model.id,
|
|
356
|
+
tier: "open-source",
|
|
357
|
+
reasoning: true,
|
|
358
|
+
tool_call: true,
|
|
359
|
+
cost: { input: 0, output: 0 },
|
|
360
|
+
limit: { context: model.context_length && model.context_length > 0 ? model.context_length : 200000, output: 65536 },
|
|
361
|
+
}
|
|
290
362
|
entries.push(entry)
|
|
291
363
|
seen.set(key, entry)
|
|
292
364
|
}
|
|
@@ -294,44 +366,68 @@ function mergeApiModels(entries: ModelEntry[], api: ApiModel[]): ModelEntry[] {
|
|
|
294
366
|
}
|
|
295
367
|
|
|
296
368
|
// Build the model list fresh on every opencode instance start:
|
|
297
|
-
// 1.
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
|
|
369
|
+
// 1. REMOTE master catalog (server-managed; always current — primary source
|
|
370
|
+
// so client devices never depend on a local CLI install for metadata)
|
|
371
|
+
// 2. local CLI-bundled catalog + Command Code's public model endpoint
|
|
372
|
+
// (used to append models newer than the server catalog, and as the
|
|
373
|
+
// offline fallback when the server is unreachable)
|
|
374
|
+
// 3. last synced cache, then the bundled snapshot as a final fallback.
|
|
375
|
+
export async function loadSyncedModels(options?: { catalogUrl?: string }): Promise<SyncResult> {
|
|
301
376
|
const bundled = loadCachedModels(BUNDLED_MODELS)
|
|
302
377
|
const cached = loadCachedModels(CACHE_MODELS)
|
|
303
378
|
const hints = new Map<string, ModelEntry>()
|
|
304
379
|
for (const entry of [...(cached ?? []), ...(bundled ?? [])]) hints.set(entry.id.toLowerCase(), entry)
|
|
305
380
|
|
|
306
|
-
let
|
|
381
|
+
let localModels: ModelEntry[] | null = null
|
|
382
|
+
let localSource = ""
|
|
307
383
|
try {
|
|
308
384
|
const modelsMd = findModelsMd()
|
|
309
385
|
if (modelsMd) {
|
|
310
386
|
const parsed = parseModelsMd(readFileSync(modelsMd, "utf-8"), hints)
|
|
311
|
-
if (parsed.length >= 5)
|
|
387
|
+
if (parsed.length >= 5) {
|
|
388
|
+
const apiModels = await fetchApiModels()
|
|
389
|
+
localModels = mergeApiModels(parsed, apiModels ?? [], hints)
|
|
390
|
+
localSource = apiModels?.length ? "cli-catalog+api" : "cli-catalog"
|
|
391
|
+
}
|
|
312
392
|
}
|
|
313
393
|
} catch {
|
|
314
|
-
// intentionally silent: fall through to the API or
|
|
394
|
+
// intentionally silent: fall through to the API or remote catalog
|
|
395
|
+
}
|
|
396
|
+
if (!localModels?.length) {
|
|
397
|
+
const apiModels = await fetchApiModels()
|
|
398
|
+
if (apiModels?.length) {
|
|
399
|
+
localModels = mergeApiModels([], apiModels, hints)
|
|
400
|
+
localSource = "api"
|
|
401
|
+
}
|
|
315
402
|
}
|
|
316
|
-
|
|
317
|
-
const apiModels = await fetchApiModels()
|
|
318
403
|
|
|
319
404
|
let models: ModelEntry[] | null = null
|
|
320
405
|
let source = ""
|
|
321
|
-
if (
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
406
|
+
if (options?.catalogUrl) {
|
|
407
|
+
const remote = await fetchRemoteCatalog(options.catalogUrl)
|
|
408
|
+
if (remote) {
|
|
409
|
+
models = remote
|
|
410
|
+
source = "remote"
|
|
411
|
+
if (localModels?.length) {
|
|
412
|
+
const seen = new Set(remote.map((entry) => entry.id.toLowerCase()))
|
|
413
|
+
const extra = localModels.filter((entry) => !seen.has(entry.id.toLowerCase()))
|
|
414
|
+
if (extra.length) {
|
|
415
|
+
models = [...remote, ...extra]
|
|
416
|
+
source = "remote+local"
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (!models?.length && localModels?.length) {
|
|
422
|
+
models = localModels
|
|
423
|
+
source = localSource
|
|
327
424
|
}
|
|
328
|
-
|
|
329
425
|
if (!models?.length) {
|
|
330
426
|
models = cached ?? bundled ?? []
|
|
331
427
|
source = cached ? "cache" : "bundled"
|
|
332
428
|
}
|
|
333
429
|
|
|
334
|
-
if (!models.length) throw new Error("Command Code model catalog unavailable (no CLI, API, cache, or bundled list)")
|
|
430
|
+
if (!models.length) throw new Error("Command Code model catalog unavailable (no remote, CLI, API, cache, or bundled list)")
|
|
335
431
|
|
|
336
432
|
if (source !== "cache" && source !== "bundled") {
|
|
337
433
|
try {
|
|
@@ -341,5 +437,9 @@ export async function loadSyncedModels(): Promise<SyncResult> {
|
|
|
341
437
|
}
|
|
342
438
|
}
|
|
343
439
|
|
|
440
|
+
if (process.env.COMMANDCODE_DEBUG) {
|
|
441
|
+
console.warn(`[commandcode] catalog source=${source} models=${models.length}`)
|
|
442
|
+
}
|
|
443
|
+
|
|
344
444
|
return { models, source }
|
|
345
445
|
}
|