@darthph0enix/commandcode-go-opencode-provider 0.5.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 ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@darthph0enix/commandcode-go-opencode-provider",
3
+ "version": "0.5.0",
4
+ "author": "Brent Weatherall",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/Darthph0enix7/opencode-commandcode-provider.git"
8
+ },
9
+ "main": "index.ts",
10
+ "devDependencies": {
11
+ "@types/node": "^25.9.1",
12
+ "ai": "6.0.191",
13
+ "typescript": "^6.0.3"
14
+ },
15
+ "exports": {
16
+ ".": {
17
+ "import": "./index.ts"
18
+ },
19
+ "./server": {
20
+ "import": "./plugin.ts"
21
+ }
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/Darthph0enix7/opencode-commandcode-provider/issues"
25
+ },
26
+ "description": "Command Code API provider for opencode — use Claude, GPT, Gemini, DeepSeek, Qwen, Kimi, GLM, MiniMax, and Step models via Command Code",
27
+ "engines": { "bun": ">=1.0.0" },
28
+ "files": ["index.ts", "plugin.ts", "models.json", "src/"],
29
+ "homepage": "https://github.com/Darthph0enix7/opencode-commandcode-provider#readme",
30
+ "keywords": [
31
+ "opencode",
32
+ "opencode-provider",
33
+ "opencode-plugin",
34
+ "commandcode",
35
+ "ai",
36
+ "llm",
37
+ "claude",
38
+ "gpt",
39
+ "gemini",
40
+ "deepseek",
41
+ "qwen",
42
+ "kimi",
43
+ "glm",
44
+ "minimax"
45
+ ],
46
+ "license": "MIT",
47
+ "scripts": {
48
+ "test": "bun test tests/unit/",
49
+ "test:integration": "bun test tests/integration/",
50
+ "test:all": "bun test",
51
+ "typecheck": "tsc --noEmit",
52
+ "generate-readme": "bun run scripts/generate-readme.ts",
53
+ "sync": "bun run scripts/sync-models.ts",
54
+ "sync:global": "bun run scripts/sync-models.ts --update-global"
55
+ },
56
+ "type": "module",
57
+ "types": "index.ts",
58
+ "peerDependencies": {
59
+ "ai": ">=6.0.0"
60
+ }
61
+ }
package/plugin.ts ADDED
@@ -0,0 +1,75 @@
1
+ import { loadSyncedModels } from "./src/model-sync.js"
2
+
3
+ function toConfigKey(id: string): string {
4
+ const slashIdx = id.indexOf("/")
5
+ const short = slashIdx >= 0 ? id.slice(slashIdx + 1) : id
6
+ return short.toLowerCase()
7
+ }
8
+
9
+ export default async function commandcodePlugin() {
10
+ return {
11
+ config: async (config: Record<string, unknown>) => {
12
+ const providers = config.provider as Record<string, Record<string, unknown>> | undefined
13
+ if (!providers) {
14
+ (config as Record<string, unknown>).provider = { commandcode: {} }
15
+ }
16
+ const cc = ((config as Record<string, unknown>).provider as Record<string, Record<string, unknown>>)?.commandcode as Record<string, unknown> | undefined
17
+ if (!cc) return
18
+
19
+ if (!cc.npm) cc.npm = "commandcode-go-opencode-provider"
20
+ if (!cc.name) cc.name = "Command Code"
21
+ if (!cc.env) cc.env = ["COMMANDCODE_API_KEY"]
22
+
23
+ if (!cc.models) {
24
+ const { models } = await loadSyncedModels()
25
+ const modelsObj: Record<string, unknown> = {}
26
+ for (const entry of models) {
27
+ const key = toConfigKey(entry.id)
28
+ const costObj: Record<string, number> = { input: entry.cost.input, output: entry.cost.output }
29
+ if (entry.cost.cache_read !== undefined) costObj.cache_read = entry.cost.cache_read
30
+ if (entry.cost.cache_write !== undefined) costObj.cache_write = entry.cost.cache_write
31
+
32
+ modelsObj[key] = {
33
+ id: entry.id,
34
+ name: entry.name,
35
+ reasoning: entry.reasoning,
36
+ tool_call: entry.tool_call,
37
+ attachment: entry.attachment ?? (entry.modalities?.input?.some((m) => m !== "text") ?? false),
38
+ modalities: entry.modalities ?? { input: ["text"], output: ["text"] },
39
+ variants: entry.variants,
40
+ cost: costObj,
41
+ limit: entry.limit,
42
+ }
43
+ }
44
+ cc.models = modelsObj
45
+ }
46
+ },
47
+
48
+ auth: {
49
+ provider: "commandcode",
50
+ methods: [
51
+ {
52
+ type: "api",
53
+ label: "API Key",
54
+ authorize: async (inputs: Record<string, unknown> | undefined) => {
55
+ const rawKey = inputs?.key
56
+ if (typeof rawKey !== "string") return { type: "failed" as const }
57
+ const key = rawKey.trim()
58
+ if (!key) return { type: "failed" as const }
59
+ return { type: "success" as const, key }
60
+ },
61
+ },
62
+ ],
63
+ loader: async (getAuth: () => Promise<{ type: string; key?: string } | null>) => {
64
+ try {
65
+ const auth = await getAuth()
66
+ if (!auth) return {}
67
+ if (auth.type === "api" && auth.key) return { apiKey: auth.key }
68
+ return {}
69
+ } catch {
70
+ return {}
71
+ }
72
+ },
73
+ },
74
+ }
75
+ }
package/src/auth.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { readFileSync, existsSync } from "fs"
2
+ import { homedir } from "os"
3
+ import { join } from "path"
4
+
5
+ export function resolveApiKey(options: {
6
+ apiKey?: string
7
+ env?: Record<string, string | undefined>
8
+ }): string | undefined {
9
+ if (options.apiKey) return options.apiKey
10
+
11
+ const envKey = options.env?.COMMANDCODE_API_KEY ?? process.env.COMMANDCODE_API_KEY
12
+ if (envKey) return envKey
13
+
14
+ const authPaths = [
15
+ join(homedir(), ".commandcode", "auth.json"),
16
+ join(homedir(), ".pi", "agent", "auth.json"),
17
+ ]
18
+
19
+ for (const p of authPaths) {
20
+ if (!existsSync(p)) continue
21
+ try {
22
+ const parsed = JSON.parse(readFileSync(p, "utf-8"))
23
+ if (typeof parsed === "object" && parsed !== null) {
24
+ if (typeof parsed.apiKey === "string") return parsed.apiKey
25
+ if (typeof parsed.commandcode === "string") return parsed.commandcode
26
+ if (
27
+ typeof parsed.commandcode === "object" &&
28
+ parsed.commandcode !== null &&
29
+ parsed.commandcode.type === "oauth" &&
30
+ typeof parsed.commandcode.access === "string"
31
+ ) {
32
+ return parsed.commandcode.access
33
+ }
34
+ }
35
+ } catch {
36
+ // intentionally silent: skip unreadable or malformed auth files
37
+ continue
38
+ }
39
+ }
40
+
41
+ return undefined
42
+ }
package/src/convert.ts ADDED
@@ -0,0 +1,308 @@
1
+ import type { LanguageModelV3CallOptions } from "@ai-sdk/provider"
2
+ import type {
3
+ LanguageModelV3FunctionTool,
4
+ LanguageModelV3Message,
5
+ LanguageModelV3TextPart,
6
+ LanguageModelV3ReasoningPart,
7
+ LanguageModelV3ToolCallPart,
8
+ LanguageModelV3ToolResultPart,
9
+ LanguageModelV3ToolResultOutput,
10
+ } from "@ai-sdk/provider"
11
+
12
+ type CCMessage =
13
+ | { role: "user"; content: string | unknown[] }
14
+ | { role: "assistant"; content: CCAssistantContent[] }
15
+ | { role: "tool"; content: CCToolResultContent[] }
16
+
17
+ type CCAssistantContent =
18
+ | { type: "text"; text: string }
19
+ | { type: "reasoning"; text: string }
20
+ | { type: "tool-call"; toolCallId: string; toolName: string; input: unknown }
21
+
22
+ type CCToolResultContent = {
23
+ type: "tool-result"
24
+ toolCallId: string
25
+ toolName: string
26
+ output: { type: "text"; value: string } | { type: "error-text"; value: string }
27
+ }
28
+
29
+ type CCTool = {
30
+ type: "function"
31
+ name: string
32
+ description?: string
33
+ input_schema: unknown
34
+ }
35
+
36
+ interface CCRequestEnvelope {
37
+ config: {
38
+ workingDir: string
39
+ date: string
40
+ environment: string
41
+ structure: unknown[]
42
+ isGitRepo: boolean
43
+ currentBranch: string
44
+ mainBranch: string
45
+ gitStatus: string
46
+ recentCommits: unknown[]
47
+ }
48
+ memory: string
49
+ taste: string
50
+ skills: null
51
+ permissionMode: string
52
+ params: {
53
+ model: string
54
+ messages: CCMessage[]
55
+ tools: CCTool[]
56
+ system: string
57
+ max_tokens: number
58
+ stream: true
59
+ temperature?: number
60
+ top_p?: number
61
+ top_k?: number
62
+ reasoning_effort?: string
63
+ }
64
+ }
65
+
66
+ function hasType(p: unknown, type: string): boolean {
67
+ return typeof p === "object" && p !== null && (p as { type?: string }).type === type
68
+ }
69
+
70
+ function isTextPart(p: unknown): p is LanguageModelV3TextPart {
71
+ return hasType(p, "text")
72
+ }
73
+
74
+ function isReasoningPart(p: unknown): p is LanguageModelV3ReasoningPart {
75
+ return hasType(p, "reasoning")
76
+ }
77
+
78
+ function isToolCallPart(p: unknown): p is LanguageModelV3ToolCallPart {
79
+ return hasType(p, "tool-call")
80
+ }
81
+
82
+ function isToolResultPart(p: unknown): p is LanguageModelV3ToolResultPart {
83
+ return hasType(p, "tool-result")
84
+ }
85
+
86
+ function extractText(content: unknown): string {
87
+ if (typeof content === "string") return content
88
+ if (Array.isArray(content)) {
89
+ const textParts = content.filter(isTextPart) as LanguageModelV3TextPart[]
90
+ return textParts.map((p) => p.text).join("\n")
91
+ }
92
+ return ""
93
+ }
94
+
95
+ function toDataUriOrUrl(data: string | Uint8Array | URL, defaultMime: string = "image/png"): string {
96
+ if (typeof data === "string") {
97
+ if (data.startsWith("data:") || data.startsWith("http://") || data.startsWith("https://")) {
98
+ return data
99
+ }
100
+ return `data:${defaultMime};base64,${data}`
101
+ }
102
+ if (data instanceof URL) {
103
+ return data.toString()
104
+ }
105
+ if (data instanceof Uint8Array || (typeof Buffer !== "undefined" && Buffer.isBuffer(data))) {
106
+ const b64 = Buffer.from(data).toString("base64")
107
+ return `data:${defaultMime};base64,${b64}`
108
+ }
109
+ return String(data)
110
+ }
111
+
112
+ function convertUserContent(content: unknown): string | Array<{ type: string; [key: string]: unknown }> {
113
+ if (typeof content === "string") return content
114
+ if (!Array.isArray(content)) return ""
115
+
116
+ const parts: Array<{ type: string; [key: string]: unknown }> = []
117
+ let hasMultimodal = false
118
+
119
+ for (const p of content) {
120
+ if (!p || typeof p !== "object") continue
121
+ const item = p as Record<string, unknown>
122
+
123
+ if (item.type === "text" && typeof item.text === "string") {
124
+ parts.push({ type: "text", text: item.text })
125
+ } else if (item.type === "image" && item.image) {
126
+ hasMultimodal = true
127
+ const mimeType = typeof item.mimeType === "string" ? item.mimeType : "image/png"
128
+ const imgUrl = toDataUriOrUrl(item.image as string | Uint8Array | URL, mimeType)
129
+ parts.push({ type: "image", image: imgUrl, mimeType })
130
+ } else if (item.type === "file" && item.data) {
131
+ const mimeType = typeof item.mimeType === "string" ? item.mimeType : "application/octet-stream"
132
+ if (mimeType.startsWith("image/")) {
133
+ hasMultimodal = true
134
+ const imgUrl = toDataUriOrUrl(item.data as string | Uint8Array | URL, mimeType)
135
+ parts.push({ type: "image", image: imgUrl, mimeType })
136
+ } else if (typeof item.data === "string" && !item.data.startsWith("data:")) {
137
+ parts.push({ type: "text", text: item.data })
138
+ }
139
+ }
140
+ }
141
+
142
+ if (!hasMultimodal) {
143
+ return parts.map((p) => (p.type === "text" ? String(p.text) : "")).join("\n")
144
+ }
145
+ return parts
146
+ }
147
+
148
+ function convertToolResultOutput(output: LanguageModelV3ToolResultOutput): CCToolResultContent["output"] {
149
+ switch (output.type) {
150
+ case "text":
151
+ return { type: "text", value: output.value }
152
+ case "error-text":
153
+ return { type: "error-text", value: output.value }
154
+ case "json":
155
+ return { type: "text", value: JSON.stringify(output.value) }
156
+ case "execution-denied":
157
+ return { type: "error-text", value: output.reason ?? "Execution denied" }
158
+ case "error-json":
159
+ return { type: "error-text", value: JSON.stringify(output.value) }
160
+ case "content":
161
+ return { type: "text", value: output.value.map((v: Record<string, unknown>) => ("text" in v ? v.text : JSON.stringify(v))).join("\n") }
162
+ default:
163
+ return { type: "text", value: JSON.stringify(output) }
164
+ }
165
+ }
166
+
167
+ function convertMessage(msg: LanguageModelV3Message): CCMessage | null {
168
+ switch (msg.role) {
169
+ case "user": {
170
+ const content = convertUserContent(msg.content)
171
+ return { role: "user", content: content as any }
172
+ }
173
+ case "assistant": {
174
+ const parts: CCAssistantContent[] = []
175
+ for (const part of msg.content) {
176
+ if (isTextPart(part)) {
177
+ parts.push({ type: "text", text: part.text })
178
+ } else if (isReasoningPart(part)) {
179
+ parts.push({ type: "reasoning", text: part.text })
180
+ } else if (isToolCallPart(part)) {
181
+ parts.push({
182
+ type: "tool-call",
183
+ toolCallId: part.toolCallId,
184
+ toolName: part.toolName,
185
+ input: part.input,
186
+ })
187
+ }
188
+ }
189
+ return { role: "assistant", content: parts }
190
+ }
191
+ case "tool": {
192
+ const parts: CCToolResultContent[] = []
193
+ for (const part of msg.content) {
194
+ if (isToolResultPart(part)) {
195
+ parts.push({
196
+ type: "tool-result",
197
+ toolCallId: part.toolCallId,
198
+ toolName: part.toolName,
199
+ output: convertToolResultOutput(part.output),
200
+ })
201
+ }
202
+ }
203
+ return { role: "tool", content: parts }
204
+ }
205
+ default:
206
+ return null
207
+ }
208
+ }
209
+
210
+ function convertTools(
211
+ tools: Array<LanguageModelV3FunctionTool | { type: "provider"; id: `${string}.${string}`; name: string; args: Record<string, unknown> }> | undefined,
212
+ ): CCTool[] {
213
+ if (!tools) return []
214
+ return tools
215
+ .filter((t): t is LanguageModelV3FunctionTool => t.type === "function")
216
+ .map((t) => ({
217
+ type: "function" as const,
218
+ name: t.name,
219
+ description: t.description,
220
+ input_schema: t.inputSchema,
221
+ }))
222
+ }
223
+
224
+ function extractReasoningEffort(options: LanguageModelV3CallOptions): string | undefined {
225
+ const opt = options as Record<string, unknown>
226
+ if (typeof opt.reasoning_effort === "string") return opt.reasoning_effort
227
+ if (typeof opt.reasoningEffort === "string") return opt.reasoningEffort
228
+ if (typeof opt.effort === "string") return opt.effort
229
+ if (typeof opt.variant === "string") return opt.variant
230
+
231
+ if (opt.providerOptions && typeof opt.providerOptions === "object") {
232
+ const po = opt.providerOptions as Record<string, unknown>
233
+ for (const key of Object.keys(po)) {
234
+ const val = po[key]
235
+ if (val && typeof val === "object") {
236
+ const sub = val as Record<string, unknown>
237
+ if (typeof sub.reasoning_effort === "string") return sub.reasoning_effort
238
+ if (typeof sub.reasoningEffort === "string") return sub.reasoningEffort
239
+ if (typeof sub.effort === "string") return sub.effort
240
+ if (typeof sub.variant === "string") return sub.variant
241
+ }
242
+ }
243
+ }
244
+
245
+ if (opt.options && typeof opt.options === "object") {
246
+ const sub = opt.options as Record<string, unknown>
247
+ if (typeof sub.reasoning_effort === "string") return sub.reasoning_effort
248
+ if (typeof sub.reasoningEffort === "string") return sub.reasoningEffort
249
+ if (typeof sub.effort === "string") return sub.effort
250
+ if (typeof sub.variant === "string") return sub.variant
251
+ }
252
+
253
+ return undefined
254
+ }
255
+
256
+ export function buildRequest(
257
+ modelId: string,
258
+ options: LanguageModelV3CallOptions,
259
+ ): CCRequestEnvelope {
260
+ let systemPrompt = ""
261
+ const messages: CCMessage[] = []
262
+
263
+ for (const msg of options.prompt) {
264
+ if (msg.role === "system") {
265
+ systemPrompt += (systemPrompt ? "\n\n" : "") + msg.content
266
+ continue
267
+ }
268
+ const converted = convertMessage(msg)
269
+ if (converted) messages.push(converted)
270
+ }
271
+
272
+ const effort = extractReasoningEffort(options)
273
+
274
+ const params: CCRequestEnvelope["params"] = {
275
+ model: modelId,
276
+ messages,
277
+ tools: convertTools(options.tools),
278
+ system: systemPrompt,
279
+ max_tokens: options.maxOutputTokens ?? 16384,
280
+ stream: true,
281
+ ...(effort ? { reasoning_effort: effort } : {}),
282
+ }
283
+
284
+ if (options.temperature !== undefined) params.temperature = options.temperature
285
+ if (options.topP !== undefined) params.top_p = options.topP
286
+ if (options.topK !== undefined) params.top_k = options.topK
287
+
288
+ return {
289
+ config: {
290
+ workingDir: process.cwd() ?? "/",
291
+ date: new Date().toISOString().split("T")[0] ?? "",
292
+ environment: `${process.platform}-${process.arch}`,
293
+ // Stub: opencode does not expose project structure context
294
+ structure: [],
295
+ isGitRepo: false,
296
+ currentBranch: "",
297
+ mainBranch: "",
298
+ gitStatus: "",
299
+ recentCommits: [],
300
+ },
301
+ memory: "",
302
+ // Stub: taste/memory/permissionMode are Command Code CLI features not exposed via provider API
303
+ taste: "",
304
+ skills: null,
305
+ permissionMode: "standard",
306
+ params,
307
+ }
308
+ }