opencode-cmd-provider 0.1.0 → 0.1.2
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 +20 -1
- package/README.md +16 -8
- package/dist/src/provider/command-code-model.js +2 -3
- package/dist/src/provider/converters.js +33 -13
- package/dist/src/provider/reasoning.d.ts +9 -0
- package/dist/src/provider/reasoning.js +26 -0
- package/dist/src/provider/stream.js +16 -2
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.2 - 2026-08-15
|
|
4
|
+
|
|
5
|
+
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:
|
|
6
|
+
|
|
7
|
+
- Assistant tool-call parts carry arguments under `input`, not `args`/`arguments` — the converter read the legacy fields, so every prior tool call was re-sent with empty `{}` arguments, corrupting multi-turn context.
|
|
8
|
+
- Tool-result outputs use the v3 `{ type: "text" | "error-text" | "json" | "content", ... }` shapes — `resultText` stringified the wrapper, double-encoding results in history. Replaced with `unwrapToolResult`.
|
|
9
|
+
- `reasoning_effort` was silently dropped: opencode passes `providerOptions` namespaced per provider (`{ commandcode: { reasoningEffort } }`), but the model read the top-level keys. Added `resolveProviderReasoning` (namespaced + top-level fallback) so the configured thinking level actually reaches the API.
|
|
10
|
+
|
|
11
|
+
Also: remove duplicated README disclaimer; bump `@ai-sdk/provider` to 4.x and `@types/node` to 26.x.
|
|
12
|
+
|
|
13
|
+
## 0.1.1 - 2026-08-15
|
|
14
|
+
|
|
15
|
+
Fix: emit `text-start`/`text-end` and `reasoning-start`/`reasoning-end` stream parts. The live Command Code API sends start/end events with ids; the AI SDK's streamText consumer requires them before deltas, so reasoning-capable models (e.g. `deepseek/deepseek-v4-flash`) failed with "reasoning part <id> not found".
|
|
16
|
+
|
|
3
17
|
## 0.1.0 - 2026-08-15
|
|
4
18
|
|
|
5
|
-
Initial release: Command Code provider + plugin for opencode.
|
|
19
|
+
Initial release: Command Code provider + plugin for opencode. Published to npm as `opencode-cmd-provider@0.1.0` (`latest`), tagged `v0.1.0`.
|
|
20
|
+
|
|
21
|
+
- `createCommandCode(options)` AI SDK provider (LanguageModelV3) + `default` V1 opencode plugin module in one package.
|
|
22
|
+
- `/connect` browser auth flow, `COMMANDCODE_API_KEY` env var, and legacy auth file support (`~/.commandcode/auth.json`, `~/.omp/agent/auth.json`, `~/.pi/agent/auth.json`).
|
|
23
|
+
- Model catalog fetch, parse, and cache with offline fallback; pricing/cost display; image input; reasoning effort.
|
|
24
|
+
- Config-declared `models` map required under `provider.commandcode` until `commandcode` lands in opencode's models.dev catalog.
|
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# opencode-cmd-provider
|
|
2
2
|
|
|
3
3
|
[](https://github.com/rashidrazak/opencode-cmd-provider/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/opencode-cmd-provider)
|
|
4
5
|
|
|
5
6
|
A provider and plugin for [opencode](https://opencode.ai) that connects to the [Command Code](https://commandcode.ai) Provider API.
|
|
6
7
|
|
|
@@ -25,20 +26,27 @@ Add the package to your opencode configuration:
|
|
|
25
26
|
"name": "Claude Sonnet 5",
|
|
26
27
|
"limit": { "context": 200000, "output": 65536 },
|
|
27
28
|
},
|
|
29
|
+
"deepseek/deepseek-v4-flash": {
|
|
30
|
+
"name": "DeepSeek V4 Flash",
|
|
31
|
+
},
|
|
28
32
|
},
|
|
29
33
|
},
|
|
30
34
|
},
|
|
31
35
|
}
|
|
32
36
|
```
|
|
33
37
|
|
|
34
|
-
> **The `models` map is required
|
|
35
|
-
>
|
|
36
|
-
>
|
|
37
|
-
>
|
|
38
|
-
>
|
|
39
|
-
>
|
|
40
|
-
>
|
|
41
|
-
>
|
|
38
|
+
> **The `models` map is required, and each key must be the model's full Command
|
|
39
|
+
> Code catalog ID.** opencode only fires a provider's `models` discovery hook for
|
|
40
|
+
> providers already in its [models.dev](https://models.dev) catalog, and
|
|
41
|
+
> `commandcode` is not in that catalog yet — so you must declare the models you
|
|
42
|
+
> want. The keys are passed straight to the Command Code API, so use the exact
|
|
43
|
+
> catalog IDs (some are prefixed, e.g. `deepseek/deepseek-v4-flash`,
|
|
44
|
+
> `google/gemini-3.5-flash`, `xai/grok-4.5`). List the current catalog from the
|
|
45
|
+
> terminal with `opencode run --model commandcode/... "hi"` after connecting, or
|
|
46
|
+
> hit `https://api.commandcode.ai/provider/v1/models`. An unprefixed or wrong ID
|
|
47
|
+
> fails with a `403 Model/provider not recognized` error. When `commandcode`
|
|
48
|
+
> lands in the models.dev catalog, this map becomes optional and discovery works
|
|
49
|
+
> via the plugin's `provider.models` hook.
|
|
42
50
|
|
|
43
51
|
Start or reload opencode, then authenticate:
|
|
44
52
|
|
|
@@ -11,7 +11,7 @@ import { parseStreamEventLine, ccEventToStreamPart } from "./stream.js";
|
|
|
11
11
|
import { redactCommandCodeErrorText, commandCodeErrorMessage } from "./redact.js";
|
|
12
12
|
import { calculateCommandCodeCost } from "./cost.js";
|
|
13
13
|
import { ZERO_MODEL_COST, MODEL_COSTS } from "./pricing.js";
|
|
14
|
-
import { mappedReasoningEffort, thinkingMetadataForModel, isReasoningModel } from "./reasoning.js";
|
|
14
|
+
import { mappedReasoningEffort, resolveProviderReasoning, thinkingMetadataForModel, isReasoningModel, } from "./reasoning.js";
|
|
15
15
|
import { modelSupportsImageInput } from "./modalities.js";
|
|
16
16
|
import { isRetryableStatus, retryDelayMs, raceAbort, abortError, timeoutError, delay, } from "./retry.js";
|
|
17
17
|
import { projectSlugFromPath } from "./project-slug.js";
|
|
@@ -138,8 +138,7 @@ export class CommandCodeLanguageModel {
|
|
|
138
138
|
reasoning: isReasoningModel(this.modelId),
|
|
139
139
|
thinkingLevelMap: thinkingMetadataForModel(this.modelId)?.thinkingLevelMap,
|
|
140
140
|
}, {
|
|
141
|
-
reasoning: options.providerOptions
|
|
142
|
-
options.providerOptions?.reasoningEffort,
|
|
141
|
+
reasoning: resolveProviderReasoning(options.providerOptions, "commandcode"),
|
|
143
142
|
});
|
|
144
143
|
const maxTokens = Math.min(options.maxOutputTokens ?? DEFAULT_GENERATE_MAX_TOKENS, DEFAULT_GENERATE_MAX_TOKENS);
|
|
145
144
|
const allowImages = modelSupportsImageInput(this.modelId);
|
|
@@ -140,21 +140,41 @@ function completeToolCallIds(messages) {
|
|
|
140
140
|
}
|
|
141
141
|
return new Set([...callIds].filter((id) => resultIds.has(id)));
|
|
142
142
|
}
|
|
143
|
-
function
|
|
143
|
+
function unwrapToolResult(result) {
|
|
144
144
|
if (typeof result === "string")
|
|
145
145
|
return result;
|
|
146
146
|
if (Array.isArray(result))
|
|
147
|
-
return result.map(
|
|
148
|
-
if (result
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
return result.map(unwrapToolResult).filter(Boolean).join("\n");
|
|
148
|
+
if (!isRecord(result))
|
|
149
|
+
return String(result ?? "");
|
|
150
|
+
switch (result.type) {
|
|
151
|
+
case "text":
|
|
152
|
+
case "error-text":
|
|
153
|
+
return stringValue(result.value) ?? "";
|
|
154
|
+
case "json":
|
|
155
|
+
case "error-json":
|
|
156
|
+
return JSON.stringify(result.value);
|
|
157
|
+
case "execution-denied":
|
|
158
|
+
return stringValue(result.reason) ?? "Tool execution denied.";
|
|
159
|
+
case "content": {
|
|
160
|
+
const text = Array.isArray(result.value)
|
|
161
|
+
? result.value
|
|
162
|
+
.map((entry) => isRecord(entry) && entry.type === "text" ? (stringValue(entry.text) ?? "") : "")
|
|
163
|
+
.filter(Boolean)
|
|
164
|
+
.join("\n")
|
|
165
|
+
: "";
|
|
151
166
|
return text;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
167
|
+
}
|
|
168
|
+
default: {
|
|
169
|
+
const text = stringValue(result.text);
|
|
170
|
+
if (text !== undefined)
|
|
171
|
+
return text;
|
|
172
|
+
const content = result.content;
|
|
173
|
+
if (content !== undefined)
|
|
174
|
+
return unwrapToolResult(content);
|
|
175
|
+
return JSON.stringify(result);
|
|
176
|
+
}
|
|
156
177
|
}
|
|
157
|
-
return String(result ?? "");
|
|
158
178
|
}
|
|
159
179
|
export function messagesToCC(messages, options = {}) {
|
|
160
180
|
const allowImages = options.allowImages ?? false;
|
|
@@ -183,7 +203,7 @@ export function messagesToCC(messages, options = {}) {
|
|
|
183
203
|
type: "tool-call",
|
|
184
204
|
toolCallId,
|
|
185
205
|
toolName: stringValue(content.toolName) ?? "",
|
|
186
|
-
input: recordOrEmpty(content.args ?? content.arguments),
|
|
206
|
+
input: recordOrEmpty(content.input ?? content.args ?? content.arguments),
|
|
187
207
|
});
|
|
188
208
|
}
|
|
189
209
|
}
|
|
@@ -203,8 +223,8 @@ export function messagesToCC(messages, options = {}) {
|
|
|
203
223
|
toolCallId,
|
|
204
224
|
toolName: stringValue(content.toolName) ?? "",
|
|
205
225
|
output: content.isError
|
|
206
|
-
? { type: "error-text", value:
|
|
207
|
-
: { type: "text", value:
|
|
226
|
+
? { type: "error-text", value: unwrapToolResult(content.result ?? content.output) }
|
|
227
|
+
: { type: "text", value: unwrapToolResult(content.result ?? content.output) },
|
|
208
228
|
},
|
|
209
229
|
],
|
|
210
230
|
});
|
|
@@ -26,4 +26,13 @@ export declare function mappedReasoningEffort(model: {
|
|
|
26
26
|
}, options?: {
|
|
27
27
|
reasoning?: string;
|
|
28
28
|
}): string | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Resolves the user's requested reasoning effort from AI SDK v3 call
|
|
31
|
+
* providerOptions. The v3 shape namespaces options per provider
|
|
32
|
+
* (`{ commandcode: { reasoningEffort } }`), but some runtimes pass the
|
|
33
|
+
* effort at the top level — accept both. Without this, opencode's
|
|
34
|
+
* `reasoningEffort` agent setting is silently dropped and the API falls
|
|
35
|
+
* back to its default reasoning depth.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveProviderReasoning(providerOptions: unknown, providerId: string): string | undefined;
|
|
29
38
|
export {};
|
|
@@ -72,3 +72,29 @@ export function mappedReasoningEffort(model, options) {
|
|
|
72
72
|
const mapped = model.thinkingLevelMap?.[level];
|
|
73
73
|
return typeof mapped === "string" && mapped !== "off" ? mapped : undefined;
|
|
74
74
|
}
|
|
75
|
+
function isRecord(value) {
|
|
76
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
77
|
+
}
|
|
78
|
+
function stringValue(value) {
|
|
79
|
+
return typeof value === "string" ? value : undefined;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Resolves the user's requested reasoning effort from AI SDK v3 call
|
|
83
|
+
* providerOptions. The v3 shape namespaces options per provider
|
|
84
|
+
* (`{ commandcode: { reasoningEffort } }`), but some runtimes pass the
|
|
85
|
+
* effort at the top level — accept both. Without this, opencode's
|
|
86
|
+
* `reasoningEffort` agent setting is silently dropped and the API falls
|
|
87
|
+
* back to its default reasoning depth.
|
|
88
|
+
*/
|
|
89
|
+
export function resolveProviderReasoning(providerOptions, providerId) {
|
|
90
|
+
if (!isRecord(providerOptions))
|
|
91
|
+
return undefined;
|
|
92
|
+
const topLevel = stringValue(providerOptions.reasoning) ?? stringValue(providerOptions.reasoningEffort);
|
|
93
|
+
if (topLevel)
|
|
94
|
+
return topLevel;
|
|
95
|
+
const namespaced = providerOptions[providerId];
|
|
96
|
+
if (isRecord(namespaced)) {
|
|
97
|
+
return stringValue(namespaced.reasoning) ?? stringValue(namespaced.reasoningEffort);
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
@@ -56,16 +56,30 @@ export function ccEventToStreamPart(event) {
|
|
|
56
56
|
if (!isRecord(event))
|
|
57
57
|
return [];
|
|
58
58
|
switch (event.type) {
|
|
59
|
+
case "text-start": {
|
|
60
|
+
const id = toolCallIdOf(event) || "text";
|
|
61
|
+
return [{ type: "text-start", id }];
|
|
62
|
+
}
|
|
59
63
|
case "text-delta": {
|
|
60
64
|
const id = toolCallIdOf(event) || "text";
|
|
61
65
|
return [{ type: "text-delta", id, delta: stringValue(event.text) ?? "" }];
|
|
62
66
|
}
|
|
67
|
+
case "text-end": {
|
|
68
|
+
const id = toolCallIdOf(event) || "text";
|
|
69
|
+
return [{ type: "text-end", id }];
|
|
70
|
+
}
|
|
71
|
+
case "reasoning-start": {
|
|
72
|
+
const id = toolCallIdOf(event) || "reasoning";
|
|
73
|
+
return [{ type: "reasoning-start", id }];
|
|
74
|
+
}
|
|
63
75
|
case "reasoning-delta": {
|
|
64
76
|
const id = toolCallIdOf(event) || "reasoning";
|
|
65
77
|
return [{ type: "reasoning-delta", id, delta: stringValue(event.text) ?? "" }];
|
|
66
78
|
}
|
|
67
|
-
case "reasoning-
|
|
68
|
-
|
|
79
|
+
case "reasoning-end": {
|
|
80
|
+
const id = toolCallIdOf(event) || "reasoning";
|
|
81
|
+
return [{ type: "reasoning-end", id }];
|
|
82
|
+
}
|
|
69
83
|
case "tool-result":
|
|
70
84
|
return [];
|
|
71
85
|
case "tool-call": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-cmd-provider",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Command Code provider + plugin for opencode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -38,11 +38,11 @@
|
|
|
38
38
|
"url": "git+https://github.com/rashidrazak/opencode-cmd-provider.git"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@ai-sdk/provider": "^
|
|
41
|
+
"@ai-sdk/provider": "^4.0.7"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@opencode-ai/plugin": "*",
|
|
45
|
-
"@types/node": "^
|
|
45
|
+
"@types/node": "^26.2.0",
|
|
46
46
|
"prettier": "^3.9.6",
|
|
47
47
|
"tsx": "^4.23.12",
|
|
48
48
|
"typescript": "^7.0.2"
|