Merge pull request #49 from patlux/rebuild/issue-5-provider-api

feat(account): Official CommandCode API
This commit is contained in:
Patrick Wozniak
2026-08-20 00:16:41 +02:00
committed by GitHub
24 changed files with 1058 additions and 204 deletions
+1 -1
View File
@@ -41,5 +41,5 @@ title = "pi-commandcode-provider secret scan"
[[rules]] [[rules]]
id = "pi-test-api-key" id = "pi-test-api-key"
description = "Test API key value that looks real" description = "Test API key value that looks real"
regex = '''(user_testKey|mock-key|fake-key|test-api-key)''' regex = '''['"](user_testKey|mock-key|fake-key|test-api-key)['"]'''
tags = ["pi-extension", "test"] tags = ["pi-extension", "test"]
+8
View File
@@ -2,6 +2,14 @@
## Unreleased ## Unreleased
- Prefer Command Code's Provider API (`/provider/v1/chat/completions` and `/provider/v1/messages`) and automatically fall back to the existing `/alpha/generate` transport only when the Provider API returns `403 upgrade_required` for a Go-plan account.
- Remember the detected transport for the running process, re-detect it when credentials change, prevent stale in-flight requests from overwriting the new credential's transport, and never fall back for unrelated authentication, permission, rate-limit, network, or server failures.
- Use Pi's native OpenAI- and Anthropic-compatible providers for Provider API streaming, including adaptive thinking for current reasoning-capable Claude models, while preserving the existing hardened generate transport, dynamic model discovery, offline cache, refresh/status commands, pricing, and OAuth credentials.
- Let `/login` use browser authentication, an explicit API-key prompt, or a directly pasted API key.
- Add optional zero-data-retention headers through `COMMANDCODE_ZDR=1`.
- Refresh GPT-5.6 Terra and Luna display prices after their temporary 50% promotion ended, and display the current DeepSeek V4 off-peak rates for its time-dependent pricing.
- Add isolated live E2E profiles for separate Go-plan and Provider-API credentials, including an explicit selected-transport assertion and packed-package validation.
## 0.5.1 - 2026-08-11 ## 0.5.1 - 2026-08-11
- Add model-specific image input capabilities from the `command-code@1.15.1` catalog and forward user and tool-result images using the current Command Code wire format. - Add model-specific image input capabilities from the `command-code@1.15.1` catalog and forward user and tool-result images using the current Command Code wire format.
+9
View File
@@ -42,6 +42,15 @@ npm run pi:authenticated
Both commands accept additional pi arguments after `--`, for example `npm run pi:authenticated -- --model claude-sonnet-4-6`. Both commands accept additional pi arguments after `--`, for example `npm run pi:authenticated -- --model claude-sonnet-4-6`.
Run the transport-specific live tests with separate credentials:
```sh
COMMANDCODE_E2E_GO_API_KEY_FILE=/path/to/go-key npm run test:e2e:live:go
COMMANDCODE_E2E_PROVIDER_API_KEY_FILE=/path/to/provider-key npm run test:e2e:live:provider
```
Use `npm run test:e2e:live:all` with both file variables to run them sequentially. Store the keys in a secret manager and export each one to a new mode-`0600` temporary file for the test; never add key files to the repository. The direct `COMMANDCODE_E2E_GO_API_KEY` and `COMMANDCODE_E2E_PROVIDER_API_KEY` variables are intended primarily for protected CI secrets.
Before opening a PR, run: Before opening a PR, run:
```sh ```sh
+39 -10
View File
@@ -5,7 +5,16 @@
A custom provider for [pi](https://github.com/earendil-works/pi) that connects to the [Command Code](https://commandcode.ai) Provider API. A custom provider for [pi](https://github.com/earendil-works/pi) that connects to the [Command Code](https://commandcode.ai) Provider API.
> **Disclaimer:** This is an unofficial, community-maintained integration. It is not affiliated with, endorsed by, or supported by Command Code. You need your own Command Code account and API key or subscription. Command Code's terms, availability, and pricing apply. > **Disclaimer:** This is an unofficial, community-maintained integration. It is not affiliated with, endorsed by, or supported by Command Code. You need your own Command Code account, API key, and a plan with Provider API access. Command Code's terms, availability, and pricing apply.
The extension uses one provider and automatically selects the transport supported by the authenticated account:
- `GET /provider/v1/models` for model discovery
- `POST /provider/v1/chat/completions` for non-Claude models with Provider API access
- `POST /provider/v1/messages` for Claude models with Provider API access
- `/alpha/generate` after the Provider API explicitly returns `403 upgrade_required`, which currently identifies Go-plan accounts
The detected transport is remembered only for the running process and is re-evaluated when the credential changes. Other authentication, permission, rate-limit, network, and server errors never trigger the fallback.
## Install ## Install
@@ -19,7 +28,7 @@ Start or reload pi, then authenticate:
/login /login
``` ```
Select **Use a subscription**, then **Command Code**. Complete the browser flow and choose a model with `/model`. Select **Use a subscription**, then **Command Code**. Choose browser login or paste an API key, then select a model with `/model`.
## Oh My Pi ## Oh My Pi
@@ -33,9 +42,9 @@ Restart OMP or run `/reload`, then use `/login` and select **Use a subscription*
## Authentication ## Authentication
### Browser login ### Login dialog
Run `/login` in pi or OMP. Select **Use a subscription**, then **Command Code**. The browser flow stores the returned credential in the host's auth file. Run `/login` in pi or OMP. Select **Use a subscription**, then **Command Code**. Press Enter for browser login, type `key` to open a paste prompt, or paste the API key directly. The selected credential is stored in the host's auth file.
<img width="1520" height="554" alt="Select Command Code in pi's login dialog" src="https://github.com/user-attachments/assets/071e929a-6f49-4803-bfec-7a31368fb12a" /> <img width="1520" height="554" alt="Select Command Code in pi's login dialog" src="https://github.com/user-attachments/assets/071e929a-6f49-4803-bfec-7a31368fb12a" />
@@ -84,9 +93,7 @@ Open `/model` and select one of the models provided by Command Code. Model avail
### Reasoning support ### Reasoning support
Reasoning metadata is enriched only for models whose Command Code effort support is known. Those models register a model-specific `thinkingLevelMap`, so pi and OMP expose only supported levels. A selected supported level is sent as the documented `params.reasoning_effort` field; `off`, unsupported levels, and newly discovered models without metadata do not add reasoning fields to the request. No prompt instructions are injected. Reasoning metadata is enriched only for models whose Command Code effort support is known. Those models register a model-specific `thinkingLevelMap`, so pi and OMP expose only supported levels. Pi's native OpenAI- and Anthropic-compatible providers translate the selected level for Provider API accounts; the existing Command Code generate transport sends the matching `reasoning_effort` for Go accounts. Unsupported levels and newly discovered models without metadata do not claim reasoning support.
Reasoning blocks from completed assistant turns remain visible in pi's local session, but are not replayed to Command Code in later requests. Only the assistant's user-visible text and completed tool calls are sent back as history. This matches the current Command Code CLI behavior and prevents prior private reasoning traces from interfering with reasoning on follow-up turns.
List Command Code models from the terminal: List Command Code models from the terminal:
@@ -123,6 +130,8 @@ While pi is running, use these provider commands without restarting:
- `/commandcode-refresh` fetches and re-registers the current model catalog. Overlapping refreshes are coalesced, and a failed refresh keeps the last valid catalog active. - `/commandcode-refresh` fetches and re-registers the current model catalog. Overlapping refreshes are coalesced, and a failed refresh keeps the last valid catalog active.
- `/commandcode-status` shows redacted discovery diagnostics, including the source, model count, timestamps, cache path, endpoint, and warning. - `/commandcode-status` shows redacted discovery diagnostics, including the source, model count, timestamps, cache path, endpoint, and warning.
Set `COMMANDCODE_ZDR=1` to send Command Code's documented `x-cmd-zdr: 1` zero-data-retention header.
The following environment variables are intended for tests, local mocks, and compatible API endpoints: The following environment variables are intended for tests, local mocks, and compatible API endpoints:
- `COMMANDCODE_API_BASE` - `COMMANDCODE_API_BASE`
@@ -134,13 +143,13 @@ The following environment variables are intended for tests, local mocks, and com
The provider advertises image input only for models marked with the `image` input modality in the official Command Code CLI model catalog. The capability snapshot currently follows `command-code@1.15.1`; unknown models default to text-only until their upstream metadata is reviewed. The provider advertises image input only for models marked with the `image` input modality in the official Command Code CLI model catalog. The capability snapshot currently follows `command-code@1.15.1`; unknown models default to text-only until their upstream metadata is reviewed.
For vision-capable models, image blocks from user messages and tool results are forwarded in Command Code's current data-URL wire format. Text-only models reject image content before making a network request instead of silently dropping it. For vision-capable models, Pi's native provider adapters forward image blocks from user messages and tool results using the documented OpenAI or Anthropic message schema. Unknown and text-only models remain marked text-only in Pi.
## Pricing display ## Pricing display
The Command Code Provider API does not currently include prices in its model catalog. This extension therefore keeps a static table for models with known prices so pi can display estimated request costs. The Command Code Provider API does not currently include prices in its model catalog. This extension therefore keeps a static table for models with known prices so pi can display estimated request costs. DeepSeek V4 uses time-dependent rates; pi displays the documented off-peak rate, which applies for 17 hours per day.
Models missing from that table display zero cost in pi. This does **not** mean that Command Code will bill the request at zero. Check the current [Command Code pricing](https://commandcode.ai/docs/resources/pricing-limits) before relying on the displayed value. Models missing from that table display zero cost in pi. This does **not** mean that Command Code will bill the request at zero. The Command Code Usage page remains authoritative for each request. Check the current [Command Code pricing](https://commandcode.ai/docs/resources/pricing-limits) before relying on the displayed value.
## Update and remove ## Update and remove
@@ -181,6 +190,26 @@ npm run pi:authenticated
Both commands accept additional pi arguments after `--`, for example `npm run pi:authenticated -- --model claude-sonnet-4-6`. Both commands accept additional pi arguments after `--`, for example `npm run pi:authenticated -- --model claude-sonnet-4-6`.
### Live transport tests
Keep the Go-plan and Provider-API test keys in separate secret-manager entries. Pass them through protected files so the keys do not enter shell history:
```sh
COMMANDCODE_E2E_GO_API_KEY_FILE=/path/to/go-key \
npm run test:e2e:live:go
COMMANDCODE_E2E_PROVIDER_API_KEY_FILE=/path/to/provider-key \
npm run test:e2e:live:provider
COMMANDCODE_E2E_GO_API_KEY_FILE=/path/to/go-key \
COMMANDCODE_E2E_PROVIDER_API_KEY_FILE=/path/to/provider-key \
npm run test:e2e:live:all
```
Each profile runs with an isolated Pi agent directory and asserts the selected transport through `/commandcode-status`: Go must select `generate`, while a Provider API account must select `provider`. The profile-specific `*_API_KEY` environment variables are also supported for CI secrets, but key files are preferred for local use.
Override the default DeepSeek test model with `COMMANDCODE_E2E_GO_MODEL` or `COMMANDCODE_E2E_PROVIDER_MODEL`. A successful live Anthropic `/provider/v1/messages` test requires a Provider API account whose plan includes the selected Claude model.
See [CONTRIBUTING.md](CONTRIBUTING.md) for local setup and tests. See [RELEASE.md](RELEASE.md) for the release process. See [CONTRIBUTING.md](CONTRIBUTING.md) for local setup and tests. See [RELEASE.md](RELEASE.md) for the release process.
## License ## License
+56 -33
View File
@@ -1,12 +1,12 @@
/** /**
* Command Code provider for pi. * Command Code provider for pi.
* *
* Connects pi to Command Code's API (https://api.commandcode.ai/alpha/generate). * Uses Command Code's documented Provider API:
* The provider uses pi's legacy extension registration surface because the * https://api.commandcode.ai/provider/v1
* current pi host exposes `registerProvider(name, config)`, including OMP.
*/ */
import { AssistantMessageEventStream } from "@earendil-works/pi-ai" import { AssistantMessageEventStream } from "@earendil-works/pi-ai"
import { streamSimple as streamNativeProvider } from "@earendil-works/pi-ai/compat"
import { import {
getAgentDir, getAgentDir,
type ExtensionAPI, type ExtensionAPI,
@@ -15,10 +15,13 @@ import {
} from "@earendil-works/pi-coding-agent" } from "@earendil-works/pi-coding-agent"
import { join } from "node:path" import { join } from "node:path"
import { COMMAND_CODE_CLI_VERSION, createStreamCommandCode, DEFAULT_API_BASE } from "./src/core.ts" import { getConfiguredApiKey } from "./src/api-key.ts"
import { createStreamCommandCode } from "./src/core.ts"
import { calculateCommandCodeCost } from "./src/cost.ts" import { calculateCommandCodeCost } from "./src/cost.ts"
import { import {
baseUrlForModel,
DEFAULT_MODELS_URL, DEFAULT_MODELS_URL,
DEFAULT_PROVIDER_API_BASE,
getModelsTimeoutMs, getModelsTimeoutMs,
inputModalitiesForModel, inputModalitiesForModel,
loadCommandCodeModels, loadCommandCodeModels,
@@ -26,68 +29,87 @@ import {
type CommandCodeModel, type CommandCodeModel,
} from "./src/models.ts" } from "./src/models.ts"
import { getApiKey as getOAuthApiKey, login, refreshToken } from "./src/oauth.ts" import { getApiKey as getOAuthApiKey, login, refreshToken } from "./src/oauth.ts"
import { normalizeCommandCodeMessage } from "./src/overflow.ts"
import { MODEL_COSTS, ZERO_MODEL_COST } from "./src/pricing.ts" import { MODEL_COSTS, ZERO_MODEL_COST } from "./src/pricing.ts"
import { createCommandCodeRuntime } from "./src/runtime.ts" import { createCommandCodeRuntime } from "./src/runtime.ts"
import { normalizeCommandCodeMessage } from "./src/overflow.ts" import { createCommandCodeTransportRouter } from "./src/transport.ts"
function commandCodeHeaders(): Record<string, string> | undefined {
if (process.env.COMMANDCODE_ZDR === "1") {
return { "x-cmd-zdr": "1" }
}
return undefined
}
function createProviderConfig( function createProviderConfig(
models: readonly CommandCodeModel[], models: readonly CommandCodeModel[],
apiBase: string, apiBase: string,
streamCommandCode: ProviderConfig["streamSimple"], streamCommandCode: ProviderConfig["streamSimple"],
): ProviderConfig { ): ProviderConfig {
const headers = commandCodeHeaders()
return { return {
name: "Command Code", name: "Command Code",
baseUrl: apiBase, baseUrl: apiBase,
// Keep environment authentication dynamic. OAuth credentials are resolved apiKey: getConfiguredApiKey() ?? "$COMMANDCODE_API_KEY",
// by pi's oauth registration, while the custom stream retains its own api: "openai-completions",
// request-time legacy-file fallback for older compatible hosts.
apiKey: "$COMMANDCODE_API_KEY",
authHeader: true,
api: "commandcode-custom",
streamSimple: streamCommandCode, streamSimple: streamCommandCode,
headers: { headers,
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
"x-cli-environment": "production",
},
oauth: { oauth: {
name: "Command Code", name: "Command Code",
login, login,
refreshToken, refreshToken,
getApiKey: getOAuthApiKey, getApiKey: getOAuthApiKey,
}, },
models: models.map(createProviderModel), models: models.map((model) => ({
}
}
function createProviderModel(model: {
id: string
name: string
reasoning: boolean
contextWindow: number
maxTokens: number
}) {
return {
id: model.id, id: model.id,
name: model.name, name: model.name,
api: model.api,
baseUrl: baseUrlForModel(apiBase, model.api),
reasoning: model.reasoning, reasoning: model.reasoning,
...(thinkingMetadataForModel(model.id) ?? {}), ...(thinkingMetadataForModel(model.id) ?? {}),
input: inputModalitiesForModel(model.id), input: [...inputModalitiesForModel(model.id)],
cost: MODEL_COSTS[model.id] ?? ZERO_MODEL_COST, cost: MODEL_COSTS[model.id] ?? ZERO_MODEL_COST,
contextWindow: model.contextWindow, contextWindow: model.contextWindow,
maxTokens: model.maxTokens, maxTokens: model.maxTokens,
} as const headers,
compat:
model.api === "openai-completions"
? {
supportsStore: false,
supportsDeveloperRole: false,
supportsReasoningEffort: true,
maxTokensField: "max_tokens",
}
: {
supportsEagerToolInputStreaming: false,
supportsLongCacheRetention: false,
supportsCacheControlOnTools: false,
supportsToolReferences: false,
...(model.reasoning ? { forceAdaptiveThinking: true } : {}),
},
})),
}
}
function legacyApiBase(providerApiBase: string): string {
return providerApiBase.replace(/\/provider\/v1\/?$/, "")
} }
export default async function (pi: ExtensionAPI) { export default async function (pi: ExtensionAPI) {
const apiBase = process.env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE const apiBase = process.env.COMMANDCODE_API_BASE ?? DEFAULT_PROVIDER_API_BASE
const modelsUrl = process.env.COMMANDCODE_MODELS_URL ?? DEFAULT_MODELS_URL const modelsUrl = process.env.COMMANDCODE_MODELS_URL ?? DEFAULT_MODELS_URL
const modelsTimeoutMs = getModelsTimeoutMs() const modelsTimeoutMs = getModelsTimeoutMs()
const modelsCachePath = const modelsCachePath =
process.env.COMMANDCODE_MODELS_CACHE ?? join(getAgentDir(), "commandcode-models.json") process.env.COMMANDCODE_MODELS_CACHE ?? join(getAgentDir(), "commandcode-models.json")
const streamCommandCode = createStreamCommandCode({ const streamGenerate = createStreamCommandCode({
createStream: () => new AssistantMessageEventStream(), createStream: () => new AssistantMessageEventStream(),
calculateCost: calculateCommandCodeCost, calculateCost: calculateCommandCodeCost,
apiBase, apiBase: legacyApiBase(apiBase),
})
const transport = createCommandCodeTransportRouter({
createStream: () => new AssistantMessageEventStream(),
streamProvider: streamNativeProvider,
streamGenerate,
}) })
pi.on("message_end", async (event, ctx) => { pi.on("message_end", async (event, ctx) => {
@@ -105,7 +127,8 @@ export default async function (pi: ExtensionAPI) {
cachePath: modelsCachePath, cachePath: modelsCachePath,
timeoutMs: modelsTimeoutMs, timeoutMs: modelsTimeoutMs,
}), }),
createProviderConfig: (models) => createProviderConfig(models, apiBase, streamCommandCode), createProviderConfig: (models) => createProviderConfig(models, apiBase, transport.stream),
getTransport: transport.getTransport,
}) })
await runtime.initialize() await runtime.initialize()
+7 -2
View File
@@ -29,13 +29,14 @@
"LICENSE" "LICENSE"
], ],
"scripts": { "scripts": {
"test": "npm run typecheck && tsx tests/test-package-manifest.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-runtime.ts && tsx tests/test-pricing.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-overflow.ts && tsx tests/test-stream.ts && tsx tests/test-retry.ts && node tests/test-pi-isolated.mjs && node tests/test-pi-authenticated.mjs && node tests/test-pi-local.mjs && node tests/test-omp-compat.mjs", "test": "npm run typecheck && tsx tests/test-package-manifest.ts && tsx tests/test-api-key.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-runtime.ts && tsx tests/test-pricing.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-overflow.ts && tsx tests/test-stream.ts && tsx tests/test-retry.ts && tsx tests/test-transport.ts && node tests/test-pi-isolated.mjs && node tests/test-pi-authenticated.mjs && node tests/test-pi-local.mjs && node tests/test-omp-compat.mjs",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"format:check": "prettier --check '**/*.{ts,mjs,json,md}'", "format:check": "prettier --check '**/*.{ts,mjs,json,md}'",
"format": "prettier --write '**/*.{ts,mjs,json,md}'", "format": "prettier --write '**/*.{ts,mjs,json,md}'",
"pi:isolated": "node scripts/pi-isolated.mjs", "pi:isolated": "node scripts/pi-isolated.mjs",
"pi:authenticated": "node scripts/pi-authenticated.mjs", "pi:authenticated": "node scripts/pi-authenticated.mjs",
"test:unit": "tsx tests/test-pure-functions.ts", "test:unit": "tsx tests/test-api-key.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-runtime.ts && tsx tests/test-pricing.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-overflow.ts && tsx tests/test-stream.ts && tsx tests/test-retry.ts && tsx tests/test-transport.ts",
"test:api-key": "tsx tests/test-api-key.ts",
"test:models": "tsx tests/test-models.ts", "test:models": "tsx tests/test-models.ts",
"test:runtime": "tsx tests/test-runtime.ts", "test:runtime": "tsx tests/test-runtime.ts",
"test:pricing": "tsx tests/test-pricing.ts", "test:pricing": "tsx tests/test-pricing.ts",
@@ -44,11 +45,15 @@
"test:overflow": "tsx tests/test-overflow.ts", "test:overflow": "tsx tests/test-overflow.ts",
"test:stream": "tsx tests/test-stream.ts", "test:stream": "tsx tests/test-stream.ts",
"test:retry": "tsx tests/test-retry.ts", "test:retry": "tsx tests/test-retry.ts",
"test:transport": "tsx tests/test-transport.ts",
"test:pi-isolated": "node tests/test-pi-isolated.mjs", "test:pi-isolated": "node tests/test-pi-isolated.mjs",
"test:pi-authenticated": "node tests/test-pi-authenticated.mjs", "test:pi-authenticated": "node tests/test-pi-authenticated.mjs",
"test:pi-local": "node tests/test-pi-local.mjs", "test:pi-local": "node tests/test-pi-local.mjs",
"test:smoke": "node tests/test-smoke.mjs", "test:smoke": "node tests/test-smoke.mjs",
"test:e2e:live": "node tests/test-live-e2e.mjs", "test:e2e:live": "node tests/test-live-e2e.mjs",
"test:e2e:live:go": "node scripts/live-e2e-profile.mjs go",
"test:e2e:live:provider": "node scripts/live-e2e-profile.mjs provider",
"test:e2e:live:all": "node scripts/live-e2e-profile.mjs go provider",
"test:cost": "tsx tests/test-cost.ts" "test:cost": "tsx tests/test-cost.ts"
}, },
"pi": { "pi": {
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env node
import { spawn } from "node:child_process"
import { readFile } from "node:fs/promises"
import { dirname, resolve } from "node:path"
import { fileURLToPath } from "node:url"
const projectDir = resolve(dirname(fileURLToPath(import.meta.url)), "..")
const liveTest = resolve(projectDir, "tests", "test-live-e2e.mjs")
const profiles = process.argv.slice(2)
if (
profiles.length === 0 ||
profiles.some((profile) => profile !== "go" && profile !== "provider")
) {
console.error("Usage: node scripts/live-e2e-profile.mjs <go|provider> [go|provider]")
process.exit(2)
}
async function credentialFor(profile) {
const prefix = profile === "go" ? "COMMANDCODE_E2E_GO" : "COMMANDCODE_E2E_PROVIDER"
const direct = process.env[`${prefix}_API_KEY`]?.trim()
const file = process.env[`${prefix}_API_KEY_FILE`]
if (direct && file)
throw new Error(`${prefix}_API_KEY and ${prefix}_API_KEY_FILE are mutually exclusive`)
if (direct) return direct
if (file) {
const credential = (await readFile(file, "utf-8")).trim()
if (credential) return credential
}
throw new Error(`Set ${prefix}_API_KEY_FILE (recommended) or ${prefix}_API_KEY`)
}
function runProfile(profile, apiKey) {
const modelVariable =
profile === "go" ? "COMMANDCODE_E2E_GO_MODEL" : "COMMANDCODE_E2E_PROVIDER_MODEL"
const model = process.env[modelVariable] ?? "deepseek/deepseek-v4-flash"
const env = {
...process.env,
COMMANDCODE_API_KEY: apiKey,
COMMANDCODE_E2E_MODEL: model,
COMMANDCODE_E2E_PROFILE: profile,
}
delete env.COMMANDCODE_E2E_GO_API_KEY
delete env.COMMANDCODE_E2E_PROVIDER_API_KEY
return new Promise((resolveRun, reject) => {
console.log(`[live-e2e:${profile}] model ${model}`)
const child = spawn(process.execPath, [liveTest], {
cwd: projectDir,
env,
stdio: "inherit",
})
child.on("error", reject)
child.on("close", (code, signal) => {
if (code === 0) {
resolveRun()
return
}
reject(new Error(`[live-e2e:${profile}] failed (${signal ?? `exit ${code}`})`))
})
})
}
try {
for (const profile of profiles) {
await runProfile(profile, await credentialFor(profile))
}
} catch (error) {
console.error(error instanceof Error ? error.message : String(error))
process.exit(1)
}
+68
View File
@@ -0,0 +1,68 @@
import { existsSync, readFileSync } from "node:fs"
import { homedir } from "node:os"
import { join } from "node:path"
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
}
function stringValue(value: unknown): string | undefined {
return typeof value === "string" ? value : undefined
}
function defaultAuthPaths(home: string): string[] {
return [
join(home, ".commandcode", "auth.json"),
join(home, ".pi", "agent", "auth.json"),
join(home, ".omp", "agent", "auth.json"),
]
}
function apiKeyFromCredential(value: unknown): string | undefined {
if (!isRecord(value)) return undefined
if (stringValue(value.type) === "oauth") return stringValue(value.access)
if (stringValue(value.type) === "api") return stringValue(value.key)
return stringValue(value.access) ?? stringValue(value.key)
}
export function getConfiguredApiKey(
options: {
env?: NodeJS.ProcessEnv
authPaths?: readonly string[]
homeDir?: () => string
} = {},
): string | undefined {
const env = options.env ?? process.env
if (env.COMMANDCODE_API_KEY) return env.COMMANDCODE_API_KEY
const home = options.homeDir?.() ?? homedir()
const authPaths = options.authPaths ?? defaultAuthPaths(home)
for (const authPath of authPaths) {
try {
if (!existsSync(authPath)) continue
const parsed: unknown = JSON.parse(readFileSync(authPath, "utf-8"))
if (!isRecord(parsed)) continue
const apiKey = stringValue(parsed.apiKey)
if (apiKey) return apiKey
const commandcode = stringValue(parsed.commandcode)
if (commandcode) return commandcode
const providerKey = apiKeyFromCredential(parsed.commandcode)
if (providerKey) return providerKey
const commandCode = stringValue(parsed["command-code"])
if (commandCode) return commandCode
const commandCodeKey = apiKeyFromCredential(parsed["command-code"])
if (commandCodeKey) return commandCodeKey
} catch {
// Ignore malformed or unreadable auth files.
}
}
return undefined
}
+16 -1
View File
@@ -1,12 +1,14 @@
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises" import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
import { dirname } from "node:path" import { dirname } from "node:path"
export const DEFAULT_MODELS_URL = "https://api.commandcode.ai/provider/v1/models" export const DEFAULT_PROVIDER_API_BASE = "https://api.commandcode.ai/provider/v1"
export const DEFAULT_MODELS_URL = `${DEFAULT_PROVIDER_API_BASE}/models`
export const DEFAULT_MODELS_TIMEOUT_MS = 10_000 export const DEFAULT_MODELS_TIMEOUT_MS = 10_000
const DEFAULT_MAX_OUTPUT_TOKENS = 65_536 const DEFAULT_MAX_OUTPUT_TOKENS = 65_536
const MODEL_CACHE_VERSION = 1 const MODEL_CACHE_VERSION = 1
export type CommandCodeApi = "openai-completions" | "anthropic-messages"
export type CommandCodeInputType = "text" | "image" export type CommandCodeInputType = "text" | "image"
/** /**
@@ -159,11 +161,22 @@ interface ApiModel {
export interface CommandCodeModel { export interface CommandCodeModel {
id: string id: string
name: string name: string
api: CommandCodeApi
reasoning: boolean reasoning: boolean
contextWindow: number contextWindow: number
maxTokens: number maxTokens: number
} }
export function apiForModelId(id: string): CommandCodeApi {
return id.startsWith("claude-") ? "anthropic-messages" : "openai-completions"
}
export function baseUrlForModel(apiBase: string, api: CommandCodeApi): string {
const normalized = apiBase.replace(/\/+$/g, "")
if (api !== "anthropic-messages") return normalized
return normalized.endsWith("/v1") ? normalized.slice(0, -3) : normalized
}
interface FetchCommandCodeModelsOptions { interface FetchCommandCodeModelsOptions {
url?: string url?: string
fetchImpl?: typeof fetch fetchImpl?: typeof fetch
@@ -225,6 +238,7 @@ function parseCachedModel(value: unknown): CommandCodeModel {
return { return {
id, id,
name: stringField(value, "name"), name: stringField(value, "name"),
api: apiForModelId(id),
reasoning: isReasoningModel(id), reasoning: isReasoningModel(id),
contextWindow: positiveNumberField(value, "contextWindow"), contextWindow: positiveNumberField(value, "contextWindow"),
maxTokens: positiveNumberField(value, "maxTokens"), maxTokens: positiveNumberField(value, "maxTokens"),
@@ -329,6 +343,7 @@ export function commandCodeModelsFromApiResponse(value: unknown): readonly Comma
return data.map(parseApiModel).map((model) => ({ return data.map(parseApiModel).map((model) => ({
id: model.id, id: model.id,
name: `${model.name} (CC)`, name: `${model.name} (CC)`,
api: apiForModelId(model.id),
reasoning: isReasoningModel(model.id), reasoning: isReasoningModel(model.id),
contextWindow: model.contextLength, contextWindow: model.contextLength,
maxTokens: Math.min(model.contextLength, DEFAULT_MAX_OUTPUT_TOKENS), maxTokens: Math.min(model.contextLength, DEFAULT_MAX_OUTPUT_TOKENS),
+53 -14
View File
@@ -1,13 +1,13 @@
/** /**
* Command Code OAuth provider for pi's /login flow. * Command Code OAuth provider for pi's /login flow.
* *
* Implements a browser-assisted API key retrieval flow: * Implements two API key retrieval flows:
* 1. Starts a local HTTP server on a Command Code CLI-compatible port * 1. Browser-assisted login opens Command Code Studio and waits for the
* 2. Opens the Command Code Studio auth page in the browser * website to POST the API key back to a local callback server.
* 3. The user authenticates on the Command Code website * 2. Direct API key login prompts the user to paste a Studio API key.
* 4. The website POSTs the API key back to the local server *
* 5. If browser transfer fails, the user can paste the API key manually * If browser transfer fails, the user can still paste the API key manually.
* 6. The API key is stored in pi's auth.json as OAuth credentials * The API key is stored in pi's auth.json as OAuth credentials.
* *
* Since Command Code API keys don't expire, we store them as * Since Command Code API keys don't expire, we store them as
* OAuth credentials with a far-future expiry. * OAuth credentials with a far-future expiry.
@@ -101,13 +101,35 @@ async function promptForApiKey(callbacks: OAuthLoginCallbacks, message: string)
return credentialsFromApiKey(apiKey) return credentialsFromApiKey(apiKey)
} }
/** type LoginChoice = { type: "browser" } | { type: "prompt" } | { type: "apiKey"; apiKey: string }
* Starts the browser-based login flow for Command Code.
* async function chooseLoginFlow(callbacks: OAuthLoginCallbacks): Promise<LoginChoice> {
* Returns OAuth credentials where access == refresh == the user's API key. const input = sanitizeApiKey(
* The keys don't expire, so we set a far-future expiry. await callbacks.onPrompt({
*/ message:
export async function login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> { "Command Code login: press Enter for browser login, type 'key' to paste an API key, or paste the API key directly:",
}),
)
const normalized = input.toLowerCase()
if (!input || normalized === "1" || normalized === "b" || normalized === "browser") {
return { type: "browser" }
}
if (
normalized === "2" ||
normalized === "k" ||
normalized === "key" ||
normalized === "api" ||
normalized === "paste"
) {
return { type: "prompt" }
}
return { type: "apiKey", apiKey: input }
}
async function browserLogin(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
let authServer let authServer
try { try {
authServer = await startAuthServer() authServer = await startAuthServer()
@@ -151,6 +173,23 @@ export async function login(callbacks: OAuthLoginCallbacks): Promise<OAuthCreden
return credentialsFromApiKey(callback.apiKey) return credentialsFromApiKey(callback.apiKey)
} }
/**
* Starts the login flow for Command Code.
*
* Returns OAuth credentials where access == refresh == the user's API key.
* The keys don't expire, so we set a far-future expiry.
*/
export async function login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
const choice = await chooseLoginFlow(callbacks)
if (choice.type === "apiKey") return credentialsFromApiKey(choice.apiKey)
if (choice.type === "prompt") {
return promptForApiKey(callbacks, "Paste your Command Code API key:")
}
return browserLogin(callbacks)
}
/** /**
* Command Code API keys don't expire, so "refresh" is a no-op. * Command Code API keys don't expire, so "refresh" is a no-op.
* Returns the same credentials with an updated far-future expiry. * Returns the same credentials with an updated far-future expiry.
+11 -44
View File
@@ -20,7 +20,7 @@ export interface TemporaryPricing {
} }
export const PRICING_SOURCE_URL = "https://commandcode.ai/docs/resources/pricing-limits" export const PRICING_SOURCE_URL = "https://commandcode.ai/docs/resources/pricing-limits"
export const PRICING_LAST_VERIFIED = "2026-08-04" export const PRICING_LAST_VERIFIED = "2026-08-20"
export const ZERO_MODEL_COST: CommandCodeModelCost = { export const ZERO_MODEL_COST: CommandCodeModelCost = {
input: 0, input: 0,
@@ -61,17 +61,18 @@ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
"MiniMaxAI/MiniMax-M3": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 }, "MiniMaxAI/MiniMax-M3": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
"MiniMaxAI/MiniMax-M2.7": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 }, "MiniMaxAI/MiniMax-M2.7": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
"MiniMaxAI/MiniMax-M2.5": { input: 0.3, output: 1.2, cacheRead: 0.03, cacheWrite: 0 }, "MiniMaxAI/MiniMax-M2.5": { input: 0.3, output: 1.2, cacheRead: 0.03, cacheWrite: 0 },
// Permanent 75% discount. // DeepSeek V4 uses time-dependent rates. Display the documented off-peak
// rates, which apply for 17 hours per day; the Usage page remains authoritative.
"deepseek/deepseek-v4-pro": { "deepseek/deepseek-v4-pro": {
input: 0.435, input: 0.66,
output: 0.87, output: 1.98,
cacheRead: 0.003625, cacheRead: 0.022,
cacheWrite: 0, cacheWrite: 0,
}, },
"deepseek/deepseek-v4-flash": { "deepseek/deepseek-v4-flash": {
input: 0.14, input: 0.22,
output: 0.28, output: 0.66,
cacheRead: 0.0028, cacheRead: 0.007,
cacheWrite: 0, cacheWrite: 0,
}, },
"Qwen/Qwen3.8-Max": { input: 2, output: 6, cacheRead: 0.25, cacheWrite: 2.5 }, "Qwen/Qwen3.8-Max": { input: 2, output: 6, cacheRead: 0.25, cacheWrite: 2.5 },
@@ -158,37 +159,8 @@ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
// OpenAI // OpenAI
"gpt-5.6-sol": { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 }, "gpt-5.6-sol": { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 },
// Discounted rates through 2026-08-14. "gpt-5.6-terra": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 2.5 },
"gpt-5.6-terra": { "gpt-5.6-luna": { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite: 0.25 },
input: 1,
output: 6,
cacheRead: 0.1,
cacheWrite: 1.25,
tiers: [
{
inputTokensAbove: 272_000,
input: 2,
output: 9,
cacheRead: 0.2,
cacheWrite: 2.5,
},
],
},
"gpt-5.6-luna": {
input: 0.1,
output: 0.6,
cacheRead: 0.01,
cacheWrite: 0.125,
tiers: [
{
inputTokensAbove: 272_000,
input: 0.2,
output: 0.9,
cacheRead: 0.02,
cacheWrite: 0.25,
},
],
},
"gpt-5.5": { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 }, "gpt-5.5": { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
"gpt-5.4": { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 }, "gpt-5.4": { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 },
"gpt-5.3-codex": { input: 2, output: 8, cacheRead: 0.5, cacheWrite: 0 }, "gpt-5.3-codex": { input: 2, output: 8, cacheRead: 0.5, cacheWrite: 0 },
@@ -213,11 +185,6 @@ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
} }
export const TEMPORARY_PRICING: readonly TemporaryPricing[] = [ export const TEMPORARY_PRICING: readonly TemporaryPricing[] = [
{
models: ["gpt-5.6-terra", "gpt-5.6-luna"],
expiresOn: "2026-08-14",
description: "50% promotional rates",
},
{ {
models: ["claude-sonnet-5"], models: ["claude-sonnet-5"],
expiresOn: "2026-08-31", expiresOn: "2026-08-31",
+10 -5
View File
@@ -28,11 +28,13 @@ export interface CommandCodeRuntimeOptions<TProviderConfig> {
cachePath: string cachePath: string
loadModels: () => Promise<LoadCommandCodeModelsResult> loadModels: () => Promise<LoadCommandCodeModelsResult>
createProviderConfig: (models: readonly CommandCodeModel[]) => TProviderConfig createProviderConfig: (models: readonly CommandCodeModel[]) => TProviderConfig
getTransport?: () => "unknown" | "provider" | "generate"
now?: () => number now?: () => number
logWarning?: (message: string) => void logWarning?: (message: string) => void
} }
export interface CommandCodeRuntimeStatus { export interface CommandCodeRuntimeStatus {
transport: "unknown" | "provider" | "generate"
source: LoadCommandCodeModelsResult["source"] source: LoadCommandCodeModelsResult["source"]
modelCount: number modelCount: number
lastSuccess?: number lastSuccess?: number
@@ -86,6 +88,7 @@ function formatTimestamp(timestamp: number | undefined): string {
export function formatCommandCodeStatus(status: CommandCodeRuntimeStatus): string { export function formatCommandCodeStatus(status: CommandCodeRuntimeStatus): string {
const lines = [ const lines = [
`transport: ${status.transport}`,
`source: ${status.source}`, `source: ${status.source}`,
`model count: ${status.modelCount}`, `model count: ${status.modelCount}`,
`last success: ${formatTimestamp(status.lastSuccess)}`, `last success: ${formatTimestamp(status.lastSuccess)}`,
@@ -113,6 +116,7 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
this.now = options.now ?? Date.now this.now = options.now ?? Date.now
this.logWarning = options.logWarning ?? ((message) => console.warn(`[commandcode] ${message}`)) this.logWarning = options.logWarning ?? ((message) => console.warn(`[commandcode] ${message}`))
const initialStatus: CommandCodeRuntimeStatus = { const initialStatus: CommandCodeRuntimeStatus = {
transport: "unknown",
source: "empty", source: "empty",
modelCount: 0, modelCount: 0,
cachePath: options.cachePath, cachePath: options.cachePath,
@@ -123,7 +127,10 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
} }
getStatus(): CommandCodeRuntimeStatus { getStatus(): CommandCodeRuntimeStatus {
return { ...this.status } return {
...this.status,
transport: this.options.getTransport?.() ?? "unknown",
}
} }
async initialize(): Promise<void> { async initialize(): Promise<void> {
@@ -259,10 +266,8 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
this.pi.registerCommand("commandcode-status", { this.pi.registerCommand("commandcode-status", {
description: "Show redacted Command Code provider diagnostics", description: "Show redacted Command Code provider diagnostics",
handler: async (_args, ctx) => { handler: async (_args, ctx) => {
ctx.ui.notify( const status = this.getStatus()
formatCommandCodeStatus(this.status), ctx.ui.notify(formatCommandCodeStatus(status), status.warning ? "warning" : "info")
this.status.warning ? "warning" : "info",
)
}, },
}) })
} }
+140
View File
@@ -0,0 +1,140 @@
import type {
AssistantMessageEvent,
AssistantMessageEventStreamLike,
ContextLike,
ModelLike,
StreamOptions,
} from "./types.ts"
export type CommandCodeTransport = "unknown" | "provider" | "generate"
interface TransportDependencies {
createStream: () => AssistantMessageEventStreamLike
streamProvider: (
model: ModelLike,
context: ContextLike,
options?: StreamOptions,
) => AssistantMessageEventStreamLike
streamGenerate: (
model: ModelLike,
context: ContextLike,
options?: StreamOptions,
) => AssistantMessageEventStreamLike
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
}
async function isUpgradeRequired(response: Response): Promise<boolean> {
if (response.status !== 403) return false
try {
const body: unknown = await response.clone().json()
if (!isRecord(body)) return false
const error = isRecord(body.error) ? body.error : body
return error.code === "upgrade_required"
} catch {
return false
}
}
export function createCommandCodeTransportRouter(deps: TransportDependencies) {
let transport: CommandCodeTransport = "unknown"
let apiKey: string | undefined
function pipe(
source: AssistantMessageEventStreamLike,
target: AssistantMessageEventStreamLike,
): Promise<void> {
return (async () => {
for await (const event of source) target.push(event)
})()
}
return {
getTransport(): CommandCodeTransport {
return transport
},
reset(): void {
transport = "unknown"
apiKey = undefined
},
stream(
model: ModelLike,
context: ContextLike,
options?: StreamOptions,
): AssistantMessageEventStreamLike {
if (options?.apiKey !== apiKey) {
apiKey = options?.apiKey
transport = "unknown"
}
const requestApiKey = options?.apiKey
if (transport === "generate") return deps.streamGenerate(model, context, options)
const output = deps.createStream()
let upgradeRequired = false
const fetchImpl = options?.fetch ?? fetch
const providerOptions: StreamOptions = {
...options,
fetch: async (input, init) => {
const response = await fetchImpl(input, init)
if (await isUpgradeRequired(response)) upgradeRequired = true
return response
},
onResponse: async (response, responseModel) => {
if (upgradeRequired) return
await options?.onResponse?.(response, responseModel)
},
}
const run = async () => {
const providerStream = deps.streamProvider(model, context, providerOptions)
for await (const event of providerStream) {
if (!upgradeRequired) {
if (apiKey === requestApiKey) transport = "provider"
output.push(event)
}
}
if (upgradeRequired) {
if (apiKey === requestApiKey) transport = "generate"
await pipe(deps.streamGenerate(model, context, options), output)
}
output.end()
}
run().catch((error: unknown) => {
const message = error instanceof Error ? error.message : String(error)
output.push({
type: "error",
reason: "error",
error: {
role: "assistant",
content: [],
api: model.api,
provider: model.provider,
model: model.id,
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "error",
errorMessage: message,
timestamp: Date.now(),
},
})
output.end()
})
return output
},
}
}
+1
View File
@@ -110,6 +110,7 @@ export interface StreamOptions {
apiKey?: string apiKey?: string
signal?: AbortSignal signal?: AbortSignal
headers?: Record<string, string> headers?: Record<string, string>
fetch?: typeof fetch
maxTokens?: number maxTokens?: number
/** Resolved pi thinking level; forwarded only through the model's map. */ /** Resolved pi thinking level; forwarded only through the model's map. */
reasoning?: string reasoning?: string
+6 -8
View File
@@ -1,5 +1,5 @@
{ {
"verifiedAt": "2026-08-04", "verifiedAt": "2026-08-20",
"source": "https://commandcode.ai/docs/resources/pricing-limits", "source": "https://commandcode.ai/docs/resources/pricing-limits",
"tierPolicy": "Use request-wide input tiers; the highest threshold exceeded by input plus cache tokens applies to the full request.", "tierPolicy": "Use request-wide input tiers; the highest threshold exceeded by input plus cache tokens applies to the full request.",
"tiers": { "tiers": {
@@ -7,9 +7,7 @@
"Qwen/Qwen3.7-Flash": [ "Qwen/Qwen3.7-Flash": [
[32000, 0.1, 0.4, 0.02, 0.125], [32000, 0.1, 0.4, 0.02, 0.125],
[256000, 0.2, 0.8, 0.04, 0.25] [256000, 0.2, 0.8, 0.04, 0.25]
], ]
"gpt-5.6-terra": [[272000, 2, 9, 0.2, 2.5]],
"gpt-5.6-luna": [[272000, 0.2, 0.9, 0.02, 0.25]]
}, },
"costs": { "costs": {
"poolside/laguna-s-2.1-free": [0, 0, 0, 0], "poolside/laguna-s-2.1-free": [0, 0, 0, 0],
@@ -27,8 +25,8 @@
"MiniMaxAI/MiniMax-M3": [0.3, 1.2, 0.06, 0], "MiniMaxAI/MiniMax-M3": [0.3, 1.2, 0.06, 0],
"MiniMaxAI/MiniMax-M2.7": [0.3, 1.2, 0.06, 0], "MiniMaxAI/MiniMax-M2.7": [0.3, 1.2, 0.06, 0],
"MiniMaxAI/MiniMax-M2.5": [0.3, 1.2, 0.03, 0], "MiniMaxAI/MiniMax-M2.5": [0.3, 1.2, 0.03, 0],
"deepseek/deepseek-v4-pro": [0.435, 0.87, 0.003625, 0], "deepseek/deepseek-v4-pro": [0.66, 1.98, 0.022, 0],
"deepseek/deepseek-v4-flash": [0.14, 0.28, 0.0028, 0], "deepseek/deepseek-v4-flash": [0.22, 0.66, 0.007, 0],
"Qwen/Qwen3.8-Max": [2, 6, 0.25, 2.5], "Qwen/Qwen3.8-Max": [2, 6, 0.25, 2.5],
"Qwen/Qwen3.7-Max": [2.5, 7.5, 0.5, 3.13], "Qwen/Qwen3.7-Max": [2.5, 7.5, 0.5, 3.13],
"Qwen/Qwen3.7-Plus": [0.4, 1.6, 0.08, 0.5], "Qwen/Qwen3.7-Plus": [0.4, 1.6, 0.08, 0.5],
@@ -52,8 +50,8 @@
"claude-opus-4-7": [5, 25, 0.5, 6.25], "claude-opus-4-7": [5, 25, 0.5, 6.25],
"claude-haiku-4-5-20251001": [1, 5, 0.1, 1.25], "claude-haiku-4-5-20251001": [1, 5, 0.1, 1.25],
"gpt-5.6-sol": [5, 30, 0.5, 6.25], "gpt-5.6-sol": [5, 30, 0.5, 6.25],
"gpt-5.6-terra": [1, 6, 0.1, 1.25], "gpt-5.6-terra": [2, 12, 0.2, 2.5],
"gpt-5.6-luna": [0.1, 0.6, 0.01, 0.125], "gpt-5.6-luna": [0.2, 1.2, 0.02, 0.25],
"gpt-5.5": [5, 30, 0.5, 0], "gpt-5.5": [5, 30, 0.5, 0],
"gpt-5.4": [2.5, 15, 0.25, 0], "gpt-5.4": [2.5, 15, 0.25, 0],
"gpt-5.3-codex": [2, 8, 0.5, 0], "gpt-5.3-codex": [2, 8, 0.5, 0],
+59
View File
@@ -0,0 +1,59 @@
import assert from "node:assert/strict"
import { mkdtemp, rm, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { describe, it } from "node:test"
import { getConfiguredApiKey } from "../src/api-key.ts"
async function withAuthFile(
value: unknown,
run: (authPath: string) => Promise<void>,
): Promise<void> {
const directory = await mkdtemp(join(tmpdir(), "pi-commandcode-auth-"))
const authPath = join(directory, "auth.json")
try {
await writeFile(authPath, JSON.stringify(value), "utf-8")
await run(authPath)
} finally {
await rm(directory, { recursive: true, force: true })
}
}
describe("getConfiguredApiKey()", () => {
it("prefers the environment variable", () => {
assert.equal(
getConfiguredApiKey({ env: { COMMANDCODE_API_KEY: "env-key" }, authPaths: [] }),
"env-key",
)
})
it("reads pi OAuth and API credentials", async () => {
const cases: readonly { credential: unknown; expected: string }[] = [
{
credential: { commandcode: { type: "oauth", access: "oauth-key" } },
expected: "oauth-key",
},
{ credential: { commandcode: { type: "api", key: "api-key" } }, expected: "api-key" },
{ credential: { "command-code": { type: "api", key: "cli-key" } }, expected: "cli-key" },
{ credential: { apiKey: "legacy-key" }, expected: "legacy-key" },
]
for (const testCase of cases) {
await withAuthFile(testCase.credential, async (authPath) => {
assert.equal(getConfiguredApiKey({ env: {}, authPaths: [authPath] }), testCase.expected)
})
}
})
it("ignores malformed files", async () => {
const directory = await mkdtemp(join(tmpdir(), "pi-commandcode-auth-"))
const authPath = join(directory, "auth.json")
try {
await writeFile(authPath, "not json", "utf-8")
assert.equal(getConfiguredApiKey({ env: {}, authPaths: [authPath] }), undefined)
} finally {
await rm(directory, { recursive: true, force: true })
}
})
})
+26
View File
@@ -25,6 +25,9 @@ import { fileURLToPath } from "node:url"
const projectDir = resolve(dirname(fileURLToPath(import.meta.url)), "..") const projectDir = resolve(dirname(fileURLToPath(import.meta.url)), "..")
const extensionPath = join(projectDir, "index.ts") const extensionPath = join(projectDir, "index.ts")
const testModel = process.env.COMMANDCODE_E2E_MODEL ?? "deepseek/deepseek-v4-flash" const testModel = process.env.COMMANDCODE_E2E_MODEL ?? "deepseek/deepseek-v4-flash"
const testProfile = process.env.COMMANDCODE_E2E_PROFILE
const expectedTransport =
testProfile === "go" ? "generate" : testProfile === "provider" ? "provider" : undefined
const marker = "commandcode-live-e2e-ok" const marker = "commandcode-live-e2e-ok"
function findPiBinary() { function findPiBinary() {
@@ -57,9 +60,18 @@ if (!piBin || !hasAuthMetadata()) {
process.exit(0) process.exit(0)
} }
const profileAgentDir = testProfile
? mkdtempSync(join(tmpdir(), `pi-commandcode-live-${testProfile}-agent-`))
: undefined
function safeEnv(overrides = {}) { function safeEnv(overrides = {}) {
const env = { ...process.env, PI_SKIP_VERSION_CHECK: "1", ...overrides } const env = { ...process.env, PI_SKIP_VERSION_CHECK: "1", ...overrides }
if (testProfile && profileAgentDir) {
env.PI_CODING_AGENT_DIR = profileAgentDir
env.COMMANDCODE_MODELS_CACHE = join(profileAgentDir, "commandcode-models.json")
} else {
delete env.COMMANDCODE_API_KEY delete env.COMMANDCODE_API_KEY
}
return env return env
} }
@@ -228,12 +240,22 @@ try {
return { firstThinkingDeltas, secondThinkingDeltas, stderr: getStderr() } return { firstThinkingDeltas, secondThinkingDeltas, stderr: getStderr() }
}) })
if (testProfile !== "provider") {
assert.ok(multiTurn.firstThinkingDeltas > 0, "first turn should stream reasoning") assert.ok(multiTurn.firstThinkingDeltas > 0, "first turn should stream reasoning")
assert.ok(multiTurn.secondThinkingDeltas > 0, "follow-up turn should stream fresh reasoning") assert.ok(multiTurn.secondThinkingDeltas > 0, "follow-up turn should stream fresh reasoning")
}
assert.doesNotMatch(multiTurn.stderr, /Bearer\s+\S+/i) assert.doesNotMatch(multiTurn.stderr, /Bearer\s+\S+/i)
console.log("[live-e2e] live runtime refresh/status commands") console.log("[live-e2e] live runtime refresh/status commands")
const runtime = await runRpc(extensionPath, async ({ send, waitFor, getStderr }) => { const runtime = await runRpc(extensionPath, async ({ send, waitFor, getStderr }) => {
if (expectedTransport) {
send({ id: "transport-probe", type: "prompt", message: `Reply exactly: ${marker}` })
await waitFor(
(event) => event.type === "response" && event.id === "transport-probe" && event.success,
)
await waitFor((event) => event.type === "agent_settled")
}
send({ id: "commands", type: "get_commands" }) send({ id: "commands", type: "get_commands" })
const commands = await waitFor( const commands = await waitFor(
(event) => event.type === "response" && event.id === "commands" && event.success, (event) => event.type === "response" && event.id === "commands" && event.success,
@@ -264,6 +286,7 @@ try {
assert.ok(runtime.names.includes("commandcode-refresh")) assert.ok(runtime.names.includes("commandcode-refresh"))
assert.ok(runtime.names.includes("commandcode-status")) assert.ok(runtime.names.includes("commandcode-status"))
assert.match(runtime.refresh, /model catalog (?:refreshed|unchanged)/) assert.match(runtime.refresh, /model catalog (?:refreshed|unchanged)/)
if (expectedTransport) assert.match(runtime.status, new RegExp(`transport: ${expectedTransport}`))
assert.match(runtime.status, /source: (?:live|cache)/) assert.match(runtime.status, /source: (?:live|cache)/)
assert.match(runtime.status, /model count: [1-9][0-9]*/) assert.match(runtime.status, /model count: [1-9][0-9]*/)
assert.doesNotMatch(`${runtime.refresh}\n${runtime.status}\n${runtime.stderr}`, /Bearer\s+\S+/i) assert.doesNotMatch(`${runtime.refresh}\n${runtime.status}\n${runtime.stderr}`, /Bearer\s+\S+/i)
@@ -296,6 +319,7 @@ try {
assert.match(toolResult.stdout, new RegExp(marker)) assert.match(toolResult.stdout, new RegExp(marker))
assert.equal(readFileSync(targetPath, "utf-8"), marker) assert.equal(readFileSync(targetPath, "utf-8"), marker)
if (testProfile !== "provider") {
console.log("[live-e2e] image rejection through real RPC host") console.log("[live-e2e] image rejection through real RPC host")
const image = await runRpc(extensionPath, async ({ send, waitFor, events }) => { const image = await runRpc(extensionPath, async ({ send, waitFor, events }) => {
send({ send({
@@ -320,6 +344,7 @@ try {
/does not support image content/i.test(event.message?.errorMessage ?? ""), /does not support image content/i.test(event.message?.errorMessage ?? ""),
), ),
) )
}
console.log("[live-e2e] packed artifact with existing authentication") console.log("[live-e2e] packed artifact with existing authentication")
const packDir = join(tempRoot, "pack") const packDir = join(tempRoot, "pack")
@@ -361,4 +386,5 @@ try {
console.log("[live-e2e] PASS") console.log("[live-e2e] PASS")
} finally { } finally {
rmSync(tempRoot, { recursive: true, force: true }) rmSync(tempRoot, { recursive: true, force: true })
if (profileAgentDir) rmSync(profileAgentDir, { recursive: true, force: true })
} }
+16
View File
@@ -5,6 +5,8 @@ import { join } from "node:path"
import { describe, it } from "node:test" import { describe, it } from "node:test"
import { import {
apiForModelId,
baseUrlForModel,
commandCodeModelsFromApiResponse, commandCodeModelsFromApiResponse,
commandCodeModelsFromCache, commandCodeModelsFromCache,
DEFAULT_MODELS_TIMEOUT_MS, DEFAULT_MODELS_TIMEOUT_MS,
@@ -37,6 +39,7 @@ const EXPECTED_MODELS: readonly CommandCodeModel[] = [
{ {
id: "Qwen/Qwen3.7-Max", id: "Qwen/Qwen3.7-Max",
name: "Qwen 3.7 Max (CC)", name: "Qwen 3.7 Max (CC)",
api: "openai-completions",
reasoning: false, reasoning: false,
contextWindow: 1_000_000, contextWindow: 1_000_000,
maxTokens: 65_536, maxTokens: 65_536,
@@ -84,6 +87,19 @@ describe("commandCodeModelsFromApiResponse()", () => {
assert.deepEqual(commandCodeModelsFromApiResponse(API_RESPONSE), EXPECTED_MODELS) assert.deepEqual(commandCodeModelsFromApiResponse(API_RESPONSE), EXPECTED_MODELS)
}) })
it("routes Claude models to Anthropic Messages and all others to Chat Completions", () => {
assert.equal(apiForModelId("claude-sonnet-4-6"), "anthropic-messages")
assert.equal(apiForModelId("gpt-5.6-sol"), "openai-completions")
assert.equal(
baseUrlForModel("https://api.commandcode.ai/provider/v1/", "openai-completions"),
"https://api.commandcode.ai/provider/v1",
)
assert.equal(
baseUrlForModel("https://api.commandcode.ai/provider/v1/", "anthropic-messages"),
"https://api.commandcode.ai/provider",
)
})
it("matches command-code@1.15.1 image input capabilities", () => { it("matches command-code@1.15.1 image input capabilities", () => {
assert.deepEqual(inputModalitiesForModel("gpt-5.6-luna"), ["text", "image"]) assert.deepEqual(inputModalitiesForModel("gpt-5.6-luna"), ["text", "image"])
assert.deepEqual(inputModalitiesForModel("meta/muse-spark-1.2"), ["text", "image"]) assert.deepEqual(inputModalitiesForModel("meta/muse-spark-1.2"), ["text", "image"])
+37 -6
View File
@@ -186,7 +186,7 @@ describe("login()", () => {
authUrl = params.url authUrl = params.url
}, },
onPrompt(_params: { message: string }): Promise<string> { onPrompt(_params: { message: string }): Promise<string> {
throw new Error("onPrompt should not be called in browser flow") return Promise.resolve("")
}, },
} }
@@ -239,7 +239,7 @@ describe("login()", () => {
process.env.COMMANDCODE_AUTH_TIMEOUT_MS = "1" process.env.COMMANDCODE_AUTH_TIMEOUT_MS = "1"
let authUrl = "" let authUrl = ""
let promptMessage = "" const promptMessages: string[] = []
try { try {
const result = await login({ const result = await login({
@@ -247,13 +247,13 @@ describe("login()", () => {
authUrl = params.url authUrl = params.url
}, },
async onPrompt(params: { message: string }): Promise<string> { async onPrompt(params: { message: string }): Promise<string> {
promptMessage = params.message promptMessages.push(params.message)
return "\u001b[200~ user_manualApiKey\n\u001b[201~" return promptMessages.length === 1 ? "" : "\u001b[200~ user_manualApiKey\n\u001b[201~"
}, },
}) })
assert.match(authUrl, /^https:\/\/commandcode\.ai\/studio\/auth\/cli\?/) assert.match(authUrl, /^https:\/\/commandcode\.ai\/studio\/auth\/cli\?/)
assert.match(promptMessage, /Paste your Command Code API key/) assert.match(promptMessages[1] ?? "", /Paste your Command Code API key/)
assert.equal(result.access, "user_manualApiKey") assert.equal(result.access, "user_manualApiKey")
assert.equal(result.refresh, "user_manualApiKey") assert.equal(result.refresh, "user_manualApiKey")
assert.ok(result.expires > Date.now(), "expiry should be far in the future") assert.ok(result.expires > Date.now(), "expiry should be far in the future")
@@ -263,6 +263,37 @@ describe("login()", () => {
} }
}) })
it("accepts a directly pasted API key", async () => {
let authOpened = false
const result = await login({
onAuth() {
authOpened = true
},
onPrompt(): Promise<string> {
return Promise.resolve("user_directApiKey")
},
})
assert.equal(authOpened, false)
assert.equal(result.access, "user_directApiKey")
})
it("offers an explicit API key prompt", async () => {
let promptCount = 0
const result = await login({
onAuth() {
throw new Error("browser should not open")
},
onPrompt(): Promise<string> {
promptCount += 1
return Promise.resolve(promptCount === 1 ? "key" : "user_promptedApiKey")
},
})
assert.equal(result.access, "user_promptedApiKey")
assert.equal(promptCount, 2)
})
it("rejects on state token mismatch", async () => { it("rejects on state token mismatch", async () => {
let authUrl = "" let authUrl = ""
const callbacks = { const callbacks = {
@@ -270,7 +301,7 @@ describe("login()", () => {
authUrl = params.url authUrl = params.url
}, },
onPrompt(_params: { message: string }): Promise<string> { onPrompt(_params: { message: string }): Promise<string> {
throw new Error("should not prompt") return Promise.resolve("")
}, },
} }
+13 -8
View File
@@ -77,7 +77,7 @@ const server = createServer((req, res) => {
return return
} }
if (req.method !== "POST" || req.url !== "/alpha/generate") { if (req.method !== "POST" || req.url !== "/provider/v1/chat/completions") {
res.writeHead(404) res.writeHead(404)
res.end("Not found") res.end("Not found")
return return
@@ -103,14 +103,19 @@ const server = createServer((req, res) => {
} }
res.writeHead(200, { res.writeHead(200, {
"Content-Type": "text/plain; charset=utf-8", "Content-Type": "text/event-stream; charset=utf-8",
"Transfer-Encoding": "chunked", "Transfer-Encoding": "chunked",
}) })
res.write(`${JSON.stringify({ type: "text-delta", text: "mock-omp-ok" })}\n`)
res.write( res.write(
`${JSON.stringify({ type: "finish", finishReason: "stop", totalUsage: { inputTokens: 1, outputTokens: 1 } })}\n`, `data: ${JSON.stringify({ id: "mock", object: "chat.completion.chunk", choices: [{ index: 0, delta: { role: "assistant", content: "mock-omp-ok" }, finish_reason: null }] })}\n\n`,
) )
res.end() res.write(
`data: ${JSON.stringify({ id: "mock", object: "chat.completion.chunk", choices: [{ index: 0, delta: {}, finish_reason: "stop" }] })}\n\n`,
)
res.write(
`data: ${JSON.stringify({ id: "mock", object: "chat.completion.chunk", choices: [], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } })}\n\n`,
)
res.end("data: [DONE]\n\n")
}) })
}) })
@@ -129,7 +134,7 @@ function runOmp(args, timeoutMs = 30_000) {
USERPROFILE: tempHome, USERPROFILE: tempHome,
PI_CODING_AGENT_DIR: join(tempHome, ".omp", "agent"), PI_CODING_AGENT_DIR: join(tempHome, ".omp", "agent"),
COMMANDCODE_API_KEY: "mock-key", COMMANDCODE_API_KEY: "mock-key",
COMMANDCODE_API_BASE: apiBase, COMMANDCODE_API_BASE: `${apiBase}/provider/v1`,
COMMANDCODE_MODELS_URL: `${apiBase}/provider/v1/models`, COMMANDCODE_MODELS_URL: `${apiBase}/provider/v1/models`,
}, },
stdio: ["ignore", "pipe", "pipe"], stdio: ["ignore", "pipe", "pipe"],
@@ -185,8 +190,8 @@ try {
"Bearer mock-key", "Bearer mock-key",
"should send the resolved env-var value, not the literal var name", "should send the resolved env-var value, not the literal var name",
) )
assert.equal(lastRequestBody?.params?.model, TEST_MODEL) assert.equal(lastRequestBody?.model, TEST_MODEL)
assert.equal(typeof lastRequestBody?.params?.system, "string") assert.ok(Array.isArray(lastRequestBody?.messages))
console.log("[omp-compat] PASS") console.log("[omp-compat] PASS")
} finally { } finally {
+108 -35
View File
@@ -15,7 +15,8 @@ import { fileURLToPath } from "node:url"
const __dirname = dirname(fileURLToPath(import.meta.url)) const __dirname = dirname(fileURLToPath(import.meta.url))
const PROJECT_DIR = resolve(__dirname, "..") const PROJECT_DIR = resolve(__dirname, "..")
const EXT_PATH = resolve(PROJECT_DIR, "index.ts") const EXT_PATH = resolve(PROJECT_DIR, "index.ts")
const TEST_MODEL = "deepseek/deepseek-v4-flash" const TEST_MODEL = "gpt-5.4"
const CLAUDE_TEST_MODEL = "claude-sonnet-4-6"
function findPiBinary() { function findPiBinary() {
if (process.env.PI_BIN) return process.env.PI_BIN if (process.env.PI_BIN) return process.env.PI_BIN
@@ -63,9 +64,17 @@ function modelCatalog() {
object: "model", object: "model",
created: 1779824324, created: 1779824324,
owned_by: "command-code", owned_by: "command-code",
name: "DeepSeek V4 Flash", name: "GPT 5.4",
context_length: 1_000_000, context_length: 1_000_000,
}, },
{
id: CLAUDE_TEST_MODEL,
object: "model",
created: 1779824324,
owned_by: "command-code",
name: "Claude Sonnet 4.6",
context_length: 200_000,
},
{ {
id: "cc-second-model", id: "cc-second-model",
object: "model", object: "model",
@@ -101,7 +110,9 @@ const server = createServer((req, res) => {
return return
} }
if (req.method !== "POST" || req.url !== "/alpha/generate") { const isOpenAIRequest = req.method === "POST" && req.url === "/provider/v1/chat/completions"
const isAnthropicRequest = req.method === "POST" && req.url === "/provider/v1/messages"
if (!isOpenAIRequest && !isAnthropicRequest) {
res.writeHead(404) res.writeHead(404)
res.end("Not found") res.end("Not found")
return return
@@ -129,12 +140,20 @@ const server = createServer((req, res) => {
if (overflowMode && overflowRequestCount === 2) { if (overflowMode && overflowRequestCount === 2) {
res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" }) res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" })
res.end(JSON.stringify({ error: { message: "Input exceeds context limit" } })) res.end(
JSON.stringify({
error: {
message: "Input exceeds context limit",
type: "invalid_request_error",
code: "context_length_exceeded",
},
}),
)
return return
} }
res.writeHead(200, { res.writeHead(200, {
"Content-Type": "text/plain; charset=utf-8", "Content-Type": "text/event-stream; charset=utf-8",
"Transfer-Encoding": "chunked", "Transfer-Encoding": "chunked",
}) })
const text = overflowMode const text = overflowMode
@@ -144,11 +163,36 @@ const server = createServer((req, res) => {
? "compaction-summary" ? "compaction-summary"
: "overflow-recovered" : "overflow-recovered"
: "mock-pi-ok" : "mock-pi-ok"
res.write(`${JSON.stringify({ type: "text-delta", text })}\n`) if (isAnthropicRequest) {
res.write( res.write(
`${JSON.stringify({ type: "finish", finishReason: "stop", totalUsage: { inputTokens: 1, outputTokens: 1 } })}\n`, `event: message_start\ndata: ${JSON.stringify({ type: "message_start", message: { id: "mock", type: "message", role: "assistant", content: [], model: CLAUDE_TEST_MODEL, stop_reason: null, stop_sequence: null, usage: { input_tokens: 1, output_tokens: 0 } } })}\n\n`,
) )
res.end() res.write(
`event: content_block_start\ndata: ${JSON.stringify({ type: "content_block_start", index: 0, content_block: { type: "text", text: "" } })}\n\n`,
)
res.write(
`event: content_block_delta\ndata: ${JSON.stringify({ type: "content_block_delta", index: 0, delta: { type: "text_delta", text } })}\n\n`,
)
res.write(
`event: content_block_stop\ndata: ${JSON.stringify({ type: "content_block_stop", index: 0 })}\n\n`,
)
res.write(
`event: message_delta\ndata: ${JSON.stringify({ type: "message_delta", delta: { stop_reason: "end_turn", stop_sequence: null }, usage: { output_tokens: 1 } })}\n\n`,
)
res.end(`event: message_stop\ndata: ${JSON.stringify({ type: "message_stop" })}\n\n`)
return
}
res.write(
`data: ${JSON.stringify({ id: "mock", object: "chat.completion.chunk", choices: [{ index: 0, delta: { role: "assistant", content: text }, finish_reason: null }] })}\n\n`,
)
res.write(
`data: ${JSON.stringify({ id: "mock", object: "chat.completion.chunk", choices: [{ index: 0, delta: {}, finish_reason: "stop" }] })}\n\n`,
)
res.write(
`data: ${JSON.stringify({ id: "mock", object: "chat.completion.chunk", choices: [], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } })}\n\n`,
)
res.end("data: [DONE]\n\n")
}) })
}) })
@@ -170,8 +214,9 @@ const env = {
USERPROFILE: tempHome, USERPROFILE: tempHome,
PI_CODING_AGENT_DIR: agentDir, PI_CODING_AGENT_DIR: agentDir,
PI_CODING_AGENT_SESSION_DIR: join(tempHome, "sessions"), PI_CODING_AGENT_SESSION_DIR: join(tempHome, "sessions"),
COMMANDCODE_API_BASE: apiBase, COMMANDCODE_API_BASE: `${apiBase}/provider/v1`,
COMMANDCODE_API_KEY: "mock-key", COMMANDCODE_API_KEY: "mock-key",
COMMANDCODE_ZDR: "1",
COMMANDCODE_MODELS_URL: `${apiBase}/provider/v1/models`, COMMANDCODE_MODELS_URL: `${apiBase}/provider/v1/models`,
} }
@@ -403,7 +448,7 @@ async function runRpcExtensionCommands(timeoutMs = 30_000) {
event.type === "extension_ui_request" && event.type === "extension_ui_request" &&
event.method === "notify" && event.method === "notify" &&
typeof event.message === "string" && typeof event.message === "string" &&
event.message.includes("model count: 2"), event.message.includes("model count: 3"),
) )
includeRefreshedModel = true includeRefreshedModel = true
@@ -414,7 +459,7 @@ async function runRpcExtensionCommands(timeoutMs = 30_000) {
event.type === "extension_ui_request" && event.type === "extension_ui_request" &&
event.method === "notify" && event.method === "notify" &&
typeof event.message === "string" && typeof event.message === "string" &&
event.message.includes("3 models from live"), event.message.includes("4 models from live"),
) )
send({ id: "status-after", type: "prompt", message: "/commandcode-status" }) send({ id: "status-after", type: "prompt", message: "/commandcode-status" })
@@ -426,7 +471,7 @@ async function runRpcExtensionCommands(timeoutMs = 30_000) {
event.type === "extension_ui_request" && event.type === "extension_ui_request" &&
event.method === "notify" && event.method === "notify" &&
typeof event.message === "string" && typeof event.message === "string" &&
event.message.includes("model count: 3"), event.message.includes("model count: 4"),
) )
return { return {
@@ -565,7 +610,7 @@ try {
) )
assert.equal(recoveryList.code, 0, recoveryList.stderr) assert.equal(recoveryList.code, 0, recoveryList.stderr)
const recoveryOutput = recoveryList.stdout || recoveryList.stderr const recoveryOutput = recoveryList.stdout || recoveryList.stderr
assert.match(recoveryOutput, /deepseek\/deepseek-v4-flash/) assert.match(recoveryOutput, /gpt-5\.4/)
assert.match(recoveryOutput, /cc-second-model/) assert.match(recoveryOutput, /cc-second-model/)
assert.doesNotMatch(recoveryList.stderr, /no valid cached catalog/) assert.doesNotMatch(recoveryList.stderr, /no valid cached catalog/)
assert.doesNotMatch(recoveryList.stderr, /Failed to load extension/) assert.doesNotMatch(recoveryList.stderr, /Failed to load extension/)
@@ -578,7 +623,7 @@ try {
assert.equal(list.code, 0, list.stderr) assert.equal(list.code, 0, list.stderr)
const listOutput = list.stdout || list.stderr const listOutput = list.stdout || list.stderr
assert.match(listOutput, /commandcode/) assert.match(listOutput, /commandcode/)
assert.match(listOutput, /deepseek\/deepseek-v4-flash/) assert.match(listOutput, /gpt-5\.4/)
assert.match(listOutput, /cc-second-model/) assert.match(listOutput, /cc-second-model/)
assert.equal(modelListRequestCount, 1) assert.equal(modelListRequestCount, 1)
assert.doesNotThrow(() => accessSync(modelsCachePath, constants.R_OK)) assert.doesNotThrow(() => accessSync(modelsCachePath, constants.R_OK))
@@ -591,7 +636,7 @@ try {
) )
assert.equal(offlineList.code, 0, offlineList.stderr) assert.equal(offlineList.code, 0, offlineList.stderr)
const offlineListOutput = offlineList.stdout || offlineList.stderr const offlineListOutput = offlineList.stdout || offlineList.stderr
assert.match(offlineListOutput, /deepseek\/deepseek-v4-flash/) assert.match(offlineListOutput, /gpt-5\.4/)
assert.match(offlineListOutput, /cc-second-model/) assert.match(offlineListOutput, /cc-second-model/)
assert.match(offlineList.stderr, /Using the cached catalog/) assert.match(offlineList.stderr, /Using the cached catalog/)
@@ -659,27 +704,55 @@ try {
lastRequestHeaders.authorization.startsWith("Bearer "), lastRequestHeaders.authorization.startsWith("Bearer "),
"should send a bearer Authorization header", "should send a bearer Authorization header",
) )
assert.equal(lastRequestBody?.params?.model, TEST_MODEL) assert.equal(lastRequestHeaders["x-cmd-zdr"], "1")
assert.equal(lastRequestBody?.params?.reasoning_effort, "high") assert.equal(lastRequestBody?.model, TEST_MODEL)
const sentTools = lastRequestBody?.params?.tools assert.equal(lastRequestBody?.reasoning_effort, "high")
const sentTools = lastRequestBody?.tools
assert.ok(Array.isArray(sentTools) && sentTools.length > 0) assert.ok(Array.isArray(sentTools) && sentTools.length > 0)
const editTool = sentTools.find((tool) => tool.name === "edit") const editTool = sentTools.find((tool) => tool.function?.name === "edit")
assert.equal(editTool?.input_schema?.type, "object") assert.equal(editTool?.function?.parameters?.type, "object")
assert.equal(editTool?.input_schema?.properties?.edits?.type, "array") assert.equal(editTool?.function?.parameters?.properties?.edits?.type, "array")
assert.equal(editTool?.input_schema?.properties?.edits?.items?.type, "object") assert.equal(editTool?.function?.parameters?.properties?.edits?.items?.type, "object")
assert.equal( assert.equal(
editTool?.input_schema?.properties?.edits?.items?.properties?.oldText?.type, editTool?.function?.parameters?.properties?.edits?.items?.properties?.oldText?.type,
"string", "string",
) )
console.log("[pi-local] Claude request through Anthropic Messages endpoint")
requestCount = 0
const claudePrint = await runPi(
[
"--no-extensions",
"-e",
EXT_PATH,
"-p",
"say mock token",
"--provider",
"commandcode",
"--model",
CLAUDE_TEST_MODEL,
"--thinking",
"high",
],
30_000,
)
assert.equal(claudePrint.code, 0, claudePrint.stderr)
assert.match(claudePrint.stdout, /mock-pi-ok/)
assert.equal(requestCount, 1)
assert.equal(lastRequestBody?.model, CLAUDE_TEST_MODEL)
assert.equal(lastRequestBody?.thinking?.type, "adaptive")
assert.deepEqual(lastRequestBody?.output_config, { effort: "high" })
assert.equal(lastRequestHeaders["x-api-key"], "mock-key")
assert.equal(lastRequestHeaders["x-cmd-zdr"], "1")
console.log("[pi-local] runtime commands through real RPC extension lifecycle") console.log("[pi-local] runtime commands through real RPC extension lifecycle")
includeRefreshedModel = false includeRefreshedModel = false
const runtimeCommands = await runRpcExtensionCommands() const runtimeCommands = await runRpcExtensionCommands()
assert.ok(runtimeCommands.commandNames.includes("commandcode-refresh")) assert.ok(runtimeCommands.commandNames.includes("commandcode-refresh"))
assert.ok(runtimeCommands.commandNames.includes("commandcode-status")) assert.ok(runtimeCommands.commandNames.includes("commandcode-status"))
assert.match(runtimeCommands.statusBefore, /source: live/) assert.match(runtimeCommands.statusBefore, /source: live/)
assert.match(runtimeCommands.refreshNotification, /3 models from live/) assert.match(runtimeCommands.refreshNotification, /4 models from live/)
assert.match(runtimeCommands.statusAfter, /model count: 3/) assert.match(runtimeCommands.statusAfter, /model count: 4/)
assert.doesNotMatch( assert.doesNotMatch(
`${runtimeCommands.statusBefore}\n${runtimeCommands.statusAfter}\n${runtimeCommands.stderr}`, `${runtimeCommands.statusBefore}\n${runtimeCommands.statusAfter}\n${runtimeCommands.stderr}`,
/mock-key/, /mock-key/,
@@ -702,7 +775,7 @@ try {
assert.equal(rpc.sawTextDelta, true) assert.equal(rpc.sawTextDelta, true)
assert.equal(requestCount, 1) assert.equal(requestCount, 1)
console.log("[pi-local] reject image input through real RPC preflight/provider path") console.log("[pi-local] forward image input through the documented provider schema")
requestCount = 0 requestCount = 0
const imageRpc = await runRpcQuery(10_000, "describe image", [], { const imageRpc = await runRpcQuery(10_000, "describe image", [], {
images: [ images: [
@@ -713,14 +786,15 @@ try {
}, },
], ],
}) })
assert.equal(requestCount, 0) assert.equal(imageRpc.ok, true, imageRpc.stderr)
assert.equal(requestCount, 1)
const imageContent = lastRequestBody?.messages?.find(
(message) => message.role === "user",
)?.content
assert.ok(Array.isArray(imageContent), JSON.stringify(lastRequestBody?.messages))
assert.ok( assert.ok(
imageRpc.events.some( imageContent.some((part) => part.type === "image_url"),
(event) => JSON.stringify(imageContent),
event.type === "message_end" &&
event.message?.role === "assistant" &&
event.message?.stopReason === "error",
) || imageRpc.stderr.includes("does not support image"),
) )
console.log("[pi-local] verify overflow normalization and compaction recovery") console.log("[pi-local] verify overflow normalization and compaction recovery")
@@ -729,8 +803,7 @@ try {
const overflowRpc = await runRpcOverflowRecovery() const overflowRpc = await runRpcOverflowRecovery()
assert.equal(overflowRpc.ok, true) assert.equal(overflowRpc.ok, true)
assert.ok(overflowRpc.requests >= 4) assert.ok(overflowRpc.requests >= 4)
assert.equal(overflowRpc.sawNormalizedOverflow, true) assert.equal(overflowRpc.sawCompactionRetry, true, JSON.stringify(overflowRpc))
assert.equal(overflowRpc.sawCompactionRetry, true)
assert.equal(overflowRpc.stderrHasSecrets, false) assert.equal(overflowRpc.stderrHasSecrets, false)
overflowMode = false overflowMode = false
+20 -8
View File
@@ -108,10 +108,16 @@ describe("MODEL_COSTS pricing overlay", () => {
}) })
it("matches corrected official rates", () => { it("matches corrected official rates", () => {
assertCost("deepseek/deepseek-v4-pro", {
input: 0.66,
output: 1.98,
cacheRead: 0.022,
cacheWrite: 0,
})
assertCost("deepseek/deepseek-v4-flash", { assertCost("deepseek/deepseek-v4-flash", {
input: 0.14, input: 0.22,
output: 0.28, output: 0.66,
cacheRead: 0.0028, cacheRead: 0.007,
cacheWrite: 0, cacheWrite: 0,
}) })
assertCost("Qwen/Qwen3.7-Max", { assertCost("Qwen/Qwen3.7-Max", {
@@ -148,16 +154,22 @@ describe("MODEL_COSTS pricing overlay", () => {
cacheWrite: 0.038, cacheWrite: 0.038,
}) })
assertCost("gpt-5.6-terra", { assertCost("gpt-5.6-terra", {
input: 1, input: 2,
output: 6, output: 12,
cacheRead: 0.1, cacheRead: 0.2,
cacheWrite: 1.25, cacheWrite: 2.5,
})
assertCost("gpt-5.6-luna", {
input: 0.2,
output: 1.2,
cacheRead: 0.02,
cacheWrite: 0.25,
}) })
}) })
it("tracks pricing provenance", () => { it("tracks pricing provenance", () => {
assert.equal(PRICING_SOURCE_URL, "https://commandcode.ai/docs/resources/pricing-limits") assert.equal(PRICING_SOURCE_URL, "https://commandcode.ai/docs/resources/pricing-limits")
assert.equal(PRICING_LAST_VERIFIED, "2026-08-04") assert.equal(PRICING_LAST_VERIFIED, "2026-08-20")
}) })
it("fails once temporary pricing needs review", () => { it("fails once temporary pricing needs review", () => {
+4
View File
@@ -49,6 +49,7 @@ class CommandContext implements CommandCodeCommandContext {
const FIRST_MODEL: CommandCodeModel = { const FIRST_MODEL: CommandCodeModel = {
id: "first-model", id: "first-model",
name: "First Model", name: "First Model",
api: "openai-completions",
reasoning: true, reasoning: true,
contextWindow: 128_000, contextWindow: 128_000,
maxTokens: 16_384, maxTokens: 16_384,
@@ -57,6 +58,7 @@ const FIRST_MODEL: CommandCodeModel = {
const SECOND_MODEL: CommandCodeModel = { const SECOND_MODEL: CommandCodeModel = {
id: "second-model", id: "second-model",
name: "Second Model", name: "Second Model",
api: "openai-completions",
reasoning: true, reasoning: true,
contextWindow: 256_000, contextWindow: 256_000,
maxTokens: 32_768, maxTokens: 32_768,
@@ -96,6 +98,7 @@ describe("Command Code runtime", () => {
cachePath: "/tmp/commandcode-models.json", cachePath: "/tmp/commandcode-models.json",
loadModels: () => firstLoad.promise, loadModels: () => firstLoad.promise,
createProviderConfig: (models) => ({ models }), createProviderConfig: (models) => ({ models }),
getTransport: () => "provider",
now: () => now, now: () => now,
logWarning: () => {}, logWarning: () => {},
}) })
@@ -113,6 +116,7 @@ describe("Command Code runtime", () => {
assert.ok(statusCommand) assert.ok(statusCommand)
await statusCommand("", context) await statusCommand("", context)
const statusMessage = context.notifications.at(-1)?.message ?? "" const statusMessage = context.notifications.at(-1)?.message ?? ""
assert.match(statusMessage, /transport: provider/)
assert.match(statusMessage, /source: live/) assert.match(statusMessage, /source: live/)
assert.match(statusMessage, /model count: 1/) assert.match(statusMessage, /model count: 1/)
assert.match(statusMessage, /last success:/) assert.match(statusMessage, /last success:/)
+247
View File
@@ -0,0 +1,247 @@
import assert from "node:assert/strict"
import { describe, it } from "node:test"
import { createCommandCodeTransportRouter } from "../src/transport.ts"
import type {
AssistantMessageEvent,
AssistantMessageEventStreamLike,
StreamOptions,
} from "../src/types.ts"
import { collectEvents, createTestEventStream, makeContext, makeModel } from "./helpers.ts"
function completedStream(text: string): AssistantMessageEventStreamLike {
const stream = createTestEventStream()
const model = makeModel()
const message = {
role: "assistant" as const,
content: [{ type: "text" as const, text }],
api: model.api,
provider: model.provider,
model: model.id,
usage: {
input: 1,
output: 1,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 2,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop" as const,
timestamp: Date.now(),
}
const events: AssistantMessageEvent[] = [
{ type: "start", partial: message },
{ type: "text_start", contentIndex: 0, partial: message },
{ type: "text_delta", contentIndex: 0, delta: text, partial: message },
{ type: "text_end", contentIndex: 0, content: text, partial: message },
{ type: "done", reason: "stop", message },
]
for (const event of events) stream.push(event)
stream.end()
return stream
}
function providerStream(
response: Response,
text: string,
options?: StreamOptions,
): AssistantMessageEventStreamLike {
const stream = createTestEventStream()
const run = async () => {
const received = await (options?.fetch ?? fetch)("https://provider.test", {})
await options?.onResponse?.(
{ status: received.status, headers: {} },
makeModel({ api: "openai-completions" }),
)
const source = completedStream(text)
for await (const event of source) stream.push(event)
stream.end()
}
run().catch(() => stream.end())
return stream
}
describe("Command Code transport router", () => {
it("keeps using the Provider API after a successful request", async () => {
let providerCalls = 0
let generateCalls = 0
const router = createCommandCodeTransportRouter({
createStream: createTestEventStream,
streamProvider: (_model, _context, options) => {
providerCalls += 1
return providerStream(new Response("ok", { status: 200 }), "provider", options)
},
streamGenerate: () => {
generateCalls += 1
return completedStream("generate")
},
})
const options: StreamOptions = {
fetch: () => Promise.resolve(new Response("ok", { status: 200 })),
}
const first = await collectEvents(router.stream(makeModel(), makeContext(), options))
const second = await collectEvents(router.stream(makeModel(), makeContext(), options))
assert.equal(first.at(-1)?.type, "done")
assert.equal(second.at(-1)?.type, "done")
assert.equal(router.getTransport(), "provider")
assert.equal(providerCalls, 2)
assert.equal(generateCalls, 0)
})
it("falls back only for 403 upgrade_required and remembers generate", async () => {
let providerCalls = 0
let generateCalls = 0
const responseBody = JSON.stringify({
error: { code: "upgrade_required", type: "permission_error" },
})
const router = createCommandCodeTransportRouter({
createStream: createTestEventStream,
streamProvider: (_model, _context, options) => {
providerCalls += 1
return providerStream(new Response(responseBody, { status: 403 }), "blocked", options)
},
streamGenerate: () => {
generateCalls += 1
return completedStream("generate")
},
})
const options: StreamOptions = {
fetch: () => Promise.resolve(new Response(responseBody, { status: 403 })),
}
const first = await collectEvents(router.stream(makeModel(), makeContext(), options))
const second = await collectEvents(router.stream(makeModel(), makeContext(), options))
assert.equal(first.at(-1)?.type, "done")
assert.equal(second.at(-1)?.type, "done")
assert.equal(router.getTransport(), "generate")
assert.equal(providerCalls, 1)
assert.equal(generateCalls, 2)
})
it("re-detects the transport after the API key changes", async () => {
let providerCalls = 0
let generateCalls = 0
const upgradeBody = JSON.stringify({ error: { code: "upgrade_required" } })
const router = createCommandCodeTransportRouter({
createStream: createTestEventStream,
streamProvider: (_model, _context, options) => {
providerCalls += 1
const response =
options?.apiKey === "go-key"
? new Response(upgradeBody, { status: 403 })
: new Response("ok", { status: 200 })
return providerStream(response, "provider", options)
},
streamGenerate: () => {
generateCalls += 1
return completedStream("generate")
},
})
await collectEvents(
router.stream(makeModel(), makeContext(), {
apiKey: "go-key",
fetch: () => Promise.resolve(new Response(upgradeBody, { status: 403 })),
}),
)
await collectEvents(
router.stream(makeModel(), makeContext(), {
apiKey: "provider-key",
fetch: () => Promise.resolve(new Response("ok", { status: 200 })),
}),
)
assert.equal(router.getTransport(), "provider")
assert.equal(providerCalls, 2)
assert.equal(generateCalls, 1)
})
it("does not let a stale request overwrite the transport for a new API key", async () => {
let releaseGoRequest: (() => void) | undefined
const goRequestGate = new Promise<void>((resolve) => {
releaseGoRequest = resolve
})
let providerCalls = 0
let generateCalls = 0
const upgradeBody = JSON.stringify({ error: { code: "upgrade_required" } })
const router = createCommandCodeTransportRouter({
createStream: createTestEventStream,
streamProvider: (_model, _context, options) => {
providerCalls += 1
const response =
options?.apiKey === "go-key"
? new Response(upgradeBody, { status: 403 })
: new Response("ok", { status: 200 })
const stream = createTestEventStream()
const run = async () => {
if (options?.apiKey === "go-key") await goRequestGate
const received = await (options?.fetch ?? fetch)("https://provider.test", {})
await options?.onResponse?.(
{ status: received.status, headers: {} },
makeModel({ api: "openai-completions" }),
)
if (response.ok) {
for await (const event of completedStream("provider")) stream.push(event)
}
stream.end()
}
run().catch(() => stream.end())
return stream
},
streamGenerate: () => {
generateCalls += 1
return completedStream("generate")
},
})
const staleGoRequest = collectEvents(
router.stream(makeModel(), makeContext(), {
apiKey: "go-key",
fetch: () => Promise.resolve(new Response(upgradeBody, { status: 403 })),
}),
)
await collectEvents(
router.stream(makeModel(), makeContext(), {
apiKey: "provider-key",
fetch: () => Promise.resolve(new Response("ok", { status: 200 })),
}),
)
releaseGoRequest?.()
await staleGoRequest
await collectEvents(
router.stream(makeModel(), makeContext(), {
apiKey: "provider-key",
fetch: () => Promise.resolve(new Response("ok", { status: 200 })),
}),
)
assert.equal(router.getTransport(), "provider")
assert.equal(providerCalls, 3)
assert.equal(generateCalls, 1)
})
it("does not fall back for other 403 errors", async () => {
let generateCalls = 0
const responseBody = JSON.stringify({ error: { code: "permission_denied" } })
const router = createCommandCodeTransportRouter({
createStream: createTestEventStream,
streamProvider: (_model, _context, options) =>
providerStream(new Response(responseBody, { status: 403 }), "blocked", options),
streamGenerate: () => {
generateCalls += 1
return completedStream("generate")
},
})
const options: StreamOptions = {
fetch: () => Promise.resolve(new Response(responseBody, { status: 403 })),
}
await collectEvents(router.stream(makeModel(), makeContext(), options))
assert.equal(router.getTransport(), "provider")
assert.equal(generateCalls, 0)
})
})