opencode-cmd-provider 0.1.2 → 0.1.3
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 +10 -0
- package/README.md +39 -51
- package/dist/index.js +1 -1
- package/dist/src/catalog/snapshot.d.ts +6 -0
- package/dist/src/catalog/snapshot.js +62 -0
- package/dist/src/env.d.ts +0 -5
- package/dist/src/env.js +1 -20
- package/dist/src/plugin/index.js +12 -23
- package/dist/src/plugin/models.d.ts +33 -5
- package/dist/src/plugin/models.js +89 -45
- package/dist/src/provider/reasoning.d.ts +11 -0
- package/dist/src/provider/reasoning.js +14 -0
- package/package.json +3 -2
- package/dist/src/provider/models.d.ts +0 -29
- package/dist/src/provider/models.js +0 -229
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.3 - 2026-08-16
|
|
4
|
+
|
|
5
|
+
Feature: zero-config install via auto-registration from a bundled model snapshot.
|
|
6
|
+
|
|
7
|
+
- The plugin now bundles a snapshot of the Command Code model catalog (`src/catalog/snapshot.ts`, 55 models) and auto-registers the `commandcode` provider — npm, name, `env`, and every model — during its `config` hook. Installing the plugin is enough: all Command Code models appear in `/models` with the `[CMD]` display-name prefix, no `provider.commandcode` block or `models` map required. No network access is ever needed at runtime.
|
|
8
|
+
- Declared config wins: provider-level keys are filled only when unset, declared models are never modified or removed, models that left the catalog stay usable, and `whitelist`/`blacklist` still filter auto-registered models.
|
|
9
|
+
- The snapshot is regenerated from the live catalog at every release via `npm run refresh:snapshot` (`scripts/refresh-snapshot.mjs`); newly published models appear after a plugin update.
|
|
10
|
+
- Deleted the now-dead network machinery: live catalog fetch and cache (`loadCommandCodeModels`, cache file), the `provider.models` hook, `catalogToOpenCodeModels`, and the `COMMANDCODE_MODELS_*` env vars.
|
|
11
|
+
- `COMMANDCODE_API_BASE` now actually overrides the runtime API base (injected as `options.baseURL` when the user declares no `baseURL` of their own).
|
|
12
|
+
|
|
3
13
|
## 0.1.2 - 2026-08-15
|
|
4
14
|
|
|
5
15
|
Fix: tool-call history round-trip and reasoning effort. Live probes against the Command Code API with opencode confirmed the AI SDK v3 shapes differ from what the converters expected:
|
package/README.md
CHANGED
|
@@ -16,37 +16,29 @@ Add the package to your opencode configuration:
|
|
|
16
16
|
{
|
|
17
17
|
"$schema": "https://opencode.ai/config.json",
|
|
18
18
|
"plugin": ["opencode-cmd-provider"],
|
|
19
|
-
"provider": {
|
|
20
|
-
"commandcode": {
|
|
21
|
-
"npm": "opencode-cmd-provider",
|
|
22
|
-
"name": "Command Code",
|
|
23
|
-
"options": { "baseURL": "https://api.commandcode.ai" },
|
|
24
|
-
"models": {
|
|
25
|
-
"claude-sonnet-5": {
|
|
26
|
-
"name": "Claude Sonnet 5",
|
|
27
|
-
"limit": { "context": 200000, "output": 65536 },
|
|
28
|
-
},
|
|
29
|
-
"deepseek/deepseek-v4-flash": {
|
|
30
|
-
"name": "DeepSeek V4 Flash",
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
19
|
}
|
|
36
20
|
```
|
|
37
21
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
22
|
+
That's it. The plugin bundles a snapshot of the Command Code model catalog and
|
|
23
|
+
auto-registers the `commandcode` provider — npm package, name, API key env var,
|
|
24
|
+
and every model — when opencode loads. No `provider.commandcode` block, no
|
|
25
|
+
`models` map, no network access needed. All Command Code models appear in
|
|
26
|
+
`/models` with the `[CMD]` display-name prefix (e.g. `[CMD] Claude Sonnet 5`) so
|
|
27
|
+
they aren't confused with same-named models from other providers.
|
|
28
|
+
|
|
29
|
+
You can still declare your own `provider.commandcode` entry to customize
|
|
30
|
+
behavior; your declarations always win and the snapshot fills in only what's
|
|
31
|
+
missing:
|
|
32
|
+
|
|
33
|
+
- Declared provider-level settings (`name`, `options`, `baseURL`, `env`) are
|
|
34
|
+
kept as you wrote them.
|
|
35
|
+
- Declared models stay exactly as you wrote them — the snapshot never modifies
|
|
36
|
+
or removes them, and models that left the catalog remain usable.
|
|
37
|
+
- `whitelist`/`blacklist` on a declared entry filter the auto-registered models
|
|
38
|
+
too, keeping the picker uncluttered.
|
|
39
|
+
|
|
40
|
+
The catalog snapshot is refreshed at every release; newly published Command
|
|
41
|
+
Code models appear after a plugin update.
|
|
50
42
|
|
|
51
43
|
Start or reload opencode, then authenticate:
|
|
52
44
|
|
|
@@ -109,34 +101,30 @@ Pick a model with `/models`, or run non-interactively:
|
|
|
109
101
|
opencode run --model commandcode/claude-sonnet-5 "hello"
|
|
110
102
|
```
|
|
111
103
|
|
|
112
|
-
Model availability changes over time and is refreshed from the Command Code catalog when opencode loads.
|
|
113
|
-
|
|
114
|
-
### Reasoning support
|
|
115
|
-
|
|
116
|
-
Reasoning metadata is enriched only for models whose Command Code effort support is known. Supported levels are sent as the documented `reasoning_effort` request field; `off`, unsupported levels, and newly discovered models without metadata do not add reasoning fields to the request. No prompt instructions are injected.
|
|
117
|
-
|
|
118
|
-
Reasoning blocks from completed assistant turns are not replayed to Command Code in later requests; only the user-visible text and completed tool calls are sent back as history. This prevents prior private reasoning traces from interfering with reasoning on follow-up turns.
|
|
119
|
-
|
|
120
104
|
## Model discovery and offline behavior
|
|
121
105
|
|
|
122
|
-
The
|
|
106
|
+
The plugin ships a snapshot of the Command Code model catalog (`src/catalog/snapshot.ts`)
|
|
107
|
+
and auto-registers every model into opencode's config at startup. Model
|
|
108
|
+
availability changes when the package is updated: the snapshot is regenerated
|
|
109
|
+
from the live catalog at every release via `npm run refresh:snapshot`.
|
|
123
110
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
111
|
+
Auto-registration adds no network latency to opencode startup — the snapshot is
|
|
112
|
+
bundled, and the plugin never contacts the Command Code API to list models. The
|
|
113
|
+
plugin works fully offline; model availability never depends on the catalog
|
|
114
|
+
endpoint being reachable.
|
|
127
115
|
|
|
128
|
-
The
|
|
116
|
+
The following environment variable is intended for tests, local mocks, and
|
|
117
|
+
compatible API endpoints:
|
|
129
118
|
|
|
130
|
-
|
|
119
|
+
| Variable | Purpose |
|
|
120
|
+
| ---------------------- | -------------------------------------- |
|
|
121
|
+
| `COMMANDCODE_API_BASE` | Override the Command Code API base URL |
|
|
131
122
|
|
|
132
|
-
|
|
123
|
+
### Reasoning support
|
|
124
|
+
|
|
125
|
+
Reasoning metadata is enriched only for models whose Command Code effort support is known. Supported levels are sent as the documented `reasoning_effort` request field; `off`, unsupported levels, and newly discovered models without metadata do not add reasoning fields to the request. No prompt instructions are injected.
|
|
133
126
|
|
|
134
|
-
|
|
135
|
-
| ------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
136
|
-
| `COMMANDCODE_API_BASE` | Override the Command Code API base URL |
|
|
137
|
-
| `COMMANDCODE_MODELS_URL` | Override the model catalog endpoint |
|
|
138
|
-
| `COMMANDCODE_MODELS_CACHE` | Override the model cache file path |
|
|
139
|
-
| `COMMANDCODE_MODELS_TIMEOUT_MS` | Catalog fetch timeout (defaults to 10 seconds; invalid or non-positive values use the default) |
|
|
127
|
+
Reasoning blocks from completed assistant turns are not replayed to Command Code in later requests; only the user-visible text and completed tool calls are sent back as history. This prevents prior private reasoning traces from interfering with reasoning on follow-up turns.
|
|
140
128
|
|
|
141
129
|
## Image input
|
|
142
130
|
|
|
@@ -152,7 +140,7 @@ Models missing from that table display zero cost in opencode. This does **not**
|
|
|
152
140
|
|
|
153
141
|
## Update and remove
|
|
154
142
|
|
|
155
|
-
Update the installed package, or remove it from the `plugin` array
|
|
143
|
+
Update the installed package, or remove it from the `plugin` array in your `opencode.json` (the auto-registered `provider.commandcode` entry is injected by the plugin, so there is no config block to remove). The npm package is cached under opencode's plugin cache (`~/.cache/opencode/packages/`); remove the cached directory to fully uninstall.
|
|
156
144
|
|
|
157
145
|
## Development
|
|
158
146
|
|
|
@@ -171,7 +159,7 @@ The headless end-to-end test runs the real opencode CLI against a mock Command C
|
|
|
171
159
|
npm run build && npm run test:e2e
|
|
172
160
|
```
|
|
173
161
|
|
|
174
|
-
`scripts/opencode-fixture.mjs` writes a throwaway `opencode.json` wiring the local build
|
|
162
|
+
`scripts/opencode-fixture.mjs` writes a throwaway `opencode.json` wiring only the local build as a plugin — no declared provider or models — so `opencode models` proves auto-registration against a real opencode binary. `test:e2e` is a local dev gate (it needs the real `opencode` binary on PATH) and is excluded from `npm test`.
|
|
175
163
|
|
|
176
164
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for local setup and tests. See [RELEASE.md](RELEASE.md) for the release process.
|
|
177
165
|
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
// Dual export: opencode's provider loader scans for the first export whose name
|
|
3
3
|
// starts with "create"; its plugin loader early-returns on a V1 default module.
|
|
4
4
|
// Exactly one create* export + a { id, server } default keeps both loaders safe
|
|
5
|
-
// (see
|
|
5
|
+
// (verified loader behavior; see ADR-0001). Enforced by tests/contract.test.ts.
|
|
6
6
|
export { createCommandCode } from "./src/provider/index.js";
|
|
7
7
|
export { default } from "./src/plugin/index.js";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/catalog/snapshot.ts — GENERATED by scripts/refresh-snapshot.mjs. Do not edit.
|
|
2
|
+
//
|
|
3
|
+
// A copy of the Command Code model catalog (id, name, context length) bundled
|
|
4
|
+
// in the package so the plugin can auto-register every model without network
|
|
5
|
+
// access at runtime. Regenerate with `npm run refresh:snapshot`.
|
|
6
|
+
export const MODEL_SNAPSHOT = [
|
|
7
|
+
{ id: "claude-sonnet-5", name: "Claude Sonnet 5", contextLength: 1000000 },
|
|
8
|
+
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", contextLength: 1000000 },
|
|
9
|
+
{ id: "claude-fable-5", name: "Claude Fable 5", contextLength: 1000000 },
|
|
10
|
+
{ id: "claude-opus-5", name: "Claude Opus 5", contextLength: 1000000 },
|
|
11
|
+
{ id: "claude-opus-4-8", name: "Claude Opus 4.8", contextLength: 1000000 },
|
|
12
|
+
{ id: "claude-opus-4-7", name: "Claude Opus 4.7", contextLength: 1000000 },
|
|
13
|
+
{ id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5", contextLength: 200000 },
|
|
14
|
+
{ id: "gpt-5.6-sol", name: "GPT-5.6 Sol", contextLength: 1050000 },
|
|
15
|
+
{ id: "gpt-5.6-terra", name: "GPT-5.6 Terra", contextLength: 1050000 },
|
|
16
|
+
{ id: "gpt-5.6-luna", name: "GPT-5.6 Luna", contextLength: 1050000 },
|
|
17
|
+
{ id: "gpt-5.5", name: "GPT-5.5", contextLength: 200000 },
|
|
18
|
+
{ id: "gpt-5.4", name: "GPT-5.4", contextLength: 400000 },
|
|
19
|
+
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex", contextLength: 400000 },
|
|
20
|
+
{ id: "gpt-5.4-mini", name: "GPT-5.4 Mini", contextLength: 400000 },
|
|
21
|
+
{ id: "deepseek/deepseek-v4-pro", name: "DeepSeek V4 Pro (latest)", contextLength: 1000000 },
|
|
22
|
+
{ id: "deepseek/deepseek-v4-flash", name: "DeepSeek V4 Flash (latest)", contextLength: 1000000 },
|
|
23
|
+
{ id: "moonshotai/Kimi-K3", name: "Kimi K3", contextLength: 1000000 },
|
|
24
|
+
{ id: "moonshotai/Kimi-K2.7-Code", name: "Kimi K2.7 Code", contextLength: 256000 },
|
|
25
|
+
{ id: "moonshotai/Kimi-K2.7-Code-Highspeed", name: "Kimi K2.7 Code HighSpeed", contextLength: 262000 },
|
|
26
|
+
{ id: "moonshotai/Kimi-K2.6", name: "Kimi K2.6", contextLength: 256000 },
|
|
27
|
+
{ id: "moonshotai/Kimi-K2.5", name: "Kimi K2.5", contextLength: 256000 },
|
|
28
|
+
{ id: "zai-org/GLM-5.3", name: "GLM-5.3", contextLength: 1000000 },
|
|
29
|
+
{ id: "zai-org/GLM-5.2", name: "GLM-5.2", contextLength: 1000000 },
|
|
30
|
+
{ id: "zai-org/GLM-5.2-Fast", name: "GLM-5.2 Fast", contextLength: 1000000 },
|
|
31
|
+
{ id: "zai-org/GLM-5.1", name: "GLM-5.1", contextLength: 200000 },
|
|
32
|
+
{ id: "zai-org/GLM-5", name: "GLM-5", contextLength: 200000 },
|
|
33
|
+
{ id: "MiniMaxAI/MiniMax-M3", name: "MiniMax M3", contextLength: 1000000 },
|
|
34
|
+
{ id: "MiniMaxAI/MiniMax-M2.7", name: "MiniMax M2.7", contextLength: 200000 },
|
|
35
|
+
{ id: "MiniMaxAI/MiniMax-M2.5", name: "MiniMax M2.5", contextLength: 200000 },
|
|
36
|
+
{ id: "xiaomi/mimo-v2.5-pro", name: "MiMo V2.5 Pro", contextLength: 1000000 },
|
|
37
|
+
{ id: "xiaomi/mimo-v2.5", name: "MiMo V2.5", contextLength: 1000000 },
|
|
38
|
+
{ id: "Qwen/Qwen3.8-Max", name: "Qwen 3.8 Max", contextLength: 1000000 },
|
|
39
|
+
{ id: "Qwen/Qwen3.7-Max", name: "Qwen 3.7 Max", contextLength: 1000000 },
|
|
40
|
+
{ id: "Qwen/Qwen3.7-Plus", name: "Qwen 3.7 Plus", contextLength: 1000000 },
|
|
41
|
+
{ id: "Qwen/Qwen3.7-Flash", name: "Qwen 3.7 Flash", contextLength: 1000000 },
|
|
42
|
+
{ id: "Qwen/Qwen3.6-Max-Preview", name: "Qwen 3.6 Max Preview", contextLength: 200000 },
|
|
43
|
+
{ id: "Qwen/Qwen3.6-Plus", name: "Qwen 3.6 Plus", contextLength: 200000 },
|
|
44
|
+
{ id: "stepfun/Step-3.7-Flash", name: "Step 3.7 Flash", contextLength: 256000 },
|
|
45
|
+
{ id: "stepfun/Step-3.5-Flash", name: "Step 3.5 Flash", contextLength: 1000000 },
|
|
46
|
+
{ id: "tencent/hy3-paid", name: "Tencent Hy3", contextLength: 262144 },
|
|
47
|
+
{ id: "google/gemini-3.7-flash", name: "Gemini 3.7 Flash", contextLength: 1048576 },
|
|
48
|
+
{ id: "google/gemini-3.6-flash", name: "Gemini 3.6 Flash", contextLength: 1000000 },
|
|
49
|
+
{ id: "google/gemini-3.5-flash", name: "Gemini 3.5 Flash", contextLength: 1000000 },
|
|
50
|
+
{ id: "google/gemini-3.5-flash-lite", name: "Gemini 3.5 Flash Lite", contextLength: 1000000 },
|
|
51
|
+
{ id: "google/gemini-3.1-flash-lite", name: "Gemini 3.1 Flash Lite", contextLength: 1000000 },
|
|
52
|
+
{ id: "sakana/fugu-ultra", name: "Fugu Ultra", contextLength: 1000000 },
|
|
53
|
+
{ id: "nvidia/nemotron-3-ultra-550b-a55b", name: "Nemotron 3 Ultra", contextLength: 1000000 },
|
|
54
|
+
{ id: "thinkingmachines/inkling", name: "Inkling", contextLength: 256000 },
|
|
55
|
+
{ id: "thinkingmachines/inkling-small", name: "Inkling Small", contextLength: 1000000 },
|
|
56
|
+
{ id: "poolside/laguna-s-2.1-free", name: "Laguna S 2.1", contextLength: 256000 },
|
|
57
|
+
{ id: "meta/muse-spark-1.1", name: "Muse Spark 1.1", contextLength: 1048576 },
|
|
58
|
+
{ id: "meta/muse-spark-1.2", name: "Muse Spark 1.2", contextLength: 1048576 },
|
|
59
|
+
{ id: "meta/muse-spark-1.2-contributor", name: "Muse Spark 1.2 Contributor", contextLength: 1048576 },
|
|
60
|
+
{ id: "xai/grok-4.5", name: "Grok 4.5", contextLength: 500000 },
|
|
61
|
+
{ id: "xai/grok-4.6", name: "Grok 4.6", contextLength: 500000 },
|
|
62
|
+
];
|
package/dist/src/env.d.ts
CHANGED
|
@@ -1,7 +1,2 @@
|
|
|
1
1
|
export declare const DEFAULT_API_BASE = "https://api.commandcode.ai";
|
|
2
|
-
export declare const DEFAULT_MODELS_URL = "https://api.commandcode.ai/provider/v1/models";
|
|
3
|
-
export declare const DEFAULT_MODELS_TIMEOUT_MS = 10000;
|
|
4
2
|
export declare function getApiBase(env?: NodeJS.ProcessEnv): string;
|
|
5
|
-
export declare function getModelsUrl(env?: NodeJS.ProcessEnv): string;
|
|
6
|
-
export declare function getModelsTimeoutMs(env?: NodeJS.ProcessEnv): number;
|
|
7
|
-
export declare function getDataDir(env?: NodeJS.ProcessEnv, homeDir?: () => string): string;
|
package/dist/src/env.js
CHANGED
|
@@ -1,24 +1,5 @@
|
|
|
1
|
-
// src/env.ts — COMMANDCODE_* environment overrides with safe defaults
|
|
2
|
-
import { homedir } from "node:os";
|
|
1
|
+
// src/env.ts — COMMANDCODE_* environment overrides with safe defaults
|
|
3
2
|
export const DEFAULT_API_BASE = "https://api.commandcode.ai";
|
|
4
|
-
export const DEFAULT_MODELS_URL = "https://api.commandcode.ai/provider/v1/models";
|
|
5
|
-
export const DEFAULT_MODELS_TIMEOUT_MS = 10_000;
|
|
6
3
|
export function getApiBase(env = process.env) {
|
|
7
4
|
return env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE;
|
|
8
5
|
}
|
|
9
|
-
export function getModelsUrl(env = process.env) {
|
|
10
|
-
return env.COMMANDCODE_MODELS_URL ?? DEFAULT_MODELS_URL;
|
|
11
|
-
}
|
|
12
|
-
export function getModelsTimeoutMs(env = process.env) {
|
|
13
|
-
const raw = env.COMMANDCODE_MODELS_TIMEOUT_MS;
|
|
14
|
-
if (!raw)
|
|
15
|
-
return DEFAULT_MODELS_TIMEOUT_MS;
|
|
16
|
-
const parsed = Number(raw);
|
|
17
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MODELS_TIMEOUT_MS;
|
|
18
|
-
}
|
|
19
|
-
export function getDataDir(env = process.env, homeDir = homedir) {
|
|
20
|
-
const xdg = env.XDG_DATA_HOME;
|
|
21
|
-
if (xdg)
|
|
22
|
-
return xdg;
|
|
23
|
-
return `${homeDir()}/.local/share/opencode`;
|
|
24
|
-
}
|
package/dist/src/plugin/index.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
// src/plugin/index.ts — V1 opencode plugin module
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
// src/plugin/index.ts — V1 opencode plugin module
|
|
2
|
+
import { MODEL_SNAPSHOT } from "../catalog/snapshot.js";
|
|
3
|
+
import { getApiBase } from "../env.js";
|
|
4
|
+
import { augmentConfigCommandCodeModels, autoRegister } from "./models.js";
|
|
5
5
|
import { runAuthFlow } from "./auth.js";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
import { mkdirSync } from "node:fs";
|
|
8
6
|
const server = async () => {
|
|
9
7
|
return {
|
|
8
|
+
config: async (config) => {
|
|
9
|
+
autoRegister(config, MODEL_SNAPSHOT, {
|
|
10
|
+
npm: "opencode-cmd-provider",
|
|
11
|
+
name: "Command Code",
|
|
12
|
+
baseURL: getApiBase(),
|
|
13
|
+
});
|
|
14
|
+
augmentConfigCommandCodeModels(config);
|
|
15
|
+
},
|
|
10
16
|
auth: {
|
|
11
17
|
provider: "commandcode",
|
|
12
18
|
methods: [
|
|
@@ -17,23 +23,6 @@ const server = async () => {
|
|
|
17
23
|
},
|
|
18
24
|
],
|
|
19
25
|
},
|
|
20
|
-
provider: {
|
|
21
|
-
id: "commandcode",
|
|
22
|
-
models: async () => {
|
|
23
|
-
const cacheDir = getDataDir();
|
|
24
|
-
mkdirSync(cacheDir, { recursive: true });
|
|
25
|
-
const cachePath = process.env.COMMANDCODE_MODELS_CACHE ?? join(cacheDir, "commandcode-models.json");
|
|
26
|
-
const loaded = await loadCommandCodeModels({
|
|
27
|
-
url: getModelsUrl(),
|
|
28
|
-
cachePath,
|
|
29
|
-
timeoutMs: getModelsTimeoutMs(),
|
|
30
|
-
});
|
|
31
|
-
return catalogToOpenCodeModels(loaded.models, {
|
|
32
|
-
npm: "opencode-cmd-provider",
|
|
33
|
-
url: getApiBase(),
|
|
34
|
-
});
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
26
|
};
|
|
38
27
|
};
|
|
39
28
|
export default { id: "commandcode", server };
|
|
@@ -1,7 +1,35 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
export interface
|
|
1
|
+
import type { Config } from "@opencode-ai/sdk/v2";
|
|
2
|
+
import type { CatalogModel } from "../catalog/snapshot.js";
|
|
3
|
+
export interface AutoRegisterOptions {
|
|
4
4
|
npm: string;
|
|
5
|
-
|
|
5
|
+
name: string;
|
|
6
|
+
baseURL: string;
|
|
6
7
|
}
|
|
7
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Registers the `commandcode` provider entry into opencode's config so every
|
|
10
|
+
* snapshot model is available without any user declaration.
|
|
11
|
+
*
|
|
12
|
+
* opencode only invokes a plugin's `provider.models` hook for providers that
|
|
13
|
+
* already exist in its models.dev catalog; `commandcode` is not in that
|
|
14
|
+
* catalog, so models reach the session through the config-declared
|
|
15
|
+
* `provider.commandcode.models` map. The `config` hook runs before opencode
|
|
16
|
+
* reads `config.provider` and honors in-place mutation (ADR-0001), which makes
|
|
17
|
+
* this the only zero-declaration mechanism available today.
|
|
18
|
+
*
|
|
19
|
+
* Pure function: takes a config and a snapshot, mutates and returns the config.
|
|
20
|
+
* Merge semantics — the user's declared entry always wins:
|
|
21
|
+
* - provider-level keys (`npm`, `name`, `env`, `options.baseURL`) are filled
|
|
22
|
+
* only when unset;
|
|
23
|
+
* - snapshot models are added only when no declared model claims the id (by
|
|
24
|
+
* config key or by the entry's `id`);
|
|
25
|
+
* - declared models are never modified, and declared models that left the
|
|
26
|
+
* catalog stay usable.
|
|
27
|
+
*/
|
|
28
|
+
export declare function autoRegister(config: Config, snapshot: readonly CatalogModel[], options: AutoRegisterOptions): Config;
|
|
29
|
+
/**
|
|
30
|
+
* Augments config-declared commandcode models with reasoning metadata and
|
|
31
|
+
* variants so opencode's `ctrl+t` can cycle reasoning effort.
|
|
32
|
+
*
|
|
33
|
+
* In-place mutation of the config object (the plugin `config` hook contract).
|
|
34
|
+
*/
|
|
35
|
+
export declare function augmentConfigCommandCodeModels(config: Config): void;
|
|
@@ -1,50 +1,94 @@
|
|
|
1
1
|
import { MODEL_COSTS, ZERO_MODEL_COST } from "../provider/pricing.js";
|
|
2
|
-
import {
|
|
2
|
+
import { reasoningVariantsForModel } from "../provider/reasoning.js";
|
|
3
3
|
import { inputModalitiesForModel } from "../provider/modalities.js";
|
|
4
4
|
const DEFAULT_MAX_OUTPUT_TOKENS = 65_536;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
: {}),
|
|
44
|
-
},
|
|
45
|
-
headers: {},
|
|
46
|
-
release_date: "",
|
|
47
|
-
};
|
|
5
|
+
/**
|
|
6
|
+
* Registers the `commandcode` provider entry into opencode's config so every
|
|
7
|
+
* snapshot model is available without any user declaration.
|
|
8
|
+
*
|
|
9
|
+
* opencode only invokes a plugin's `provider.models` hook for providers that
|
|
10
|
+
* already exist in its models.dev catalog; `commandcode` is not in that
|
|
11
|
+
* catalog, so models reach the session through the config-declared
|
|
12
|
+
* `provider.commandcode.models` map. The `config` hook runs before opencode
|
|
13
|
+
* reads `config.provider` and honors in-place mutation (ADR-0001), which makes
|
|
14
|
+
* this the only zero-declaration mechanism available today.
|
|
15
|
+
*
|
|
16
|
+
* Pure function: takes a config and a snapshot, mutates and returns the config.
|
|
17
|
+
* Merge semantics — the user's declared entry always wins:
|
|
18
|
+
* - provider-level keys (`npm`, `name`, `env`, `options.baseURL`) are filled
|
|
19
|
+
* only when unset;
|
|
20
|
+
* - snapshot models are added only when no declared model claims the id (by
|
|
21
|
+
* config key or by the entry's `id`);
|
|
22
|
+
* - declared models are never modified, and declared models that left the
|
|
23
|
+
* catalog stay usable.
|
|
24
|
+
*/
|
|
25
|
+
export function autoRegister(config, snapshot, options) {
|
|
26
|
+
if (snapshot.length === 0)
|
|
27
|
+
return config;
|
|
28
|
+
const provider = (config.provider ??= {});
|
|
29
|
+
const entry = (provider["commandcode"] ??= {});
|
|
30
|
+
entry.npm ??= options.npm;
|
|
31
|
+
entry.name ??= options.name;
|
|
32
|
+
entry.env ??= ["COMMANDCODE_API_KEY"];
|
|
33
|
+
entry.options ??= {};
|
|
34
|
+
entry.options.baseURL ??= options.baseURL;
|
|
35
|
+
entry.models ??= {};
|
|
36
|
+
const declaredById = new Set(Object.values(entry.models)
|
|
37
|
+
.map((model) => model?.id)
|
|
38
|
+
.filter((id) => typeof id === "string"));
|
|
39
|
+
for (const model of snapshot) {
|
|
40
|
+
if (entry.models[model.id] !== undefined || declaredById.has(model.id))
|
|
41
|
+
continue;
|
|
42
|
+
entry.models[model.id] = configModelFor(model);
|
|
48
43
|
}
|
|
49
|
-
return
|
|
44
|
+
return config;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Augments config-declared commandcode models with reasoning metadata and
|
|
48
|
+
* variants so opencode's `ctrl+t` can cycle reasoning effort.
|
|
49
|
+
*
|
|
50
|
+
* In-place mutation of the config object (the plugin `config` hook contract).
|
|
51
|
+
*/
|
|
52
|
+
export function augmentConfigCommandCodeModels(config) {
|
|
53
|
+
const provider = config.provider?.["commandcode"];
|
|
54
|
+
if (!provider?.models)
|
|
55
|
+
return;
|
|
56
|
+
for (const [modelId, model] of Object.entries(provider.models)) {
|
|
57
|
+
if (!model)
|
|
58
|
+
continue;
|
|
59
|
+
const variants = reasoningVariantsForModel(modelId);
|
|
60
|
+
if (variants) {
|
|
61
|
+
model.reasoning = true;
|
|
62
|
+
model.variants = variants;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Config-schema model entry (not the SDK `Model` shape) for a snapshot model:
|
|
68
|
+
* `[CMD]`-prefixed display name, context/output limits with output capped at
|
|
69
|
+
* 65_536, and metadata enriched from the reasoning, modality, and pricing
|
|
70
|
+
* tables.
|
|
71
|
+
*/
|
|
72
|
+
function configModelFor(model) {
|
|
73
|
+
const variants = reasoningVariantsForModel(model.id);
|
|
74
|
+
const costs = MODEL_COSTS[model.id] ?? ZERO_MODEL_COST;
|
|
75
|
+
return {
|
|
76
|
+
name: `[CMD] ${model.name}`,
|
|
77
|
+
limit: {
|
|
78
|
+
context: model.contextLength,
|
|
79
|
+
output: Math.min(model.contextLength, DEFAULT_MAX_OUTPUT_TOKENS),
|
|
80
|
+
},
|
|
81
|
+
reasoning: variants ? true : undefined,
|
|
82
|
+
variants: variants,
|
|
83
|
+
modalities: {
|
|
84
|
+
input: [...inputModalitiesForModel(model.id)],
|
|
85
|
+
},
|
|
86
|
+
cost: {
|
|
87
|
+
input: costs.input,
|
|
88
|
+
output: costs.output,
|
|
89
|
+
cache_read: costs.cacheRead,
|
|
90
|
+
cache_write: costs.cacheWrite,
|
|
91
|
+
},
|
|
92
|
+
status: "active",
|
|
93
|
+
};
|
|
50
94
|
}
|
|
@@ -20,6 +20,17 @@ export interface ThinkingMetadata {
|
|
|
20
20
|
}
|
|
21
21
|
export declare function thinkingMetadataForModel(modelId: string): ThinkingMetadata | undefined;
|
|
22
22
|
export declare function isReasoningModel(modelId: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* opencode model `variants` for the Command Code model: one entry per supported
|
|
25
|
+
* reasoning effort, keyed by the effort name. Each variant carries
|
|
26
|
+
* `{ reasoningEffort: effort }`, which opencode merges into the request
|
|
27
|
+
* `providerOptions` when the user cycles reasoning with `ctrl+t`. Without a
|
|
28
|
+
* non-empty `variants` map (and `capabilities.reasoning: true`), opencode has
|
|
29
|
+
* nothing to cycle and the effort is never surfaced.
|
|
30
|
+
*/
|
|
31
|
+
export declare function reasoningVariantsForModel(modelId: string): Record<string, {
|
|
32
|
+
reasoningEffort: string;
|
|
33
|
+
}> | undefined;
|
|
23
34
|
export declare function mappedReasoningEffort(model: {
|
|
24
35
|
reasoning: boolean;
|
|
25
36
|
thinkingLevelMap?: Partial<Record<PiThinkingLevel, string | null>>;
|
|
@@ -65,6 +65,20 @@ export function thinkingMetadataForModel(modelId) {
|
|
|
65
65
|
export function isReasoningModel(modelId) {
|
|
66
66
|
return MODEL_EFFORTS[modelId] !== undefined;
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* opencode model `variants` for the Command Code model: one entry per supported
|
|
70
|
+
* reasoning effort, keyed by the effort name. Each variant carries
|
|
71
|
+
* `{ reasoningEffort: effort }`, which opencode merges into the request
|
|
72
|
+
* `providerOptions` when the user cycles reasoning with `ctrl+t`. Without a
|
|
73
|
+
* non-empty `variants` map (and `capabilities.reasoning: true`), opencode has
|
|
74
|
+
* nothing to cycle and the effort is never surfaced.
|
|
75
|
+
*/
|
|
76
|
+
export function reasoningVariantsForModel(modelId) {
|
|
77
|
+
const efforts = MODEL_EFFORTS[modelId];
|
|
78
|
+
if (!efforts)
|
|
79
|
+
return undefined;
|
|
80
|
+
return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }]));
|
|
81
|
+
}
|
|
68
82
|
export function mappedReasoningEffort(model, options) {
|
|
69
83
|
const level = options?.reasoning;
|
|
70
84
|
if (!level || level === "off" || !model.reasoning)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-cmd-provider",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Command Code provider + plugin for opencode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
"scripts": {
|
|
18
18
|
"build": "tsc -p tsconfig.json",
|
|
19
19
|
"typecheck": "tsc --noEmit",
|
|
20
|
+
"refresh:snapshot": "node scripts/refresh-snapshot.mjs",
|
|
20
21
|
"test": "npm run typecheck && npm run test:unit && npm run test:integration && npm run test:contract && npm run format:check",
|
|
21
|
-
"test:unit": "tsx tests/env.test.ts && tsx tests/auth-key.test.ts && tsx tests/converters.test.ts && tsx tests/stream.test.ts && tsx tests/redact.test.ts && tsx tests/cost.test.ts && tsx tests/retry.test.ts && tsx tests/reasoning.test.ts && tsx tests/modalities.test.ts && tsx tests/
|
|
22
|
+
"test:unit": "tsx tests/env.test.ts && tsx tests/auth-key.test.ts && tsx tests/converters.test.ts && tsx tests/stream.test.ts && tsx tests/redact.test.ts && tsx tests/cost.test.ts && tsx tests/retry.test.ts && tsx tests/reasoning.test.ts && tsx tests/modalities.test.ts && tsx tests/snapshot.test.ts && tsx tests/refresh-snapshot.test.ts && tsx tests/oauth.test.ts && tsx tests/plugin-models.test.ts",
|
|
22
23
|
"test:integration": "tsx tests/integration-do-stream.test.ts && tsx tests/integration-do-generate.test.ts",
|
|
23
24
|
"test:contract": "tsx tests/contract.test.ts",
|
|
24
25
|
"test:e2e": "node tests/e2e-opencode.mjs",
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
export declare const DEFAULT_MODELS_URL = "https://api.commandcode.ai/provider/v1/models";
|
|
2
|
-
export declare const DEFAULT_MODELS_TIMEOUT_MS = 10000;
|
|
3
|
-
export interface CommandCodeModel {
|
|
4
|
-
id: string;
|
|
5
|
-
name: string;
|
|
6
|
-
reasoning: boolean;
|
|
7
|
-
contextWindow: number;
|
|
8
|
-
maxTokens: number;
|
|
9
|
-
}
|
|
10
|
-
interface FetchCommandCodeModelsOptions {
|
|
11
|
-
url?: string;
|
|
12
|
-
fetchImpl?: typeof fetch;
|
|
13
|
-
signal?: AbortSignal;
|
|
14
|
-
timeoutMs?: number;
|
|
15
|
-
}
|
|
16
|
-
interface LoadCommandCodeModelsOptions extends FetchCommandCodeModelsOptions {
|
|
17
|
-
cachePath: string;
|
|
18
|
-
}
|
|
19
|
-
export interface LoadCommandCodeModelsResult {
|
|
20
|
-
models: readonly CommandCodeModel[];
|
|
21
|
-
source: "live" | "cache" | "empty";
|
|
22
|
-
warning?: string;
|
|
23
|
-
}
|
|
24
|
-
export declare function getModelsTimeoutMs(env?: NodeJS.ProcessEnv): number;
|
|
25
|
-
export declare function commandCodeModelsFromApiResponse(value: unknown): readonly CommandCodeModel[];
|
|
26
|
-
export declare function commandCodeModelsFromCache(value: unknown): readonly CommandCodeModel[];
|
|
27
|
-
export declare function fetchCommandCodeModels(options?: FetchCommandCodeModelsOptions): Promise<readonly CommandCodeModel[]>;
|
|
28
|
-
export declare function loadCommandCodeModels(options: LoadCommandCodeModelsOptions): Promise<LoadCommandCodeModelsResult>;
|
|
29
|
-
export {};
|
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
// src/provider/models.ts — model catalog fetch, parse, cache with offline
|
|
2
|
-
// fallback (PLAN #7, port of pi's models.ts)
|
|
3
|
-
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
-
import { dirname } from "node:path";
|
|
5
|
-
import { isReasoningModel } from "./reasoning.js";
|
|
6
|
-
export const DEFAULT_MODELS_URL = "https://api.commandcode.ai/provider/v1/models";
|
|
7
|
-
export const DEFAULT_MODELS_TIMEOUT_MS = 10_000;
|
|
8
|
-
const DEFAULT_MAX_OUTPUT_TOKENS = 65_536;
|
|
9
|
-
const MODEL_CACHE_VERSION = 1;
|
|
10
|
-
function isRecord(value) {
|
|
11
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
|
-
}
|
|
13
|
-
function stringField(record, key) {
|
|
14
|
-
const value = record[key];
|
|
15
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
16
|
-
throw new Error(`Expected ${key} to be a non-empty string`);
|
|
17
|
-
}
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
|
-
function booleanField(record, key) {
|
|
21
|
-
const value = record[key];
|
|
22
|
-
if (typeof value !== "boolean")
|
|
23
|
-
throw new Error(`Expected ${key} to be a boolean`);
|
|
24
|
-
return value;
|
|
25
|
-
}
|
|
26
|
-
function positiveNumberField(record, key) {
|
|
27
|
-
const value = record[key];
|
|
28
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
29
|
-
throw new Error(`Expected ${key} to be a positive number`);
|
|
30
|
-
}
|
|
31
|
-
return value;
|
|
32
|
-
}
|
|
33
|
-
function parseApiModel(value) {
|
|
34
|
-
if (!isRecord(value))
|
|
35
|
-
throw new Error("Expected model entry to be an object");
|
|
36
|
-
return {
|
|
37
|
-
id: stringField(value, "id"),
|
|
38
|
-
name: stringField(value, "name"),
|
|
39
|
-
contextLength: positiveNumberField(value, "context_length"),
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
function parseCachedModel(value) {
|
|
43
|
-
if (!isRecord(value))
|
|
44
|
-
throw new Error("Expected cached model entry to be an object");
|
|
45
|
-
const id = stringField(value, "id");
|
|
46
|
-
booleanField(value, "reasoning");
|
|
47
|
-
return {
|
|
48
|
-
id,
|
|
49
|
-
name: stringField(value, "name"),
|
|
50
|
-
reasoning: isReasoningModel(id),
|
|
51
|
-
contextWindow: positiveNumberField(value, "contextWindow"),
|
|
52
|
-
maxTokens: positiveNumberField(value, "maxTokens"),
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
function requireModels(models) {
|
|
56
|
-
if (models.length === 0)
|
|
57
|
-
throw new Error("Command Code returned an empty model catalog");
|
|
58
|
-
return models;
|
|
59
|
-
}
|
|
60
|
-
function errorMessage(error) {
|
|
61
|
-
return error instanceof Error ? error.message : String(error);
|
|
62
|
-
}
|
|
63
|
-
function abortError(reason) {
|
|
64
|
-
if (reason instanceof Error)
|
|
65
|
-
return reason;
|
|
66
|
-
return new DOMException("The operation was aborted", "AbortError");
|
|
67
|
-
}
|
|
68
|
-
function configuredTimeoutMs(timeoutMs) {
|
|
69
|
-
return timeoutMs !== undefined && Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
70
|
-
? timeoutMs
|
|
71
|
-
: DEFAULT_MODELS_TIMEOUT_MS;
|
|
72
|
-
}
|
|
73
|
-
export function getModelsTimeoutMs(env = process.env) {
|
|
74
|
-
const raw = env.COMMANDCODE_MODELS_TIMEOUT_MS;
|
|
75
|
-
if (!raw)
|
|
76
|
-
return DEFAULT_MODELS_TIMEOUT_MS;
|
|
77
|
-
const parsed = Number(raw);
|
|
78
|
-
return configuredTimeoutMs(parsed);
|
|
79
|
-
}
|
|
80
|
-
class ModelDiscoveryTimeoutError extends Error {
|
|
81
|
-
constructor(timeoutMs) {
|
|
82
|
-
super(`Command Code model discovery timed out after ${timeoutMs}ms`);
|
|
83
|
-
this.name = "ModelDiscoveryTimeoutError";
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
function runWithTimeout(operation, timeoutMs, externalSignal) {
|
|
87
|
-
const controller = new AbortController();
|
|
88
|
-
let timer;
|
|
89
|
-
let settled = false;
|
|
90
|
-
let onExternalAbort;
|
|
91
|
-
return new Promise((resolve, reject) => {
|
|
92
|
-
const cleanup = () => {
|
|
93
|
-
if (timer !== undefined)
|
|
94
|
-
clearTimeout(timer);
|
|
95
|
-
if (onExternalAbort && externalSignal) {
|
|
96
|
-
externalSignal.removeEventListener("abort", onExternalAbort);
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
const resolveOnce = (value) => {
|
|
100
|
-
if (settled)
|
|
101
|
-
return;
|
|
102
|
-
settled = true;
|
|
103
|
-
cleanup();
|
|
104
|
-
resolve(value);
|
|
105
|
-
};
|
|
106
|
-
const rejectOnce = (error) => {
|
|
107
|
-
if (settled)
|
|
108
|
-
return;
|
|
109
|
-
settled = true;
|
|
110
|
-
cleanup();
|
|
111
|
-
reject(error);
|
|
112
|
-
};
|
|
113
|
-
const abort = (reason) => {
|
|
114
|
-
const error = abortError(reason);
|
|
115
|
-
controller.abort(error);
|
|
116
|
-
rejectOnce(error);
|
|
117
|
-
};
|
|
118
|
-
if (externalSignal?.aborted) {
|
|
119
|
-
abort(externalSignal.reason);
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
onExternalAbort = () => abort(externalSignal?.reason);
|
|
123
|
-
externalSignal?.addEventListener("abort", onExternalAbort, { once: true });
|
|
124
|
-
timer = setTimeout(() => abort(new ModelDiscoveryTimeoutError(timeoutMs)), timeoutMs);
|
|
125
|
-
Promise.resolve()
|
|
126
|
-
.then(() => operation(controller.signal))
|
|
127
|
-
.then(resolveOnce, rejectOnce);
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
export function commandCodeModelsFromApiResponse(value) {
|
|
131
|
-
if (!isRecord(value))
|
|
132
|
-
throw new Error("Expected models response to be an object");
|
|
133
|
-
if (value.object !== "list")
|
|
134
|
-
throw new Error("Expected models response object to be 'list'");
|
|
135
|
-
const data = value.data;
|
|
136
|
-
if (!Array.isArray(data))
|
|
137
|
-
throw new Error("Expected models response data to be an array");
|
|
138
|
-
return data.map(parseApiModel).map((model) => ({
|
|
139
|
-
id: model.id,
|
|
140
|
-
name: `${model.name} (CC)`,
|
|
141
|
-
reasoning: isReasoningModel(model.id),
|
|
142
|
-
contextWindow: model.contextLength,
|
|
143
|
-
maxTokens: Math.min(model.contextLength, DEFAULT_MAX_OUTPUT_TOKENS),
|
|
144
|
-
}));
|
|
145
|
-
}
|
|
146
|
-
export function commandCodeModelsFromCache(value) {
|
|
147
|
-
if (!isRecord(value))
|
|
148
|
-
throw new Error("Expected model cache to be an object");
|
|
149
|
-
if (value.version !== MODEL_CACHE_VERSION) {
|
|
150
|
-
throw new Error(`Expected model cache version ${MODEL_CACHE_VERSION}`);
|
|
151
|
-
}
|
|
152
|
-
if (!Array.isArray(value.models))
|
|
153
|
-
throw new Error("Expected cached models to be an array");
|
|
154
|
-
return requireModels(value.models.map(parseCachedModel));
|
|
155
|
-
}
|
|
156
|
-
export async function fetchCommandCodeModels(options = {}) {
|
|
157
|
-
const url = options.url ?? DEFAULT_MODELS_URL;
|
|
158
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
159
|
-
const body = await runWithTimeout(async (signal) => {
|
|
160
|
-
const response = await fetchImpl(url, {
|
|
161
|
-
headers: {
|
|
162
|
-
accept: "application/json",
|
|
163
|
-
},
|
|
164
|
-
signal,
|
|
165
|
-
});
|
|
166
|
-
if (!response.ok) {
|
|
167
|
-
throw new Error(`Failed to fetch Command Code models: ${response.status} ${response.statusText}`);
|
|
168
|
-
}
|
|
169
|
-
return await response.json();
|
|
170
|
-
}, configuredTimeoutMs(options.timeoutMs), options.signal);
|
|
171
|
-
return requireModels(commandCodeModelsFromApiResponse(body));
|
|
172
|
-
}
|
|
173
|
-
async function readCommandCodeModelsCache(cachePath) {
|
|
174
|
-
const contents = await readFile(cachePath, "utf-8");
|
|
175
|
-
const parsed = JSON.parse(contents);
|
|
176
|
-
return commandCodeModelsFromCache(parsed);
|
|
177
|
-
}
|
|
178
|
-
async function writeCommandCodeModelsCache(cachePath, models) {
|
|
179
|
-
await mkdir(dirname(cachePath), { recursive: true });
|
|
180
|
-
const temporaryPath = `${cachePath}.${process.pid}.tmp`;
|
|
181
|
-
try {
|
|
182
|
-
await writeFile(temporaryPath, `${JSON.stringify({ version: MODEL_CACHE_VERSION, models }, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
|
|
183
|
-
await rename(temporaryPath, cachePath);
|
|
184
|
-
}
|
|
185
|
-
finally {
|
|
186
|
-
try {
|
|
187
|
-
await rm(temporaryPath, { force: true });
|
|
188
|
-
}
|
|
189
|
-
catch {
|
|
190
|
-
// Best-effort cleanup must not hide the original cache write error.
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
export async function loadCommandCodeModels(options) {
|
|
195
|
-
const cachePath = options.cachePath;
|
|
196
|
-
try {
|
|
197
|
-
const models = await fetchCommandCodeModels(options);
|
|
198
|
-
try {
|
|
199
|
-
await writeCommandCodeModelsCache(cachePath, models);
|
|
200
|
-
return { models, source: "live" };
|
|
201
|
-
}
|
|
202
|
-
catch (error) {
|
|
203
|
-
return {
|
|
204
|
-
models,
|
|
205
|
-
source: "live",
|
|
206
|
-
warning: `Loaded the live Command Code model catalog but could not update ${cachePath}: ${errorMessage(error)}`,
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
catch (liveError) {
|
|
211
|
-
if (options.signal?.aborted)
|
|
212
|
-
throw abortError(options.signal.reason ?? liveError);
|
|
213
|
-
try {
|
|
214
|
-
const models = await readCommandCodeModelsCache(cachePath);
|
|
215
|
-
return {
|
|
216
|
-
models,
|
|
217
|
-
source: "cache",
|
|
218
|
-
warning: `Could not refresh the Command Code model catalog (${errorMessage(liveError)}). Using the cached catalog from ${cachePath}.`,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
catch (cacheError) {
|
|
222
|
-
return {
|
|
223
|
-
models: [],
|
|
224
|
-
source: "empty",
|
|
225
|
-
warning: `Could not refresh the Command Code model catalog (${errorMessage(liveError)}), and no valid cached catalog is available at ${cachePath} (${errorMessage(cacheError)}). Command Code models will remain unavailable until /commandcode-refresh succeeds.`,
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|