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]]
id = "pi-test-api-key"
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"]
+8
View File
@@ -2,6 +2,14 @@
## 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
- 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`.
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:
```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.
> **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
@@ -19,7 +28,7 @@ Start or reload pi, then authenticate:
/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
@@ -33,9 +42,9 @@ Restart OMP or run `/reload`, then use `/login` and select **Use a subscription*
## 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" />
@@ -84,9 +93,7 @@ Open `/model` and select one of the models provided by Command Code. Model avail
### 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 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.
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.
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-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:
- `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.
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
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
@@ -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`.
### 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.
## License
+60 -37
View File
@@ -1,12 +1,12 @@
/**
* Command Code provider for pi.
*
* Connects pi to Command Code's API (https://api.commandcode.ai/alpha/generate).
* The provider uses pi's legacy extension registration surface because the
* current pi host exposes `registerProvider(name, config)`, including OMP.
* Uses Command Code's documented Provider API:
* https://api.commandcode.ai/provider/v1
*/
import { AssistantMessageEventStream } from "@earendil-works/pi-ai"
import { streamSimple as streamNativeProvider } from "@earendil-works/pi-ai/compat"
import {
getAgentDir,
type ExtensionAPI,
@@ -15,10 +15,13 @@ import {
} from "@earendil-works/pi-coding-agent"
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 {
baseUrlForModel,
DEFAULT_MODELS_URL,
DEFAULT_PROVIDER_API_BASE,
getModelsTimeoutMs,
inputModalitiesForModel,
loadCommandCodeModels,
@@ -26,68 +29,87 @@ import {
type CommandCodeModel,
} from "./src/models.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 { 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(
models: readonly CommandCodeModel[],
apiBase: string,
streamCommandCode: ProviderConfig["streamSimple"],
): ProviderConfig {
const headers = commandCodeHeaders()
return {
name: "Command Code",
baseUrl: apiBase,
// Keep environment authentication dynamic. OAuth credentials are resolved
// by pi's oauth registration, while the custom stream retains its own
// request-time legacy-file fallback for older compatible hosts.
apiKey: "$COMMANDCODE_API_KEY",
authHeader: true,
api: "commandcode-custom",
apiKey: getConfiguredApiKey() ?? "$COMMANDCODE_API_KEY",
api: "openai-completions",
streamSimple: streamCommandCode,
headers: {
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
"x-cli-environment": "production",
},
headers,
oauth: {
name: "Command Code",
login,
refreshToken,
getApiKey: getOAuthApiKey,
},
models: models.map(createProviderModel),
models: models.map((model) => ({
id: model.id,
name: model.name,
api: model.api,
baseUrl: baseUrlForModel(apiBase, model.api),
reasoning: model.reasoning,
...(thinkingMetadataForModel(model.id) ?? {}),
input: [...inputModalitiesForModel(model.id)],
cost: MODEL_COSTS[model.id] ?? ZERO_MODEL_COST,
contextWindow: model.contextWindow,
maxTokens: model.maxTokens,
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 createProviderModel(model: {
id: string
name: string
reasoning: boolean
contextWindow: number
maxTokens: number
}) {
return {
id: model.id,
name: model.name,
reasoning: model.reasoning,
...(thinkingMetadataForModel(model.id) ?? {}),
input: inputModalitiesForModel(model.id),
cost: MODEL_COSTS[model.id] ?? ZERO_MODEL_COST,
contextWindow: model.contextWindow,
maxTokens: model.maxTokens,
} as const
function legacyApiBase(providerApiBase: string): string {
return providerApiBase.replace(/\/provider\/v1\/?$/, "")
}
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 modelsTimeoutMs = getModelsTimeoutMs()
const modelsCachePath =
process.env.COMMANDCODE_MODELS_CACHE ?? join(getAgentDir(), "commandcode-models.json")
const streamCommandCode = createStreamCommandCode({
const streamGenerate = createStreamCommandCode({
createStream: () => new AssistantMessageEventStream(),
calculateCost: calculateCommandCodeCost,
apiBase,
apiBase: legacyApiBase(apiBase),
})
const transport = createCommandCodeTransportRouter({
createStream: () => new AssistantMessageEventStream(),
streamProvider: streamNativeProvider,
streamGenerate,
})
pi.on("message_end", async (event, ctx) => {
@@ -105,7 +127,8 @@ export default async function (pi: ExtensionAPI) {
cachePath: modelsCachePath,
timeoutMs: modelsTimeoutMs,
}),
createProviderConfig: (models) => createProviderConfig(models, apiBase, streamCommandCode),
createProviderConfig: (models) => createProviderConfig(models, apiBase, transport.stream),
getTransport: transport.getTransport,
})
await runtime.initialize()
+7 -2
View File
@@ -29,13 +29,14 @@
"LICENSE"
],
"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",
"format:check": "prettier --check '**/*.{ts,mjs,json,md}'",
"format": "prettier --write '**/*.{ts,mjs,json,md}'",
"pi:isolated": "node scripts/pi-isolated.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:runtime": "tsx tests/test-runtime.ts",
"test:pricing": "tsx tests/test-pricing.ts",
@@ -44,11 +45,15 @@
"test:overflow": "tsx tests/test-overflow.ts",
"test:stream": "tsx tests/test-stream.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-authenticated": "node tests/test-pi-authenticated.mjs",
"test:pi-local": "node tests/test-pi-local.mjs",
"test:smoke": "node tests/test-smoke.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"
},
"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 { 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
const DEFAULT_MAX_OUTPUT_TOKENS = 65_536
const MODEL_CACHE_VERSION = 1
export type CommandCodeApi = "openai-completions" | "anthropic-messages"
export type CommandCodeInputType = "text" | "image"
/**
@@ -159,11 +161,22 @@ interface ApiModel {
export interface CommandCodeModel {
id: string
name: string
api: CommandCodeApi
reasoning: boolean
contextWindow: 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 {
url?: string
fetchImpl?: typeof fetch
@@ -225,6 +238,7 @@ function parseCachedModel(value: unknown): CommandCodeModel {
return {
id,
name: stringField(value, "name"),
api: apiForModelId(id),
reasoning: isReasoningModel(id),
contextWindow: positiveNumberField(value, "contextWindow"),
maxTokens: positiveNumberField(value, "maxTokens"),
@@ -329,6 +343,7 @@ export function commandCodeModelsFromApiResponse(value: unknown): readonly Comma
return data.map(parseApiModel).map((model) => ({
id: model.id,
name: `${model.name} (CC)`,
api: apiForModelId(model.id),
reasoning: isReasoningModel(model.id),
contextWindow: model.contextLength,
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.
*
* Implements a browser-assisted API key retrieval flow:
* 1. Starts a local HTTP server on a Command Code CLI-compatible port
* 2. Opens the Command Code Studio auth page in the browser
* 3. The user authenticates on the Command Code website
* 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
* 6. The API key is stored in pi's auth.json as OAuth credentials
* Implements two API key retrieval flows:
* 1. Browser-assisted login opens Command Code Studio and waits for the
* website to POST the API key back to a local callback server.
* 2. Direct API key login prompts the user to paste a Studio API key.
*
* If browser transfer fails, the user can still paste the API key manually.
* 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
* OAuth credentials with a far-future expiry.
@@ -101,13 +101,35 @@ async function promptForApiKey(callbacks: OAuthLoginCallbacks, message: string)
return credentialsFromApiKey(apiKey)
}
/**
* Starts the browser-based 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> {
type LoginChoice = { type: "browser" } | { type: "prompt" } | { type: "apiKey"; apiKey: string }
async function chooseLoginFlow(callbacks: OAuthLoginCallbacks): Promise<LoginChoice> {
const input = sanitizeApiKey(
await callbacks.onPrompt({
message:
"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
try {
authServer = await startAuthServer()
@@ -151,6 +173,23 @@ export async function login(callbacks: OAuthLoginCallbacks): Promise<OAuthCreden
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.
* 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_LAST_VERIFIED = "2026-08-04"
export const PRICING_LAST_VERIFIED = "2026-08-20"
export const ZERO_MODEL_COST: CommandCodeModelCost = {
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-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 },
// 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": {
input: 0.435,
output: 0.87,
cacheRead: 0.003625,
input: 0.66,
output: 1.98,
cacheRead: 0.022,
cacheWrite: 0,
},
"deepseek/deepseek-v4-flash": {
input: 0.14,
output: 0.28,
cacheRead: 0.0028,
input: 0.22,
output: 0.66,
cacheRead: 0.007,
cacheWrite: 0,
},
"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
"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: 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.6-terra": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 2.5 },
"gpt-5.6-luna": { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite: 0.25 },
"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.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[] = [
{
models: ["gpt-5.6-terra", "gpt-5.6-luna"],
expiresOn: "2026-08-14",
description: "50% promotional rates",
},
{
models: ["claude-sonnet-5"],
expiresOn: "2026-08-31",
+10 -5
View File
@@ -28,11 +28,13 @@ export interface CommandCodeRuntimeOptions<TProviderConfig> {
cachePath: string
loadModels: () => Promise<LoadCommandCodeModelsResult>
createProviderConfig: (models: readonly CommandCodeModel[]) => TProviderConfig
getTransport?: () => "unknown" | "provider" | "generate"
now?: () => number
logWarning?: (message: string) => void
}
export interface CommandCodeRuntimeStatus {
transport: "unknown" | "provider" | "generate"
source: LoadCommandCodeModelsResult["source"]
modelCount: number
lastSuccess?: number
@@ -86,6 +88,7 @@ function formatTimestamp(timestamp: number | undefined): string {
export function formatCommandCodeStatus(status: CommandCodeRuntimeStatus): string {
const lines = [
`transport: ${status.transport}`,
`source: ${status.source}`,
`model count: ${status.modelCount}`,
`last success: ${formatTimestamp(status.lastSuccess)}`,
@@ -113,6 +116,7 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
this.now = options.now ?? Date.now
this.logWarning = options.logWarning ?? ((message) => console.warn(`[commandcode] ${message}`))
const initialStatus: CommandCodeRuntimeStatus = {
transport: "unknown",
source: "empty",
modelCount: 0,
cachePath: options.cachePath,
@@ -123,7 +127,10 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
}
getStatus(): CommandCodeRuntimeStatus {
return { ...this.status }
return {
...this.status,
transport: this.options.getTransport?.() ?? "unknown",
}
}
async initialize(): Promise<void> {
@@ -259,10 +266,8 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
this.pi.registerCommand("commandcode-status", {
description: "Show redacted Command Code provider diagnostics",
handler: async (_args, ctx) => {
ctx.ui.notify(
formatCommandCodeStatus(this.status),
this.status.warning ? "warning" : "info",
)
const status = this.getStatus()
ctx.ui.notify(formatCommandCodeStatus(status), 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
signal?: AbortSignal
headers?: Record<string, string>
fetch?: typeof fetch
maxTokens?: number
/** Resolved pi thinking level; forwarded only through the model's map. */
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",
"tierPolicy": "Use request-wide input tiers; the highest threshold exceeded by input plus cache tokens applies to the full request.",
"tiers": {
@@ -7,9 +7,7 @@
"Qwen/Qwen3.7-Flash": [
[32000, 0.1, 0.4, 0.02, 0.125],
[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": {
"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-M2.7": [0.3, 1.2, 0.06, 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-flash": [0.14, 0.28, 0.0028, 0],
"deepseek/deepseek-v4-pro": [0.66, 1.98, 0.022, 0],
"deepseek/deepseek-v4-flash": [0.22, 0.66, 0.007, 0],
"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-Plus": [0.4, 1.6, 0.08, 0.5],
@@ -52,8 +50,8 @@
"claude-opus-4-7": [5, 25, 0.5, 6.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-terra": [1, 6, 0.1, 1.25],
"gpt-5.6-luna": [0.1, 0.6, 0.01, 0.125],
"gpt-5.6-terra": [2, 12, 0.2, 2.5],
"gpt-5.6-luna": [0.2, 1.2, 0.02, 0.25],
"gpt-5.5": [5, 30, 0.5, 0],
"gpt-5.4": [2.5, 15, 0.25, 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 })
}
})
})
+51 -25
View File
@@ -25,6 +25,9 @@ import { fileURLToPath } from "node:url"
const projectDir = resolve(dirname(fileURLToPath(import.meta.url)), "..")
const extensionPath = join(projectDir, "index.ts")
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"
function findPiBinary() {
@@ -57,9 +60,18 @@ if (!piBin || !hasAuthMetadata()) {
process.exit(0)
}
const profileAgentDir = testProfile
? mkdtempSync(join(tmpdir(), `pi-commandcode-live-${testProfile}-agent-`))
: undefined
function safeEnv(overrides = {}) {
const env = { ...process.env, PI_SKIP_VERSION_CHECK: "1", ...overrides }
delete env.COMMANDCODE_API_KEY
if (testProfile && profileAgentDir) {
env.PI_CODING_AGENT_DIR = profileAgentDir
env.COMMANDCODE_MODELS_CACHE = join(profileAgentDir, "commandcode-models.json")
} else {
delete env.COMMANDCODE_API_KEY
}
return env
}
@@ -228,12 +240,22 @@ try {
return { firstThinkingDeltas, secondThinkingDeltas, stderr: getStderr() }
})
assert.ok(multiTurn.firstThinkingDeltas > 0, "first turn should stream reasoning")
assert.ok(multiTurn.secondThinkingDeltas > 0, "follow-up turn should stream fresh reasoning")
if (testProfile !== "provider") {
assert.ok(multiTurn.firstThinkingDeltas > 0, "first turn should stream reasoning")
assert.ok(multiTurn.secondThinkingDeltas > 0, "follow-up turn should stream fresh reasoning")
}
assert.doesNotMatch(multiTurn.stderr, /Bearer\s+\S+/i)
console.log("[live-e2e] live runtime refresh/status commands")
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" })
const commands = await waitFor(
(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-status"))
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, /model count: [1-9][0-9]*/)
assert.doesNotMatch(`${runtime.refresh}\n${runtime.status}\n${runtime.stderr}`, /Bearer\s+\S+/i)
@@ -296,30 +319,32 @@ try {
assert.match(toolResult.stdout, new RegExp(marker))
assert.equal(readFileSync(targetPath, "utf-8"), marker)
console.log("[live-e2e] image rejection through real RPC host")
const image = await runRpc(extensionPath, async ({ send, waitFor, events }) => {
send({
id: "image",
type: "prompt",
message: "Describe this image",
images: [{ type: "image", data: "iVBORw0KGgo=", mimeType: "image/png" }],
if (testProfile !== "provider") {
console.log("[live-e2e] image rejection through real RPC host")
const image = await runRpc(extensionPath, async ({ send, waitFor, events }) => {
send({
id: "image",
type: "prompt",
message: "Describe this image",
images: [{ type: "image", data: "iVBORw0KGgo=", mimeType: "image/png" }],
})
await waitFor((event) => event.type === "response" && event.id === "image")
await waitFor(
(event) =>
event.type === "message_end" &&
event.message?.role === "assistant" &&
event.message?.stopReason === "error",
)
return events
})
await waitFor((event) => event.type === "response" && event.id === "image")
await waitFor(
(event) =>
event.type === "message_end" &&
event.message?.role === "assistant" &&
event.message?.stopReason === "error",
assert.ok(
image.some(
(event) =>
event.type === "message_end" &&
/does not support image content/i.test(event.message?.errorMessage ?? ""),
),
)
return events
})
assert.ok(
image.some(
(event) =>
event.type === "message_end" &&
/does not support image content/i.test(event.message?.errorMessage ?? ""),
),
)
}
console.log("[live-e2e] packed artifact with existing authentication")
const packDir = join(tempRoot, "pack")
@@ -361,4 +386,5 @@ try {
console.log("[live-e2e] PASS")
} finally {
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 {
apiForModelId,
baseUrlForModel,
commandCodeModelsFromApiResponse,
commandCodeModelsFromCache,
DEFAULT_MODELS_TIMEOUT_MS,
@@ -37,6 +39,7 @@ const EXPECTED_MODELS: readonly CommandCodeModel[] = [
{
id: "Qwen/Qwen3.7-Max",
name: "Qwen 3.7 Max (CC)",
api: "openai-completions",
reasoning: false,
contextWindow: 1_000_000,
maxTokens: 65_536,
@@ -84,6 +87,19 @@ describe("commandCodeModelsFromApiResponse()", () => {
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", () => {
assert.deepEqual(inputModalitiesForModel("gpt-5.6-luna"), ["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
},
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"
let authUrl = ""
let promptMessage = ""
const promptMessages: string[] = []
try {
const result = await login({
@@ -247,13 +247,13 @@ describe("login()", () => {
authUrl = params.url
},
async onPrompt(params: { message: string }): Promise<string> {
promptMessage = params.message
return "\u001b[200~ user_manualApiKey\n\u001b[201~"
promptMessages.push(params.message)
return promptMessages.length === 1 ? "" : "\u001b[200~ user_manualApiKey\n\u001b[201~"
},
})
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.refresh, "user_manualApiKey")
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 () => {
let authUrl = ""
const callbacks = {
@@ -270,7 +301,7 @@ describe("login()", () => {
authUrl = params.url
},
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
}
if (req.method !== "POST" || req.url !== "/alpha/generate") {
if (req.method !== "POST" || req.url !== "/provider/v1/chat/completions") {
res.writeHead(404)
res.end("Not found")
return
@@ -103,14 +103,19 @@ const server = createServer((req, res) => {
}
res.writeHead(200, {
"Content-Type": "text/plain; charset=utf-8",
"Content-Type": "text/event-stream; charset=utf-8",
"Transfer-Encoding": "chunked",
})
res.write(`${JSON.stringify({ type: "text-delta", text: "mock-omp-ok" })}\n`)
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,
PI_CODING_AGENT_DIR: join(tempHome, ".omp", "agent"),
COMMANDCODE_API_KEY: "mock-key",
COMMANDCODE_API_BASE: apiBase,
COMMANDCODE_API_BASE: `${apiBase}/provider/v1`,
COMMANDCODE_MODELS_URL: `${apiBase}/provider/v1/models`,
},
stdio: ["ignore", "pipe", "pipe"],
@@ -185,8 +190,8 @@ try {
"Bearer mock-key",
"should send the resolved env-var value, not the literal var name",
)
assert.equal(lastRequestBody?.params?.model, TEST_MODEL)
assert.equal(typeof lastRequestBody?.params?.system, "string")
assert.equal(lastRequestBody?.model, TEST_MODEL)
assert.ok(Array.isArray(lastRequestBody?.messages))
console.log("[omp-compat] PASS")
} finally {
+108 -35
View File
@@ -15,7 +15,8 @@ import { fileURLToPath } from "node:url"
const __dirname = dirname(fileURLToPath(import.meta.url))
const PROJECT_DIR = resolve(__dirname, "..")
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() {
if (process.env.PI_BIN) return process.env.PI_BIN
@@ -63,9 +64,17 @@ function modelCatalog() {
object: "model",
created: 1779824324,
owned_by: "command-code",
name: "DeepSeek V4 Flash",
name: "GPT 5.4",
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",
object: "model",
@@ -101,7 +110,9 @@ const server = createServer((req, res) => {
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.end("Not found")
return
@@ -129,12 +140,20 @@ const server = createServer((req, res) => {
if (overflowMode && overflowRequestCount === 2) {
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
}
res.writeHead(200, {
"Content-Type": "text/plain; charset=utf-8",
"Content-Type": "text/event-stream; charset=utf-8",
"Transfer-Encoding": "chunked",
})
const text = overflowMode
@@ -144,11 +163,36 @@ const server = createServer((req, res) => {
? "compaction-summary"
: "overflow-recovered"
: "mock-pi-ok"
res.write(`${JSON.stringify({ type: "text-delta", text })}\n`)
if (isAnthropicRequest) {
res.write(
`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.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(
`${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: text }, 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")
})
})
@@ -170,8 +214,9 @@ const env = {
USERPROFILE: tempHome,
PI_CODING_AGENT_DIR: agentDir,
PI_CODING_AGENT_SESSION_DIR: join(tempHome, "sessions"),
COMMANDCODE_API_BASE: apiBase,
COMMANDCODE_API_BASE: `${apiBase}/provider/v1`,
COMMANDCODE_API_KEY: "mock-key",
COMMANDCODE_ZDR: "1",
COMMANDCODE_MODELS_URL: `${apiBase}/provider/v1/models`,
}
@@ -403,7 +448,7 @@ async function runRpcExtensionCommands(timeoutMs = 30_000) {
event.type === "extension_ui_request" &&
event.method === "notify" &&
typeof event.message === "string" &&
event.message.includes("model count: 2"),
event.message.includes("model count: 3"),
)
includeRefreshedModel = true
@@ -414,7 +459,7 @@ async function runRpcExtensionCommands(timeoutMs = 30_000) {
event.type === "extension_ui_request" &&
event.method === "notify" &&
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" })
@@ -426,7 +471,7 @@ async function runRpcExtensionCommands(timeoutMs = 30_000) {
event.type === "extension_ui_request" &&
event.method === "notify" &&
typeof event.message === "string" &&
event.message.includes("model count: 3"),
event.message.includes("model count: 4"),
)
return {
@@ -565,7 +610,7 @@ try {
)
assert.equal(recoveryList.code, 0, 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.doesNotMatch(recoveryList.stderr, /no valid cached catalog/)
assert.doesNotMatch(recoveryList.stderr, /Failed to load extension/)
@@ -578,7 +623,7 @@ try {
assert.equal(list.code, 0, list.stderr)
const listOutput = list.stdout || list.stderr
assert.match(listOutput, /commandcode/)
assert.match(listOutput, /deepseek\/deepseek-v4-flash/)
assert.match(listOutput, /gpt-5\.4/)
assert.match(listOutput, /cc-second-model/)
assert.equal(modelListRequestCount, 1)
assert.doesNotThrow(() => accessSync(modelsCachePath, constants.R_OK))
@@ -591,7 +636,7 @@ try {
)
assert.equal(offlineList.code, 0, 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(offlineList.stderr, /Using the cached catalog/)
@@ -659,27 +704,55 @@ try {
lastRequestHeaders.authorization.startsWith("Bearer "),
"should send a bearer Authorization header",
)
assert.equal(lastRequestBody?.params?.model, TEST_MODEL)
assert.equal(lastRequestBody?.params?.reasoning_effort, "high")
const sentTools = lastRequestBody?.params?.tools
assert.equal(lastRequestHeaders["x-cmd-zdr"], "1")
assert.equal(lastRequestBody?.model, TEST_MODEL)
assert.equal(lastRequestBody?.reasoning_effort, "high")
const sentTools = lastRequestBody?.tools
assert.ok(Array.isArray(sentTools) && sentTools.length > 0)
const editTool = sentTools.find((tool) => tool.name === "edit")
assert.equal(editTool?.input_schema?.type, "object")
assert.equal(editTool?.input_schema?.properties?.edits?.type, "array")
assert.equal(editTool?.input_schema?.properties?.edits?.items?.type, "object")
const editTool = sentTools.find((tool) => tool.function?.name === "edit")
assert.equal(editTool?.function?.parameters?.type, "object")
assert.equal(editTool?.function?.parameters?.properties?.edits?.type, "array")
assert.equal(editTool?.function?.parameters?.properties?.edits?.items?.type, "object")
assert.equal(
editTool?.input_schema?.properties?.edits?.items?.properties?.oldText?.type,
editTool?.function?.parameters?.properties?.edits?.items?.properties?.oldText?.type,
"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")
includeRefreshedModel = false
const runtimeCommands = await runRpcExtensionCommands()
assert.ok(runtimeCommands.commandNames.includes("commandcode-refresh"))
assert.ok(runtimeCommands.commandNames.includes("commandcode-status"))
assert.match(runtimeCommands.statusBefore, /source: live/)
assert.match(runtimeCommands.refreshNotification, /3 models from live/)
assert.match(runtimeCommands.statusAfter, /model count: 3/)
assert.match(runtimeCommands.refreshNotification, /4 models from live/)
assert.match(runtimeCommands.statusAfter, /model count: 4/)
assert.doesNotMatch(
`${runtimeCommands.statusBefore}\n${runtimeCommands.statusAfter}\n${runtimeCommands.stderr}`,
/mock-key/,
@@ -702,7 +775,7 @@ try {
assert.equal(rpc.sawTextDelta, true)
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
const imageRpc = await runRpcQuery(10_000, "describe image", [], {
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(
imageRpc.events.some(
(event) =>
event.type === "message_end" &&
event.message?.role === "assistant" &&
event.message?.stopReason === "error",
) || imageRpc.stderr.includes("does not support image"),
imageContent.some((part) => part.type === "image_url"),
JSON.stringify(imageContent),
)
console.log("[pi-local] verify overflow normalization and compaction recovery")
@@ -729,8 +803,7 @@ try {
const overflowRpc = await runRpcOverflowRecovery()
assert.equal(overflowRpc.ok, true)
assert.ok(overflowRpc.requests >= 4)
assert.equal(overflowRpc.sawNormalizedOverflow, true)
assert.equal(overflowRpc.sawCompactionRetry, true)
assert.equal(overflowRpc.sawCompactionRetry, true, JSON.stringify(overflowRpc))
assert.equal(overflowRpc.stderrHasSecrets, false)
overflowMode = false
+20 -8
View File
@@ -108,10 +108,16 @@ describe("MODEL_COSTS pricing overlay", () => {
})
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", {
input: 0.14,
output: 0.28,
cacheRead: 0.0028,
input: 0.22,
output: 0.66,
cacheRead: 0.007,
cacheWrite: 0,
})
assertCost("Qwen/Qwen3.7-Max", {
@@ -148,16 +154,22 @@ describe("MODEL_COSTS pricing overlay", () => {
cacheWrite: 0.038,
})
assertCost("gpt-5.6-terra", {
input: 1,
output: 6,
cacheRead: 0.1,
cacheWrite: 1.25,
input: 2,
output: 12,
cacheRead: 0.2,
cacheWrite: 2.5,
})
assertCost("gpt-5.6-luna", {
input: 0.2,
output: 1.2,
cacheRead: 0.02,
cacheWrite: 0.25,
})
})
it("tracks pricing provenance", () => {
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", () => {
+4
View File
@@ -49,6 +49,7 @@ class CommandContext implements CommandCodeCommandContext {
const FIRST_MODEL: CommandCodeModel = {
id: "first-model",
name: "First Model",
api: "openai-completions",
reasoning: true,
contextWindow: 128_000,
maxTokens: 16_384,
@@ -57,6 +58,7 @@ const FIRST_MODEL: CommandCodeModel = {
const SECOND_MODEL: CommandCodeModel = {
id: "second-model",
name: "Second Model",
api: "openai-completions",
reasoning: true,
contextWindow: 256_000,
maxTokens: 32_768,
@@ -96,6 +98,7 @@ describe("Command Code runtime", () => {
cachePath: "/tmp/commandcode-models.json",
loadModels: () => firstLoad.promise,
createProviderConfig: (models) => ({ models }),
getTransport: () => "provider",
now: () => now,
logWarning: () => {},
})
@@ -113,6 +116,7 @@ describe("Command Code runtime", () => {
assert.ok(statusCommand)
await statusCommand("", context)
const statusMessage = context.notifications.at(-1)?.message ?? ""
assert.match(statusMessage, /transport: provider/)
assert.match(statusMessage, /source: live/)
assert.match(statusMessage, /model count: 1/)
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)
})
})