pi-commandcode-provider 0.1.0 → 0.1.1-next.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/README.md +29 -12
- package/index.ts +10 -142
- package/package.json +3 -2
- package/src/converters.ts +17 -7
- package/src/core.ts +42 -9
- package/src/models.ts +85 -0
package/README.md
CHANGED
|
@@ -6,17 +6,17 @@ A [pi](https://github.com/badlogic/pi-mono) custom provider that connects pi to
|
|
|
6
6
|
|
|
7
7
|
> **Note:** This package only provides a model _provider_. It does **not** include an API key. You must bring your own Command Code API key or subscription.
|
|
8
8
|
|
|
9
|
-
> 💰 **Current
|
|
9
|
+
> 💰 **Current offers:** Command Code offers [4× usage of DeepSeek V4 Pro](https://commandcode.ai/docs/resources/pricing-limits#deepseek-v4-pro-4x-usage) and [2× usage of Qwen 3.7 Max](https://commandcode.ai/docs/resources/pricing-limits#qwen-3.7-max-2x-usage).
|
|
10
10
|
|
|
11
11
|
## Models
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Models are fetched live from Command Code's Provider API at startup, so new models like Qwen 3.7 Max show up without a package release.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
You can list the current Command Code models with:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
pi -e index.ts --list-models
|
|
19
|
+
```
|
|
20
20
|
|
|
21
21
|
## Install
|
|
22
22
|
|
|
@@ -50,6 +50,8 @@ In pi, run:
|
|
|
50
50
|
|
|
51
51
|
Then select **Command Code** from the provider list.
|
|
52
52
|
|
|
53
|
+
<img width="1520" height="554" alt="image" src="https://github.com/user-attachments/assets/071e929a-6f49-4803-bfec-7a31368fb12a" />
|
|
54
|
+
|
|
53
55
|
This opens Command Code in your browser and stores the returned API key in pi's auth file. If the browser shows "Copy your API key" because automatic transfer failed, copy that key and paste it into the pi terminal prompt.
|
|
54
56
|
|
|
55
57
|
> Note: `/login commandcode` is not supported by pi currently; use interactive `/login` and select Command Code.
|
|
@@ -70,6 +72,17 @@ Create `~/.commandcode/auth.json`:
|
|
|
70
72
|
}
|
|
71
73
|
```
|
|
72
74
|
|
|
75
|
+
The official Command Code CLI auth shape is also supported:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"command-code": {
|
|
80
|
+
"type": "api",
|
|
81
|
+
"key": "user_..."
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
73
86
|
Or use pi's auth file at `~/.pi/agent/auth.json`:
|
|
74
87
|
|
|
75
88
|
```json
|
|
@@ -86,18 +99,22 @@ After installing and setting your API key, select a Command Code model in pi:
|
|
|
86
99
|
/model deepseek/deepseek-v4-flash
|
|
87
100
|
```
|
|
88
101
|
|
|
89
|
-
Any query will then use the Command Code API. You can list available models:
|
|
102
|
+
Any query will then use the Command Code API. You can list available models within pi:
|
|
90
103
|
|
|
91
|
-
```
|
|
92
|
-
|
|
104
|
+
```txt
|
|
105
|
+
/models
|
|
93
106
|
```
|
|
94
107
|
|
|
95
|
-
|
|
108
|
+
## Model discovery
|
|
109
|
+
|
|
110
|
+
On startup, the provider fetches:
|
|
96
111
|
|
|
97
112
|
```txt
|
|
98
|
-
/models
|
|
113
|
+
https://api.commandcode.ai/provider/v1/models
|
|
99
114
|
```
|
|
100
115
|
|
|
116
|
+
For tests or local mocks, override it with `COMMANDCODE_MODELS_URL`.
|
|
117
|
+
|
|
101
118
|
## Publish
|
|
102
119
|
|
|
103
120
|
```sh
|
package/index.ts
CHANGED
|
@@ -9,152 +9,18 @@
|
|
|
9
9
|
* 3. Place API key in `~/.commandcode/auth.json` or `~/.pi/agent/auth.json`
|
|
10
10
|
* as {"apiKey": "user_..."} or {"commandcode": "user_..."}
|
|
11
11
|
*
|
|
12
|
-
* Models
|
|
12
|
+
* Models are fetched from Command Code's Provider API at startup.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { calculateCost, createAssistantMessageEventStream } from "@mariozechner/pi-ai"
|
|
16
16
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"
|
|
17
17
|
|
|
18
|
-
import { createStreamCommandCode, DEFAULT_API_BASE } from "./src/core.ts"
|
|
18
|
+
import { COMMAND_CODE_CLI_VERSION, createStreamCommandCode, DEFAULT_API_BASE } from "./src/core.ts"
|
|
19
|
+
import { DEFAULT_MODELS_URL, fetchCommandCodeModels } from "./src/models.ts"
|
|
19
20
|
import { getApiKey, login, refreshToken } from "./src/oauth.ts"
|
|
20
21
|
|
|
21
22
|
const API_BASE = process.env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE
|
|
22
|
-
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// Model definitions
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
const MODELS = [
|
|
28
|
-
// Premium (Anthropic)
|
|
29
|
-
{
|
|
30
|
-
id: "claude-opus-4-7",
|
|
31
|
-
name: "Claude Opus 4.7 (CC)",
|
|
32
|
-
reasoning: true,
|
|
33
|
-
contextWindow: 200_000,
|
|
34
|
-
maxTokens: 32_000,
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
id: "claude-opus-4-6",
|
|
38
|
-
name: "Claude Opus 4.6 (CC)",
|
|
39
|
-
reasoning: true,
|
|
40
|
-
contextWindow: 200_000,
|
|
41
|
-
maxTokens: 32_000,
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
id: "claude-sonnet-4-6",
|
|
45
|
-
name: "Claude Sonnet 4.6 (CC)",
|
|
46
|
-
reasoning: true,
|
|
47
|
-
contextWindow: 200_000,
|
|
48
|
-
maxTokens: 16_384,
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
id: "claude-haiku-4-5-20251001",
|
|
52
|
-
name: "Claude Haiku 4.5 (CC)",
|
|
53
|
-
reasoning: true,
|
|
54
|
-
contextWindow: 200_000,
|
|
55
|
-
maxTokens: 8_192,
|
|
56
|
-
},
|
|
57
|
-
// Premium (OpenAI)
|
|
58
|
-
{
|
|
59
|
-
id: "gpt-5.5",
|
|
60
|
-
name: "GPT-5.5 (CC)",
|
|
61
|
-
reasoning: true,
|
|
62
|
-
contextWindow: 256_000,
|
|
63
|
-
maxTokens: 128_000,
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
id: "gpt-5.4",
|
|
67
|
-
name: "GPT-5.4 (CC)",
|
|
68
|
-
reasoning: true,
|
|
69
|
-
contextWindow: 256_000,
|
|
70
|
-
maxTokens: 128_000,
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
id: "gpt-5.3-codex",
|
|
74
|
-
name: "GPT-5.3 Codex (CC)",
|
|
75
|
-
reasoning: true,
|
|
76
|
-
contextWindow: 256_000,
|
|
77
|
-
maxTokens: 128_000,
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
id: "gpt-5.4-mini",
|
|
81
|
-
name: "GPT-5.4 Mini (CC)",
|
|
82
|
-
reasoning: false,
|
|
83
|
-
contextWindow: 256_000,
|
|
84
|
-
maxTokens: 128_000,
|
|
85
|
-
},
|
|
86
|
-
// Open-source
|
|
87
|
-
{
|
|
88
|
-
id: "deepseek/deepseek-v4-pro",
|
|
89
|
-
name: "DeepSeek V4 Pro (CC)",
|
|
90
|
-
reasoning: true,
|
|
91
|
-
contextWindow: 1_000_000,
|
|
92
|
-
maxTokens: 384_000,
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
id: "deepseek/deepseek-v4-flash",
|
|
96
|
-
name: "DeepSeek V4 Flash (CC)",
|
|
97
|
-
reasoning: true,
|
|
98
|
-
contextWindow: 1_000_000,
|
|
99
|
-
maxTokens: 384_000,
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
id: "moonshotai/Kimi-K2.6",
|
|
103
|
-
name: "Kimi K2.6 (CC)",
|
|
104
|
-
reasoning: true,
|
|
105
|
-
contextWindow: 262_144,
|
|
106
|
-
maxTokens: 131_072,
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
id: "moonshotai/Kimi-K2.5",
|
|
110
|
-
name: "Kimi K2.5 (CC)",
|
|
111
|
-
reasoning: true,
|
|
112
|
-
contextWindow: 262_144,
|
|
113
|
-
maxTokens: 131_072,
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
id: "zai-org/GLM-5.1",
|
|
117
|
-
name: "GLM-5.1 (CC)",
|
|
118
|
-
reasoning: true,
|
|
119
|
-
contextWindow: 200_000,
|
|
120
|
-
maxTokens: 131_072,
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
id: "zai-org/GLM-5",
|
|
124
|
-
name: "GLM-5 (CC)",
|
|
125
|
-
reasoning: true,
|
|
126
|
-
contextWindow: 200_000,
|
|
127
|
-
maxTokens: 131_072,
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
id: "MiniMaxAI/MiniMax-M2.7",
|
|
131
|
-
name: "MiniMax M2.7 (CC)",
|
|
132
|
-
reasoning: true,
|
|
133
|
-
contextWindow: 1_048_576,
|
|
134
|
-
maxTokens: 131_072,
|
|
135
|
-
},
|
|
136
|
-
{
|
|
137
|
-
id: "MiniMaxAI/MiniMax-M2.5",
|
|
138
|
-
name: "MiniMax M2.5 (CC)",
|
|
139
|
-
reasoning: true,
|
|
140
|
-
contextWindow: 1_048_576,
|
|
141
|
-
maxTokens: 131_072,
|
|
142
|
-
},
|
|
143
|
-
{
|
|
144
|
-
id: "Qwen/Qwen3.6-Max-Preview",
|
|
145
|
-
name: "Qwen 3.6 Max (CC)",
|
|
146
|
-
reasoning: true,
|
|
147
|
-
contextWindow: 1_000_000,
|
|
148
|
-
maxTokens: 131_072,
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
id: "Qwen/Qwen3.6-Plus",
|
|
152
|
-
name: "Qwen 3.6 Plus (CC)",
|
|
153
|
-
reasoning: true,
|
|
154
|
-
contextWindow: 1_000_000,
|
|
155
|
-
maxTokens: 131_072,
|
|
156
|
-
},
|
|
157
|
-
]
|
|
23
|
+
const MODELS_URL = process.env.COMMANDCODE_MODELS_URL ?? DEFAULT_MODELS_URL
|
|
158
24
|
|
|
159
25
|
const streamCommandCode = createStreamCommandCode({
|
|
160
26
|
createStream: createAssistantMessageEventStream,
|
|
@@ -166,7 +32,9 @@ const streamCommandCode = createStreamCommandCode({
|
|
|
166
32
|
// Extension entry point
|
|
167
33
|
// ---------------------------------------------------------------------------
|
|
168
34
|
|
|
169
|
-
export default function (pi: ExtensionAPI) {
|
|
35
|
+
export default async function (pi: ExtensionAPI) {
|
|
36
|
+
const models = await fetchCommandCodeModels({ url: MODELS_URL })
|
|
37
|
+
|
|
170
38
|
pi.registerProvider("commandcode", {
|
|
171
39
|
name: "Command Code",
|
|
172
40
|
baseUrl: API_BASE,
|
|
@@ -175,7 +43,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
175
43
|
api: "commandcode-custom",
|
|
176
44
|
streamSimple: streamCommandCode,
|
|
177
45
|
headers: {
|
|
178
|
-
"x-command-code-version":
|
|
46
|
+
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
|
|
179
47
|
"x-cli-environment": "production",
|
|
180
48
|
},
|
|
181
49
|
oauth: {
|
|
@@ -184,11 +52,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
184
52
|
refreshToken,
|
|
185
53
|
getApiKey,
|
|
186
54
|
},
|
|
187
|
-
models:
|
|
55
|
+
models: models.map((model) => ({
|
|
188
56
|
id: model.id,
|
|
189
57
|
name: model.name,
|
|
190
58
|
reasoning: model.reasoning,
|
|
191
|
-
input: ["text"],
|
|
59
|
+
input: ["text"] as const,
|
|
192
60
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
193
61
|
contextWindow: model.contextWindow,
|
|
194
62
|
maxTokens: model.maxTokens,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-commandcode-provider",
|
|
3
|
-
"version": "0.1.0",
|
|
3
|
+
"version": "0.1.1-next.0",
|
|
4
4
|
"description": "pi custom provider for Command Code API (commandcode.ai)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -25,11 +25,12 @@
|
|
|
25
25
|
"LICENSE"
|
|
26
26
|
],
|
|
27
27
|
"scripts": {
|
|
28
|
-
"test": "npm run typecheck && tsx tests/test-pure-functions.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-stream.ts && node tests/test-pi-local.mjs",
|
|
28
|
+
"test": "npm run typecheck && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-stream.ts && node tests/test-pi-local.mjs",
|
|
29
29
|
"typecheck": "tsc --noEmit",
|
|
30
30
|
"format:check": "prettier --check '**/*.{ts,mjs,json,md}'",
|
|
31
31
|
"format": "prettier --write '**/*.{ts,mjs,json,md}'",
|
|
32
32
|
"test:unit": "tsx tests/test-pure-functions.ts",
|
|
33
|
+
"test:models": "tsx tests/test-models.ts",
|
|
33
34
|
"test:oauth": "tsx tests/test-oauth.ts",
|
|
34
35
|
"test:abort": "tsx tests/test-abort.ts",
|
|
35
36
|
"test:stream": "tsx tests/test-stream.ts",
|
package/src/converters.ts
CHANGED
|
@@ -42,6 +42,16 @@ function defaultAuthPaths(home: string): string[] {
|
|
|
42
42
|
return [join(home, ".commandcode", "auth.json"), join(home, ".pi", "agent", "auth.json")]
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function apiKeyFromCredentialRecord(value: unknown): string | undefined {
|
|
46
|
+
if (!isRecord(value)) return undefined
|
|
47
|
+
|
|
48
|
+
const type = stringValue(value.type)
|
|
49
|
+
if (type === "api") return stringValue(value.key)
|
|
50
|
+
if (type === "oauth") return stringValue(value.access)
|
|
51
|
+
|
|
52
|
+
return stringValue(value.key) ?? stringValue(value.access)
|
|
53
|
+
}
|
|
54
|
+
|
|
45
55
|
export function getApiKey(
|
|
46
56
|
options: {
|
|
47
57
|
env?: NodeJS.ProcessEnv
|
|
@@ -61,18 +71,18 @@ export function getApiKey(
|
|
|
61
71
|
const parsed: unknown = JSON.parse(readFileSync(authPath, "utf-8"))
|
|
62
72
|
if (!isRecord(parsed)) continue
|
|
63
73
|
|
|
64
|
-
// Legacy: direct apiKey or commandcode field
|
|
74
|
+
// Legacy: direct apiKey or commandcode field.
|
|
65
75
|
const apiKey = stringValue(parsed.apiKey)
|
|
66
76
|
if (apiKey) return apiKey
|
|
67
77
|
const commandcode = stringValue(parsed.commandcode)
|
|
68
78
|
if (commandcode) return commandcode
|
|
69
79
|
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
// pi stores OAuth credentials as {"commandcode": {"type":"oauth","access":"..."}}.
|
|
81
|
+
// The official Command Code CLI stores API credentials under "command-code".
|
|
82
|
+
const providerKey =
|
|
83
|
+
apiKeyFromCredentialRecord(parsed.commandcode) ??
|
|
84
|
+
apiKeyFromCredentialRecord(parsed["command-code"])
|
|
85
|
+
if (providerKey) return providerKey
|
|
76
86
|
} catch {
|
|
77
87
|
// Ignore malformed or unreadable auth files.
|
|
78
88
|
}
|
package/src/core.ts
CHANGED
|
@@ -38,6 +38,9 @@ export * from "./converters.ts"
|
|
|
38
38
|
export * from "./types.ts"
|
|
39
39
|
|
|
40
40
|
export const DEFAULT_API_BASE = "https://api.commandcode.ai"
|
|
41
|
+
export const COMMAND_CODE_CLI_VERSION = "0.27.2"
|
|
42
|
+
|
|
43
|
+
const DEFAULT_GENERATE_MAX_TOKENS = 64_000
|
|
41
44
|
|
|
42
45
|
function defaultUsage(): Usage {
|
|
43
46
|
return {
|
|
@@ -77,6 +80,23 @@ function successStopReason(reason: TerminalReason): StopReason {
|
|
|
77
80
|
return "stop"
|
|
78
81
|
}
|
|
79
82
|
|
|
83
|
+
function generateMaxTokens(model: ModelLike, options?: StreamOptions): number {
|
|
84
|
+
return Math.min(
|
|
85
|
+
options?.maxTokens ?? model.maxTokens,
|
|
86
|
+
model.maxTokens,
|
|
87
|
+
DEFAULT_GENERATE_MAX_TOKENS,
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function projectSlugFromPath(pathName: string): string {
|
|
92
|
+
const slug = pathName
|
|
93
|
+
.toLowerCase()
|
|
94
|
+
.replace(/^[a-z]:/i, "")
|
|
95
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
96
|
+
.replace(/^-+|-+$/g, "")
|
|
97
|
+
return slug || "project"
|
|
98
|
+
}
|
|
99
|
+
|
|
80
100
|
export function createStreamCommandCode(deps: CoreDependencies) {
|
|
81
101
|
const apiBase = deps.apiBase ?? DEFAULT_API_BASE
|
|
82
102
|
const fetchImpl = deps.fetchImpl ?? fetch
|
|
@@ -235,7 +255,13 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
235
255
|
break
|
|
236
256
|
}
|
|
237
257
|
|
|
258
|
+
case "reasoning-start": {
|
|
259
|
+
endTextBlock()
|
|
260
|
+
break
|
|
261
|
+
}
|
|
262
|
+
|
|
238
263
|
case "reasoning-delta": {
|
|
264
|
+
endTextBlock()
|
|
239
265
|
thinkingBlock.push(stringValue(event.text) ?? "")
|
|
240
266
|
break
|
|
241
267
|
}
|
|
@@ -245,6 +271,10 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
245
271
|
break
|
|
246
272
|
}
|
|
247
273
|
|
|
274
|
+
case "tool-result": {
|
|
275
|
+
break
|
|
276
|
+
}
|
|
277
|
+
|
|
248
278
|
case "tool-call": {
|
|
249
279
|
endTextBlock()
|
|
250
280
|
const toolCall: ToolCallContent = {
|
|
@@ -303,9 +333,12 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
303
333
|
try {
|
|
304
334
|
stream.push({ type: "start", partial: output })
|
|
305
335
|
|
|
336
|
+
const workingDir = cwd()
|
|
337
|
+
const threadId = uuid()
|
|
338
|
+
|
|
306
339
|
let body: unknown = {
|
|
307
340
|
config: {
|
|
308
|
-
workingDir
|
|
341
|
+
workingDir,
|
|
309
342
|
date: new Date(now()).toISOString().split("T")[0],
|
|
310
343
|
environment: getEnvironmentInfo(),
|
|
311
344
|
structure: [],
|
|
@@ -315,18 +348,19 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
315
348
|
gitStatus: "",
|
|
316
349
|
recentCommits: [],
|
|
317
350
|
},
|
|
318
|
-
memory:
|
|
319
|
-
taste:
|
|
351
|
+
memory: null,
|
|
352
|
+
taste: null,
|
|
320
353
|
skills: null,
|
|
321
|
-
permissionMode: "standard",
|
|
322
354
|
params: {
|
|
323
355
|
model: model.id,
|
|
324
356
|
messages: messagesToCC(context.messages),
|
|
325
357
|
tools: toolsToJson(context.tools),
|
|
326
358
|
system: context.systemPrompt ?? "",
|
|
327
|
-
max_tokens:
|
|
359
|
+
max_tokens: generateMaxTokens(model, options),
|
|
360
|
+
temperature: 0.3,
|
|
328
361
|
stream: true,
|
|
329
362
|
},
|
|
363
|
+
threadId,
|
|
330
364
|
}
|
|
331
365
|
|
|
332
366
|
const nextBody = await raceAbort(
|
|
@@ -341,12 +375,11 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
341
375
|
headers: {
|
|
342
376
|
"Content-Type": "application/json",
|
|
343
377
|
Authorization: `Bearer ${apiKey}`,
|
|
344
|
-
"x-command-code-version":
|
|
378
|
+
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
|
|
345
379
|
"x-cli-environment": "production",
|
|
346
|
-
"x-project-slug":
|
|
347
|
-
"x-taste-learning": "
|
|
380
|
+
"x-project-slug": projectSlugFromPath(workingDir),
|
|
381
|
+
"x-taste-learning": "true",
|
|
348
382
|
"x-co-flag": "false",
|
|
349
|
-
"x-session-id": uuid(),
|
|
350
383
|
...options?.headers,
|
|
351
384
|
},
|
|
352
385
|
body: JSON.stringify(body),
|
package/src/models.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export const DEFAULT_MODELS_URL = "https://api.commandcode.ai/provider/v1/models"
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MAX_OUTPUT_TOKENS = 65_536
|
|
4
|
+
|
|
5
|
+
interface ApiModel {
|
|
6
|
+
id: string
|
|
7
|
+
name: string
|
|
8
|
+
contextLength: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CommandCodeModel {
|
|
12
|
+
id: string
|
|
13
|
+
name: string
|
|
14
|
+
reasoning: boolean
|
|
15
|
+
contextWindow: number
|
|
16
|
+
maxTokens: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface FetchCommandCodeModelsOptions {
|
|
20
|
+
url?: string
|
|
21
|
+
fetchImpl?: typeof fetch
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
25
|
+
return typeof value === "object" && value !== null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function stringField(record: Record<string, unknown>, key: string): string {
|
|
29
|
+
const value = record[key]
|
|
30
|
+
if (typeof value !== "string") throw new Error(`Expected ${key} to be a string`)
|
|
31
|
+
return value
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function numberField(record: Record<string, unknown>, key: string): number {
|
|
35
|
+
const value = record[key]
|
|
36
|
+
if (typeof value !== "number") throw new Error(`Expected ${key} to be a number`)
|
|
37
|
+
return value
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseApiModel(value: unknown): ApiModel {
|
|
41
|
+
if (!isRecord(value)) throw new Error("Expected model entry to be an object")
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
id: stringField(value, "id"),
|
|
45
|
+
name: stringField(value, "name"),
|
|
46
|
+
contextLength: numberField(value, "context_length"),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function commandCodeModelsFromApiResponse(value: unknown): readonly CommandCodeModel[] {
|
|
51
|
+
if (!isRecord(value)) throw new Error("Expected models response to be an object")
|
|
52
|
+
if (value.object !== "list") throw new Error("Expected models response object to be 'list'")
|
|
53
|
+
|
|
54
|
+
const data = value.data
|
|
55
|
+
if (!Array.isArray(data)) throw new Error("Expected models response data to be an array")
|
|
56
|
+
|
|
57
|
+
return data.map(parseApiModel).map((model) => ({
|
|
58
|
+
id: model.id,
|
|
59
|
+
name: `${model.name} (CC)`,
|
|
60
|
+
reasoning: true,
|
|
61
|
+
contextWindow: model.contextLength,
|
|
62
|
+
maxTokens: Math.min(model.contextLength, DEFAULT_MAX_OUTPUT_TOKENS),
|
|
63
|
+
}))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function fetchCommandCodeModels(
|
|
67
|
+
options: FetchCommandCodeModelsOptions = {},
|
|
68
|
+
): Promise<readonly CommandCodeModel[]> {
|
|
69
|
+
const url = options.url ?? DEFAULT_MODELS_URL
|
|
70
|
+
const fetchImpl = options.fetchImpl ?? fetch
|
|
71
|
+
const response = await fetchImpl(url, {
|
|
72
|
+
headers: {
|
|
73
|
+
accept: "application/json",
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Failed to fetch Command Code models: ${response.status} ${response.statusText}`,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const body: unknown = await response.json()
|
|
84
|
+
return commandCodeModelsFromApiResponse(body)
|
|
85
|
+
}
|