Merge pull request #58 from patlux/fix/commandcode-cli-parity-complete
fix(core): align provider with Command Code CLI 1.32.2
This commit is contained in:
@@ -9,6 +9,8 @@ import {
|
||||
COMMAND_CODE_CLI_VERSION,
|
||||
MODEL_EFFORTS,
|
||||
MODEL_INPUT_MODALITIES,
|
||||
MODEL_MAX_OUTPUT_TOKENS,
|
||||
MODEL_REASONING,
|
||||
} from "../../src/commandcode-catalog.ts"
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
@@ -21,7 +23,9 @@ const README_PATH = new URL("../../README.md", import.meta.url)
|
||||
|
||||
export interface CommandCodeModelMetadata {
|
||||
imageModelIds: readonly string[]
|
||||
reasoningModelIds: readonly string[]
|
||||
reasoningEfforts: Readonly<Record<string, readonly string[]>>
|
||||
maxOutputTokens: Readonly<Record<string, number>>
|
||||
}
|
||||
|
||||
export interface ModelMetadataDiff {
|
||||
@@ -30,7 +34,12 @@ export interface ModelMetadataDiff {
|
||||
removedImageModelIds: readonly string[]
|
||||
addedReasoningModelIds: readonly string[]
|
||||
removedReasoningModelIds: readonly string[]
|
||||
changedReasoningModelIds: readonly string[]
|
||||
addedEffortModelIds: readonly string[]
|
||||
removedEffortModelIds: readonly string[]
|
||||
changedEffortModelIds: readonly string[]
|
||||
addedMaxOutputModelIds: readonly string[]
|
||||
removedMaxOutputModelIds: readonly string[]
|
||||
changedMaxOutputModelIds: readonly string[]
|
||||
}
|
||||
|
||||
interface PackedPackage {
|
||||
@@ -123,27 +132,93 @@ export function parseKnownTextOnlyModelIds(bundle: string): readonly string[] {
|
||||
return sorted(new Set(parsed))
|
||||
}
|
||||
|
||||
function modelObject(bundle: string, modelId: string): string {
|
||||
const start = bundle.indexOf(`{id:${JSON.stringify(modelId)},inputModalities:`)
|
||||
if (start < 0) throw new Error(`Could not find model metadata for ${modelId}`)
|
||||
|
||||
let depth = 0
|
||||
let quote = ""
|
||||
let escaped = false
|
||||
for (let index = start; index < bundle.length; index += 1) {
|
||||
const character = bundle[index] ?? ""
|
||||
if (quote) {
|
||||
if (escaped) escaped = false
|
||||
else if (character === "\\") escaped = true
|
||||
else if (character === quote) quote = ""
|
||||
continue
|
||||
}
|
||||
if (character === '"' || character === "'" || character === "`") {
|
||||
quote = character
|
||||
continue
|
||||
}
|
||||
if (character === "{") depth += 1
|
||||
else if (character === "}" && --depth === 0) return bundle.slice(start, index + 1)
|
||||
}
|
||||
|
||||
throw new Error(`Unterminated model metadata for ${modelId}`)
|
||||
}
|
||||
|
||||
export function parseBundleModelCapabilities(
|
||||
bundle: string,
|
||||
modelIds: readonly string[],
|
||||
): {
|
||||
reasoningModelIds: readonly string[]
|
||||
maxOutputTokens: Readonly<Record<string, number>>
|
||||
} {
|
||||
const reasoningModelIds: string[] = []
|
||||
const maxOutputTokens: Record<string, number> = {}
|
||||
|
||||
for (const modelId of modelIds) {
|
||||
const entry = modelObject(bundle, modelId)
|
||||
if (entry.includes("reasoning:!0") || entry.includes("reasoningEfforts:[")) {
|
||||
reasoningModelIds.push(modelId)
|
||||
}
|
||||
const maxOutput = /maxOutputTokens:([^,}]+)/.exec(entry)?.[1]
|
||||
if (maxOutput) {
|
||||
const value = Number(maxOutput)
|
||||
if (!Number.isFinite(value) || value <= 0) {
|
||||
throw new Error(`Unexpected max output tokens for ${modelId}: ${maxOutput}`)
|
||||
}
|
||||
maxOutputTokens[modelId] = value
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
reasoningModelIds: sorted(reasoningModelIds),
|
||||
maxOutputTokens: Object.fromEntries(
|
||||
Object.entries(maxOutputTokens).sort(([left], [right]) => left.localeCompare(right)),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export function commandCodeModelMetadataFromContents(
|
||||
modelsReference: string,
|
||||
cliBundle: string,
|
||||
): CommandCodeModelMetadata {
|
||||
const reference = parseModelsReference(modelsReference)
|
||||
const textOnlyModelIds = new Set(parseKnownTextOnlyModelIds(cliBundle))
|
||||
const capabilities = parseBundleModelCapabilities(cliBundle, reference.modelIds)
|
||||
|
||||
return {
|
||||
imageModelIds: reference.modelIds.filter((modelId) => !textOnlyModelIds.has(modelId)),
|
||||
reasoningModelIds: capabilities.reasoningModelIds,
|
||||
reasoningEfforts: reference.reasoningEfforts,
|
||||
maxOutputTokens: capabilities.maxOutputTokens,
|
||||
}
|
||||
}
|
||||
|
||||
export function currentModelMetadata(): CommandCodeModelMetadata {
|
||||
return {
|
||||
imageModelIds: sorted(Object.keys(MODEL_INPUT_MODALITIES)),
|
||||
reasoningModelIds: sorted(Object.keys(MODEL_REASONING)),
|
||||
reasoningEfforts: Object.fromEntries(
|
||||
Object.entries(MODEL_EFFORTS)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([modelId, efforts]) => [modelId, [...efforts]]),
|
||||
),
|
||||
maxOutputTokens: Object.fromEntries(
|
||||
Object.entries(MODEL_MAX_OUTPUT_TOKENS).sort(([left], [right]) => left.localeCompare(right)),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,10 +230,16 @@ export function diffModelMetadata(
|
||||
): ModelMetadataDiff {
|
||||
const currentImages = new Set(current.imageModelIds)
|
||||
const upstreamImages = new Set(upstream.imageModelIds)
|
||||
const currentReasoningIds = Object.keys(current.reasoningEfforts)
|
||||
const upstreamReasoningIds = Object.keys(upstream.reasoningEfforts)
|
||||
const currentReasoningSet = new Set(currentReasoningIds)
|
||||
const upstreamReasoningSet = new Set(upstreamReasoningIds)
|
||||
const currentReasoning = new Set(current.reasoningModelIds)
|
||||
const upstreamReasoning = new Set(upstream.reasoningModelIds)
|
||||
const currentEffortIds = Object.keys(current.reasoningEfforts)
|
||||
const upstreamEffortIds = Object.keys(upstream.reasoningEfforts)
|
||||
const currentEffortSet = new Set(currentEffortIds)
|
||||
const upstreamEffortSet = new Set(upstreamEffortIds)
|
||||
const currentMaxOutputIds = Object.keys(current.maxOutputTokens)
|
||||
const upstreamMaxOutputIds = Object.keys(upstream.maxOutputTokens)
|
||||
const currentMaxOutputSet = new Set(currentMaxOutputIds)
|
||||
const upstreamMaxOutputSet = new Set(upstreamMaxOutputIds)
|
||||
|
||||
return {
|
||||
versionChanged: currentVersion !== upstreamVersion,
|
||||
@@ -169,19 +250,38 @@ export function diffModelMetadata(
|
||||
current.imageModelIds.filter((modelId) => !upstreamImages.has(modelId)),
|
||||
),
|
||||
addedReasoningModelIds: sorted(
|
||||
upstreamReasoningIds.filter((modelId) => !currentReasoningSet.has(modelId)),
|
||||
upstream.reasoningModelIds.filter((modelId) => !currentReasoning.has(modelId)),
|
||||
),
|
||||
removedReasoningModelIds: sorted(
|
||||
currentReasoningIds.filter((modelId) => !upstreamReasoningSet.has(modelId)),
|
||||
current.reasoningModelIds.filter((modelId) => !upstreamReasoning.has(modelId)),
|
||||
),
|
||||
changedReasoningModelIds: sorted(
|
||||
upstreamReasoningIds.filter(
|
||||
addedEffortModelIds: sorted(
|
||||
upstreamEffortIds.filter((modelId) => !currentEffortSet.has(modelId)),
|
||||
),
|
||||
removedEffortModelIds: sorted(
|
||||
currentEffortIds.filter((modelId) => !upstreamEffortSet.has(modelId)),
|
||||
),
|
||||
changedEffortModelIds: sorted(
|
||||
upstreamEffortIds.filter(
|
||||
(modelId) =>
|
||||
currentReasoningSet.has(modelId) &&
|
||||
currentEffortSet.has(modelId) &&
|
||||
JSON.stringify(current.reasoningEfforts[modelId]) !==
|
||||
JSON.stringify(upstream.reasoningEfforts[modelId]),
|
||||
),
|
||||
),
|
||||
addedMaxOutputModelIds: sorted(
|
||||
upstreamMaxOutputIds.filter((modelId) => !currentMaxOutputSet.has(modelId)),
|
||||
),
|
||||
removedMaxOutputModelIds: sorted(
|
||||
currentMaxOutputIds.filter((modelId) => !upstreamMaxOutputSet.has(modelId)),
|
||||
),
|
||||
changedMaxOutputModelIds: sorted(
|
||||
upstreamMaxOutputIds.filter(
|
||||
(modelId) =>
|
||||
currentMaxOutputSet.has(modelId) &&
|
||||
current.maxOutputTokens[modelId] !== upstream.maxOutputTokens[modelId],
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,14 +329,24 @@ export function renderCommandCodeCatalog(
|
||||
const imageEntries = sorted(metadata.imageModelIds)
|
||||
.map((modelId) => ` ${quoted(modelId)}: ["text", "image"],`)
|
||||
.join("\n")
|
||||
const reasoningEntries = recordEntries(metadata.reasoningEfforts)
|
||||
const reasoningEntries = sorted(metadata.reasoningModelIds)
|
||||
.map((modelId) => ` ${quoted(modelId)}: true,`)
|
||||
.join("\n")
|
||||
const effortEntries = recordEntries(metadata.reasoningEfforts)
|
||||
.map(
|
||||
([modelId, efforts]) =>
|
||||
` ${quoted(modelId)}: [${efforts.map((effort) => quoted(effort)).join(", ")}],`,
|
||||
)
|
||||
.join("\n")
|
||||
const maxOutputEntries = Object.entries(metadata.maxOutputTokens)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(
|
||||
([modelId, value]) =>
|
||||
` ${quoted(modelId)}: ${value.toLocaleString("en-US").replaceAll(",", "_")},`,
|
||||
)
|
||||
.join("\n")
|
||||
|
||||
return `export const COMMAND_CODE_CLI_VERSION = ${quoted(packageVersion)}\n\nexport type CommandCodeInputType = "text" | "image"\nexport type CommandCodeReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh" | "max"\n\n/**\n * Generated from command-code@${packageVersion} by \`npm run sync:commandcode-catalog\`.\n * Do not edit manually.\n */\nexport const MODEL_INPUT_MODALITIES: Readonly<Record<string, readonly CommandCodeInputType[]>> = {\n${imageEntries}\n}\n\nexport const MODEL_EFFORTS: Readonly<Record<string, readonly CommandCodeReasoningEffort[]>> = {\n${reasoningEntries}\n}\n`
|
||||
return `export const COMMAND_CODE_CLI_VERSION = ${quoted(packageVersion)}\n\nexport type CommandCodeInputType = "text" | "image"\nexport type CommandCodeReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh" | "max"\n\n/**\n * Generated from command-code@${packageVersion} by \`npm run sync:commandcode-catalog\`.\n * Do not edit manually.\n */\nexport const MODEL_INPUT_MODALITIES: Readonly<Record<string, readonly CommandCodeInputType[]>> = {\n${imageEntries}\n}\n\nexport const MODEL_REASONING: Readonly<Record<string, true>> = {\n${reasoningEntries}\n}\n\nexport const MODEL_EFFORTS: Readonly<Record<string, readonly CommandCodeReasoningEffort[]>> = {\n${effortEntries}\n}\n\nexport const MODEL_MAX_OUTPUT_TOKENS: Readonly<Record<string, number>> = {\n${maxOutputEntries}\n}\n`
|
||||
}
|
||||
|
||||
function updateDocumentedCatalogVersion(
|
||||
@@ -279,16 +389,23 @@ function metadataReport(
|
||||
`- Repository snapshot: \`command-code@${COMMAND_CODE_CLI_VERSION}\``,
|
||||
`- Inspected package: \`command-code@${packageVersion}\``,
|
||||
`- Image-capable models: ${current.imageModelIds.length} repository / ${upstream.imageModelIds.length} upstream`,
|
||||
`- Reasoning models: ${Object.keys(current.reasoningEfforts).length} repository / ${Object.keys(upstream.reasoningEfforts).length} upstream`,
|
||||
`- Reasoning models: ${current.reasoningModelIds.length} repository / ${upstream.reasoningModelIds.length} upstream`,
|
||||
`- Models with selectable efforts: ${Object.keys(current.reasoningEfforts).length} repository / ${Object.keys(upstream.reasoningEfforts).length} upstream`,
|
||||
`- Model-specific output limits: ${Object.keys(current.maxOutputTokens).length} repository / ${Object.keys(upstream.maxOutputTokens).length} upstream`,
|
||||
"",
|
||||
"| Change | Models |",
|
||||
"| --- | --- |",
|
||||
`| CLI version | ${diff.versionChanged ? `\`${COMMAND_CODE_CLI_VERSION}\` → \`${packageVersion}\`` : "Current"} |`,
|
||||
`| New image support | ${formatList(diff.addedImageModelIds)} |`,
|
||||
`| Removed image support | ${formatList(diff.removedImageModelIds)} |`,
|
||||
`| New reasoning metadata | ${formatList(diff.addedReasoningModelIds)} |`,
|
||||
`| Removed reasoning metadata | ${formatList(diff.removedReasoningModelIds)} |`,
|
||||
`| Changed reasoning efforts | ${formatReasoningChanges(diff.changedReasoningModelIds, current, upstream)} |`,
|
||||
`| New reasoning models | ${formatList(diff.addedReasoningModelIds)} |`,
|
||||
`| Removed reasoning models | ${formatList(diff.removedReasoningModelIds)} |`,
|
||||
`| New effort metadata | ${formatList(diff.addedEffortModelIds)} |`,
|
||||
`| Removed effort metadata | ${formatList(diff.removedEffortModelIds)} |`,
|
||||
`| Changed reasoning efforts | ${formatReasoningChanges(diff.changedEffortModelIds, current, upstream)} |`,
|
||||
`| New output limits | ${formatList(diff.addedMaxOutputModelIds)} |`,
|
||||
`| Removed output limits | ${formatList(diff.removedMaxOutputModelIds)} |`,
|
||||
`| Changed output limits | ${formatList(diff.changedMaxOutputModelIds)} |`,
|
||||
"",
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
@@ -79,7 +79,8 @@ jobs:
|
||||
This updates only machine-readable compatibility metadata:
|
||||
- CLI version used in the `x-command-code-version` header
|
||||
- image-input capabilities
|
||||
- supported reasoning efforts
|
||||
- reasoning capability and selectable effort levels
|
||||
- model-specific maximum output limits
|
||||
- documented catalog snapshot version
|
||||
|
||||
Pricing remains review-only because CLI documentation does not represent every pricing tier and temporary promotion used by the provider.
|
||||
|
||||
@@ -256,10 +256,10 @@ rules:
|
||||
- pattern: process.env.$VAR
|
||||
- metavariable-regex:
|
||||
metavariable: $VAR
|
||||
regex: "(?!COMMANDCODE_|NODE_|PATH|HOME|SHELL|USER|LANG|LC_|TERM|TMPDIR|NIX_).*"
|
||||
regex: "(?!COMMANDCODE_|COMMAND_CODE_|CMD_ZDR|NODE_|PATH|HOME|SHELL|USER|LANG|LC_|TERM|TMPDIR|NIX_).*"
|
||||
message: >
|
||||
Reading unexpected environment variable $VAR. Provider should only
|
||||
read COMMANDCODE_* variables.
|
||||
read documented Command Code or standard runtime variables.
|
||||
severity: WARNING
|
||||
languages: [javascript, typescript]
|
||||
paths:
|
||||
@@ -270,4 +270,3 @@ rules:
|
||||
# ────────────────────────────────────────────────────────────────────────
|
||||
# OAuth flow manipulation
|
||||
# ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
+10
-4
@@ -2,17 +2,23 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Add a daily GitHub Actions synchronization job that opens or updates a pull request for CLI version, image capability, and reasoning-effort changes in the latest published Command Code catalog.
|
||||
- Refresh static model capabilities from `command-code@1.32.2`, including new image and reasoning metadata.
|
||||
- Add a daily GitHub Actions synchronization job that opens or updates a pull request for CLI version, image capability, reasoning, effort, and output-limit changes in the latest published Command Code catalog.
|
||||
- Refresh static model capabilities from `command-code@1.32.2`, separating reasoning support from selectable effort levels and honoring model-specific output limits.
|
||||
- Reject truncated, aborted, and network-failed generate streams instead of reporting partial responses as successful.
|
||||
- Normalize malformed tool results and synthesize missing tool results so follow-up requests preserve valid tool-call history.
|
||||
- Refresh display pricing for all 58 current models, including Gemini 3.7 Flash, Qwen 3.8 27B, Ox Alpha, Muse Spark 1.2, and Grok 4.6 long-context rates.
|
||||
- Accept the official `COMMAND_CODE_API_KEY` and `CMD_ZDR` environment variables while retaining legacy aliases.
|
||||
- Align generate request metadata with the CLI by forwarding stable session IDs, optional temperature, and the CLI user agent.
|
||||
- Validate manually pasted API keys, use the CLI's two-minute browser timeout, and reject OAuth state mismatches without closing the callback server.
|
||||
- Add `/commandcode-quota` with live credits, plan, usage totals, and rolling-limit diagnostics from Command Code's alpha usage endpoints.
|
||||
- Add `zai-org/GLM-5.3` with its verified reasoning efforts and display pricing.
|
||||
- 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`.
|
||||
- Add optional zero-data-retention headers through `CMD_ZDR=1` and the legacy `COMMANDCODE_ZDR=1` alias.
|
||||
- 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.
|
||||
- Add isolated live E2E profiles for separate Go-, GOAT-, and Provider-plan credentials, covering transport selection, reasoning across turns, quota identity, aborts, tools, GOAT vision, Go image rejection, and packed-package validation.
|
||||
- Fix extension load failure on newer pi hosts that reject registering a custom API under a built-in name (`openai-completions`); register under `commandcode-custom` instead and restore the real wire API before native compat dispatch.
|
||||
|
||||
## 0.5.1 - 2026-08-11
|
||||
|
||||
+2
-1
@@ -46,10 +46,11 @@ 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_GOAT_API_KEY_FILE=/path/to/goat-key npm run test:e2e:live:goat
|
||||
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.
|
||||
Use `npm run test:e2e:live:all` with the Go and GOAT file variables to run both subscription transports sequentially. Store 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. Direct `*_API_KEY` variables are intended primarily for protected CI secrets.
|
||||
|
||||
Before opening a PR, run:
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ If automatic transfer from the browser fails, copy the API key shown by Command
|
||||
### Environment variable
|
||||
|
||||
```sh
|
||||
export COMMANDCODE_API_KEY="user_..."
|
||||
export COMMAND_CODE_API_KEY="user_..."
|
||||
```
|
||||
|
||||
### Auth file
|
||||
@@ -93,7 +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. 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 capability and selectable effort levels follow the official CLI catalog independently. Models can therefore be marked as reasoning-capable even when Command Code chooses their depth automatically. Models with explicit effort support also register a model-specific `thinkingLevelMap`, so pi and OMP expose only valid 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.
|
||||
|
||||
List Command Code models from the terminal:
|
||||
|
||||
@@ -133,7 +133,7 @@ While pi is running, use these provider commands without restarting:
|
||||
|
||||
The `commandcode-quota` command reads from the Command Code alpha usage endpoints (the same ones the `cmd` CLI `/usage` command uses): `whoami`, `billing/credits`, `billing/subscriptions`, and `usage/summary`. It authenticates with the same API key the provider already uses. If the command cannot reach those endpoints or an endpoint schema changes, unavailable sections are reported explicitly instead of being displayed as zero usage. Output is plain text (via `ui.notify`) so it works across pi and compatible hosts such as OMP.
|
||||
|
||||
Set `COMMANDCODE_ZDR=1` to send Command Code's documented `x-cmd-zdr: 1` zero-data-retention header.
|
||||
Set `CMD_ZDR=1` to send Command Code's documented `x-cmd-zdr: 1` zero-data-retention header. The legacy `COMMANDCODE_ZDR=1` alias remains supported.
|
||||
|
||||
The following environment variables are intended for tests, local mocks, and compatible API endpoints:
|
||||
|
||||
@@ -144,7 +144,7 @@ The following environment variables are intended for tests, local mocks, and com
|
||||
|
||||
## Image input
|
||||
|
||||
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.32.2`; unknown models default to text-only until their upstream metadata is reviewed. A daily GitHub Actions job synchronizes the CLI version, image capabilities, and reasoning efforts with the latest published CLI package and opens or updates a reviewable pull request when they change. Pricing remains manually reviewed because the CLI catalog does not expose every pricing tier and temporary promotion used by the provider.
|
||||
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.32.2`; unknown models default to text-only until their upstream metadata is reviewed. A daily GitHub Actions job synchronizes the CLI version, image capabilities, reasoning flags, reasoning efforts, and model-specific output limits with the latest published CLI package and opens or updates a reviewable pull request when they change. Pricing remains manually reviewed because temporary promotions and long-context tiers require explicit review.
|
||||
|
||||
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.
|
||||
|
||||
@@ -195,23 +195,26 @@ Both commands accept additional pi arguments after `--`, for example `npm run pi
|
||||
|
||||
### 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:
|
||||
Keep Go-, GOAT-, and optional Provider-plan 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_GOAT_API_KEY_FILE=/path/to/goat-key \
|
||||
npm run test:e2e:live:goat
|
||||
|
||||
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 \
|
||||
COMMANDCODE_E2E_GOAT_API_KEY_FILE=/path/to/goat-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.
|
||||
Each profile runs with an isolated Pi agent directory and asserts transport selection, reasoning across turns, quota plan identity, abort handling, tool calls, and the packed npm artifact. Go must select `generate` and reject unsupported images; GOAT must select `provider` and complete a live vision request. 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.
|
||||
The Go profile defaults to DeepSeek V4 Flash; GOAT defaults to Grok 4.6 because its Provider API stream exposes reasoning consistently across consecutive turns. Override them with `COMMANDCODE_E2E_GO_MODEL`, `COMMANDCODE_E2E_GOAT_MODEL`, or `COMMANDCODE_E2E_PROVIDER_MODEL`. A successful live Anthropic `/provider/v1/messages` test requires a paid 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.
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
getModelsTimeoutMs,
|
||||
inputModalitiesForModel,
|
||||
loadCommandCodeModels,
|
||||
MODEL_EFFORTS,
|
||||
thinkingMetadataForModel,
|
||||
type CommandCodeModel,
|
||||
} from "./src/models.ts"
|
||||
@@ -37,7 +38,7 @@ import { createCommandCodeRuntime } from "./src/runtime.ts"
|
||||
import { createCommandCodeTransportRouter } from "./src/transport.ts"
|
||||
|
||||
function commandCodeHeaders(): Record<string, string> | undefined {
|
||||
if (process.env.COMMANDCODE_ZDR === "1") {
|
||||
if (process.env.CMD_ZDR === "1" || process.env.COMMANDCODE_ZDR === "1") {
|
||||
return { "x-cmd-zdr": "1" }
|
||||
}
|
||||
return undefined
|
||||
@@ -52,7 +53,7 @@ function createProviderConfig(
|
||||
return {
|
||||
name: "Command Code",
|
||||
baseUrl: apiBase,
|
||||
apiKey: getConfiguredApiKey() ?? "$COMMANDCODE_API_KEY",
|
||||
apiKey: getConfiguredApiKey() ?? "$COMMAND_CODE_API_KEY",
|
||||
api: "commandcode-custom",
|
||||
streamSimple: streamCommandCode,
|
||||
headers,
|
||||
@@ -79,7 +80,7 @@ function createProviderConfig(
|
||||
? {
|
||||
supportsStore: false,
|
||||
supportsDeveloperRole: false,
|
||||
supportsReasoningEffort: true,
|
||||
supportsReasoningEffort: MODEL_EFFORTS[model.id] !== undefined,
|
||||
maxTokensField: "max_tokens",
|
||||
}
|
||||
: {
|
||||
|
||||
+2
-1
@@ -55,8 +55,9 @@
|
||||
"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:goat": "node scripts/live-e2e-profile.mjs goat",
|
||||
"test:e2e:live:provider": "node scripts/live-e2e-profile.mjs provider",
|
||||
"test:e2e:live:all": "node scripts/live-e2e-profile.mjs go provider",
|
||||
"test:e2e:live:all": "node scripts/live-e2e-profile.mjs go goat",
|
||||
"test:cost": "tsx tests/test-cost.ts"
|
||||
},
|
||||
"pi": {
|
||||
|
||||
@@ -11,14 +11,19 @@ const profiles = process.argv.slice(2)
|
||||
|
||||
if (
|
||||
profiles.length === 0 ||
|
||||
profiles.some((profile) => profile !== "go" && profile !== "provider")
|
||||
profiles.some((profile) => profile !== "go" && profile !== "goat" && profile !== "provider")
|
||||
) {
|
||||
console.error("Usage: node scripts/live-e2e-profile.mjs <go|provider> [go|provider]")
|
||||
console.error("Usage: node scripts/live-e2e-profile.mjs <go|goat|provider> [go|goat|provider]")
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
async function credentialFor(profile) {
|
||||
const prefix = profile === "go" ? "COMMANDCODE_E2E_GO" : "COMMANDCODE_E2E_PROVIDER"
|
||||
const prefix =
|
||||
profile === "go"
|
||||
? "COMMANDCODE_E2E_GO"
|
||||
: profile === "goat"
|
||||
? "COMMANDCODE_E2E_GOAT"
|
||||
: "COMMANDCODE_E2E_PROVIDER"
|
||||
const direct = process.env[`${prefix}_API_KEY`]?.trim()
|
||||
const file = process.env[`${prefix}_API_KEY_FILE`]
|
||||
|
||||
@@ -35,15 +40,23 @@ async function credentialFor(profile) {
|
||||
|
||||
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"
|
||||
profile === "go"
|
||||
? "COMMANDCODE_E2E_GO_MODEL"
|
||||
: profile === "goat"
|
||||
? "COMMANDCODE_E2E_GOAT_MODEL"
|
||||
: "COMMANDCODE_E2E_PROVIDER_MODEL"
|
||||
const model =
|
||||
process.env[modelVariable] ??
|
||||
(profile === "goat" ? "xai/grok-4.6" : "deepseek/deepseek-v4-flash")
|
||||
const env = {
|
||||
...process.env,
|
||||
COMMANDCODE_API_KEY: apiKey,
|
||||
COMMAND_CODE_API_KEY: apiKey,
|
||||
COMMANDCODE_E2E_MODEL: model,
|
||||
COMMANDCODE_E2E_PROFILE: profile,
|
||||
}
|
||||
delete env.COMMANDCODE_API_KEY
|
||||
delete env.COMMANDCODE_E2E_GO_API_KEY
|
||||
delete env.COMMANDCODE_E2E_GOAT_API_KEY
|
||||
delete env.COMMANDCODE_E2E_PROVIDER_API_KEY
|
||||
|
||||
return new Promise((resolveRun, reject) => {
|
||||
|
||||
@@ -11,6 +11,7 @@ const env = {
|
||||
...process.env,
|
||||
PI_SKIP_VERSION_CHECK: "1",
|
||||
}
|
||||
delete env.COMMAND_CODE_API_KEY
|
||||
delete env.COMMANDCODE_API_KEY
|
||||
|
||||
const child = spawn(
|
||||
|
||||
@@ -22,6 +22,7 @@ const env = {
|
||||
PI_CODING_AGENT_SESSION_DIR: sessionDir,
|
||||
PI_SKIP_VERSION_CHECK: "1",
|
||||
}
|
||||
delete env.COMMAND_CODE_API_KEY
|
||||
delete env.COMMANDCODE_API_KEY
|
||||
|
||||
let activeChild
|
||||
|
||||
@@ -34,6 +34,7 @@ export function getConfiguredApiKey(
|
||||
} = {},
|
||||
): string | undefined {
|
||||
const env = options.env ?? process.env
|
||||
if (env.COMMAND_CODE_API_KEY) return env.COMMAND_CODE_API_KEY
|
||||
if (env.COMMANDCODE_API_KEY) return env.COMMANDCODE_API_KEY
|
||||
|
||||
const home = options.homeDir?.() ?? homedir()
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface AuthServer {
|
||||
export interface AuthServerOptions {
|
||||
startPort?: number
|
||||
portRange?: number
|
||||
expectedState?: string
|
||||
}
|
||||
|
||||
function listenOnAvailablePort(
|
||||
@@ -181,6 +182,12 @@ export async function startAuthServer(options: AuthServerOptions = {}): Promise<
|
||||
return
|
||||
}
|
||||
|
||||
if (options.expectedState !== undefined && state !== options.expectedState) {
|
||||
res.writeHead(403)
|
||||
res.end(JSON.stringify({ success: false, error: "Invalid state token" }))
|
||||
return
|
||||
}
|
||||
|
||||
res.writeHead(200)
|
||||
res.end(JSON.stringify({ success: true }))
|
||||
|
||||
|
||||
@@ -51,6 +51,57 @@ export const MODEL_INPUT_MODALITIES: Readonly<Record<string, readonly CommandCod
|
||||
"xiaomi/mimo-v2.5": ["text", "image"],
|
||||
}
|
||||
|
||||
export const MODEL_REASONING: Readonly<Record<string, true>> = {
|
||||
"claude-fable-5": true,
|
||||
"claude-opus-4-7": true,
|
||||
"claude-opus-4-8": true,
|
||||
"claude-opus-5": true,
|
||||
"claude-sonnet-4-6": true,
|
||||
"claude-sonnet-5": true,
|
||||
"deepseek/deepseek-v4-flash": true,
|
||||
"deepseek/deepseek-v4-flash-vision-exp": true,
|
||||
"deepseek/deepseek-v4-pro": true,
|
||||
"google/gemini-3.1-flash-lite": true,
|
||||
"google/gemini-3.5-flash": true,
|
||||
"google/gemini-3.5-flash-lite": true,
|
||||
"google/gemini-3.6-flash": true,
|
||||
"google/gemini-3.7-flash": true,
|
||||
"gpt-5.3-codex": true,
|
||||
"gpt-5.4": true,
|
||||
"gpt-5.4-mini": true,
|
||||
"gpt-5.5": true,
|
||||
"gpt-5.6-luna": true,
|
||||
"gpt-5.6-sol": true,
|
||||
"gpt-5.6-terra": true,
|
||||
"meta/muse-spark-1.1": true,
|
||||
"meta/muse-spark-1.2": true,
|
||||
"meta/muse-spark-1.2-contributor": true,
|
||||
"MiniMaxAI/MiniMax-M3": true,
|
||||
"moonshotai/Kimi-K2.7-Code": true,
|
||||
"moonshotai/Kimi-K2.7-Code-Highspeed": true,
|
||||
"moonshotai/Kimi-K3": true,
|
||||
"nvidia/nemotron-3-ultra-550b-a55b": true,
|
||||
"poolside/laguna-s-2.1-free": true,
|
||||
"Qwen/Qwen3.6-Max-Preview": true,
|
||||
"Qwen/Qwen3.6-Plus": true,
|
||||
"Qwen/Qwen3.7-Flash": true,
|
||||
"Qwen/Qwen3.7-Max": true,
|
||||
"Qwen/Qwen3.7-Plus": true,
|
||||
"Qwen/Qwen3.8-27B": true,
|
||||
"Qwen/Qwen3.8-Max": true,
|
||||
"sakana/fugu-ultra": true,
|
||||
"stealth/ox-alpha": true,
|
||||
"stepfun/Step-3.5-Flash": true,
|
||||
"stepfun/Step-3.7-Flash": true,
|
||||
"tencent/hy3-paid": true,
|
||||
"thinkingmachines/inkling": true,
|
||||
"thinkingmachines/inkling-small": true,
|
||||
"xai/grok-4.5": true,
|
||||
"xai/grok-4.6": true,
|
||||
"zai-org/GLM-5.2": true,
|
||||
"zai-org/GLM-5.3": true,
|
||||
}
|
||||
|
||||
export const MODEL_EFFORTS: Readonly<Record<string, readonly CommandCodeReasoningEffort[]>> = {
|
||||
"claude-fable-5": ["low", "medium", "high", "xhigh", "max"],
|
||||
"claude-opus-4-7": ["low", "medium", "high", "xhigh", "max"],
|
||||
@@ -82,3 +133,9 @@ export const MODEL_EFFORTS: Readonly<Record<string, readonly CommandCodeReasonin
|
||||
"zai-org/GLM-5.2": ["high", "max"],
|
||||
"zai-org/GLM-5.3": ["low", "high", "max"],
|
||||
}
|
||||
|
||||
export const MODEL_MAX_OUTPUT_TOKENS: Readonly<Record<string, number>> = {
|
||||
"poolside/laguna-s-2.1-free": 32_768,
|
||||
"Qwen/Qwen3.8-27B": 32_768,
|
||||
"stealth/ox-alpha": 131_072,
|
||||
}
|
||||
|
||||
+42
-11
@@ -107,6 +107,7 @@ export function getApiKey(
|
||||
} = {},
|
||||
): string | undefined {
|
||||
const env = options.env ?? process.env
|
||||
if (env.COMMAND_CODE_API_KEY) return env.COMMAND_CODE_API_KEY
|
||||
if (env.COMMANDCODE_API_KEY) return env.COMMANDCODE_API_KEY
|
||||
|
||||
const home = options.homeDir?.() ?? homedir()
|
||||
@@ -138,10 +139,11 @@ export function getApiKey(
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Hosts such as OMP may pass the literal env-var name "$COMMANDCODE_API_KEY"
|
||||
// (or "COMMANDCODE_API_KEY") as the "resolved" registry key instead of the
|
||||
// actual credential. Treat those as unresolved.
|
||||
// Hosts such as OMP may pass a literal env-var name as the "resolved" registry
|
||||
// key instead of the actual credential. Treat those as unresolved.
|
||||
export const COMMAND_CODE_PLACEHOLDER_KEYS = new Set([
|
||||
"$COMMAND_CODE_API_KEY",
|
||||
"COMMAND_CODE_API_KEY",
|
||||
"$COMMANDCODE_API_KEY",
|
||||
"COMMANDCODE_API_KEY",
|
||||
])
|
||||
@@ -162,6 +164,16 @@ export function pickCommandCodeApiKey(
|
||||
}
|
||||
|
||||
export function textContent(message: { content?: unknown }): string {
|
||||
if (typeof message.content === "string") return message.content
|
||||
if (message.content === null || message.content === undefined) return ""
|
||||
if (!Array.isArray(message.content)) {
|
||||
try {
|
||||
return JSON.stringify(message.content) ?? String(message.content)
|
||||
} catch {
|
||||
return String(message.content)
|
||||
}
|
||||
}
|
||||
|
||||
return recordArray(message.content)
|
||||
.filter((part) => part.type === "text")
|
||||
.map((part) => stringValue(part.text) ?? "")
|
||||
@@ -182,7 +194,12 @@ export function toolsToJson(tools?: readonly ToolLike[]): unknown[] {
|
||||
}))
|
||||
}
|
||||
|
||||
function completeToolCallIds(messages?: readonly MessageLike[]): Set<string> {
|
||||
interface ToolCallState {
|
||||
callIds: ReadonlySet<string>
|
||||
resultIds: ReadonlySet<string>
|
||||
}
|
||||
|
||||
function toolCallState(messages?: readonly MessageLike[]): ToolCallState {
|
||||
const callIds = new Set<string>()
|
||||
const resultIds = new Set<string>()
|
||||
|
||||
@@ -194,12 +211,12 @@ function completeToolCallIds(messages?: readonly MessageLike[]): Set<string> {
|
||||
if (id) callIds.add(id)
|
||||
}
|
||||
}
|
||||
} else if (message.role === "toolResult") {
|
||||
if (message.toolCallId) resultIds.add(message.toolCallId)
|
||||
} else if (message.role === "toolResult" && message.toolCallId) {
|
||||
resultIds.add(message.toolCallId)
|
||||
}
|
||||
}
|
||||
|
||||
return new Set([...callIds].filter((id) => resultIds.has(id)))
|
||||
return { callIds, resultIds }
|
||||
}
|
||||
|
||||
export function messagesToCC(
|
||||
@@ -210,7 +227,7 @@ export function messagesToCC(
|
||||
if (!allowImages) assertTextOnlyMessages(messages)
|
||||
|
||||
const out: unknown[] = []
|
||||
const pairedToolCallIds = completeToolCallIds(messages)
|
||||
const { callIds, resultIds } = toolCallState(messages)
|
||||
|
||||
for (const message of messages ?? []) {
|
||||
if (message.role === "user") {
|
||||
@@ -220,23 +237,37 @@ export function messagesToCC(
|
||||
})
|
||||
} else if (message.role === "assistant") {
|
||||
const parts: unknown[] = []
|
||||
const missingResults: unknown[] = []
|
||||
for (const content of recordArray(message.content)) {
|
||||
if (content.type === "text") {
|
||||
parts.push({ type: "text", text: stringValue(content.text) ?? "" })
|
||||
} else if (content.type === "toolCall") {
|
||||
const toolCallId = stringValue(content.id) ?? ""
|
||||
if (!pairedToolCallIds.has(toolCallId)) continue
|
||||
const toolName = stringValue(content.name) ?? ""
|
||||
if (!toolCallId) continue
|
||||
parts.push({
|
||||
type: "tool-call",
|
||||
toolCallId,
|
||||
toolName: stringValue(content.name) ?? "",
|
||||
toolName,
|
||||
input: recordOrEmpty(content.arguments),
|
||||
})
|
||||
if (!resultIds.has(toolCallId)) {
|
||||
missingResults.push({
|
||||
type: "tool-result",
|
||||
toolCallId,
|
||||
toolName,
|
||||
output: {
|
||||
type: "error-text",
|
||||
value: "No result — the tool call did not complete (interrupted or lost).",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parts.length > 0) out.push({ role: "assistant", content: parts })
|
||||
if (missingResults.length > 0) out.push({ role: "tool", content: missingResults })
|
||||
} else if (message.role === "toolResult") {
|
||||
if (!message.toolCallId || !pairedToolCallIds.has(message.toolCallId)) continue
|
||||
if (!message.toolCallId || !callIds.has(message.toolCallId)) continue
|
||||
out.push({
|
||||
role: "tool",
|
||||
content: [
|
||||
|
||||
+49
-16
@@ -148,6 +148,10 @@ function mappedReasoningEffort(model: ModelLike, options?: StreamOptions): strin
|
||||
return typeof mapped === "string" && mapped !== "off" ? mapped : undefined
|
||||
}
|
||||
|
||||
function isUuid(value: string): boolean {
|
||||
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)
|
||||
}
|
||||
|
||||
export function projectSlugFromPath(pathName: string): string {
|
||||
const slug = pathName
|
||||
.toLowerCase()
|
||||
@@ -232,17 +236,15 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
const stream = deps.createStream()
|
||||
|
||||
async function run() {
|
||||
// OMP may pass the legacy env-var name "COMMANDCODE_API_KEY" (old pi)
|
||||
// or "$COMMANDCODE_API_KEY" (new pi) as the apiKey value instead of
|
||||
// resolving it. Filter out these specific strings.
|
||||
const LEGACY_API_KEY_REF = "$COMMANDCODE_API_KEY"
|
||||
const OLD_API_KEY_REF = "COMMANDCODE_API_KEY"
|
||||
// Some hosts pass a literal env-var reference instead of resolving it.
|
||||
const PLACEHOLDER_API_KEYS = new Set([
|
||||
"$COMMAND_CODE_API_KEY",
|
||||
"COMMAND_CODE_API_KEY",
|
||||
"$COMMANDCODE_API_KEY",
|
||||
"COMMANDCODE_API_KEY",
|
||||
])
|
||||
const hostKey =
|
||||
options?.apiKey &&
|
||||
options.apiKey !== LEGACY_API_KEY_REF &&
|
||||
options.apiKey !== OLD_API_KEY_REF
|
||||
? options.apiKey
|
||||
: undefined
|
||||
options?.apiKey && !PLACEHOLDER_API_KEYS.has(options.apiKey) ? options.apiKey : undefined
|
||||
|
||||
const apiKey =
|
||||
hostKey ??
|
||||
@@ -262,7 +264,7 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
usage: defaultUsage(),
|
||||
stopReason: "error",
|
||||
errorMessage:
|
||||
"No Command Code API key. Run /login and select Command Code, set the COMMANDCODE_API_KEY env var, or configure ~/.commandcode/auth.json, ~/.pi/agent/auth.json or ~/.omp/agent/auth.json",
|
||||
"No Command Code API key. Run /login and select Command Code, set COMMAND_CODE_API_KEY (or legacy COMMANDCODE_API_KEY), or configure ~/.commandcode/auth.json, ~/.pi/agent/auth.json or ~/.omp/agent/auth.json",
|
||||
timestamp: now(),
|
||||
}
|
||||
stream.push({ type: "error", reason: "error", error: msg })
|
||||
@@ -424,6 +426,15 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
}
|
||||
|
||||
case "finish": {
|
||||
const rawFinishReason = stringValue(event.rawFinishReason)
|
||||
if (
|
||||
rawFinishReason &&
|
||||
/^(?:network|connection|upstream)[-_\s]?error$/i.test(rawFinishReason)
|
||||
) {
|
||||
throw new Error(
|
||||
`Provider finished with reason "${rawFinishReason}" — upstream connection failed mid-stream`,
|
||||
)
|
||||
}
|
||||
const usage = commandCodeUsage(event)
|
||||
if (usage) {
|
||||
const details = commandCodeInputTokenDetails(usage)
|
||||
@@ -447,6 +458,10 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
break
|
||||
}
|
||||
|
||||
case "abort": {
|
||||
throw abortError("Request aborted")
|
||||
}
|
||||
|
||||
case "error": {
|
||||
const message =
|
||||
commandCodeErrorMessage(event.error) ??
|
||||
@@ -464,7 +479,11 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
if (controller.signal.aborted) throw abortError("Aborted")
|
||||
|
||||
const workingDir = cwd()
|
||||
const threadId = uuid()
|
||||
const threadId = options?.sessionId
|
||||
? isUuid(options.sessionId)
|
||||
? options.sessionId
|
||||
: undefined
|
||||
: uuid()
|
||||
const reasoningEffort = mappedReasoningEffort(model, options)
|
||||
const timeoutMs = options?.timeoutMs
|
||||
|
||||
@@ -492,8 +511,8 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
tools: toolsToJson(context.tools),
|
||||
system: systemPromptToText(context.systemPrompt),
|
||||
max_tokens: generateMaxTokens(model, options),
|
||||
temperature: 0.3,
|
||||
stream: true,
|
||||
...(options?.temperature !== undefined ? { temperature: options.temperature } : {}),
|
||||
...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
|
||||
},
|
||||
threadId,
|
||||
@@ -523,7 +542,8 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
"x-cli-environment": "production",
|
||||
"x-project-slug": projectSlugFromPath(workingDir),
|
||||
"x-taste-learning": "true",
|
||||
"x-co-flag": "false",
|
||||
...(options?.sessionId ? { "x-session-id": options.sessionId } : {}),
|
||||
"User-Agent": "cli",
|
||||
...options?.headers,
|
||||
}
|
||||
const bodyStr = JSON.stringify(body)
|
||||
@@ -636,6 +656,11 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
const { done, value } = await raceAbort(reader.read(), attemptController.signal)
|
||||
if (done) {
|
||||
if (buffer.trim()) handleEvent(parseStreamEventLine(buffer))
|
||||
if (!finished) {
|
||||
throw new Error(
|
||||
"Stream ended unexpectedly before completion (no finish event) — response was truncated",
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
if (controller.signal.aborted) throw abortError("Aborted")
|
||||
@@ -659,7 +684,12 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
} catch {}
|
||||
reader = undefined
|
||||
|
||||
if (controller.signal.aborted) throw streamError
|
||||
if (
|
||||
controller.signal.aborted ||
|
||||
(streamError instanceof Error && streamError.name === "AbortError")
|
||||
) {
|
||||
throw streamError
|
||||
}
|
||||
|
||||
// Never retry after visible content was emitted (including timeout mid-stream).
|
||||
const canRetry = output.content.length === 0 && attempt < maxRetries
|
||||
@@ -696,7 +726,10 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const reason: ErrorReason = controller.signal.aborted ? "aborted" : "error"
|
||||
const reason: ErrorReason =
|
||||
controller.signal.aborted || (error instanceof Error && error.name === "AbortError")
|
||||
? "aborted"
|
||||
: "error"
|
||||
output.stopReason = reason
|
||||
output.errorMessage =
|
||||
reason === "aborted"
|
||||
|
||||
+18
-7
@@ -4,11 +4,13 @@ import { dirname } from "node:path"
|
||||
import {
|
||||
MODEL_EFFORTS,
|
||||
MODEL_INPUT_MODALITIES,
|
||||
MODEL_MAX_OUTPUT_TOKENS,
|
||||
MODEL_REASONING,
|
||||
type CommandCodeInputType,
|
||||
type CommandCodeReasoningEffort,
|
||||
} from "./commandcode-catalog.ts"
|
||||
|
||||
export { MODEL_EFFORTS, MODEL_INPUT_MODALITIES }
|
||||
export { MODEL_EFFORTS, MODEL_INPUT_MODALITIES, MODEL_MAX_OUTPUT_TOKENS, MODEL_REASONING }
|
||||
export type { CommandCodeInputType }
|
||||
|
||||
export const DEFAULT_PROVIDER_API_BASE = "https://api.commandcode.ai/provider/v1"
|
||||
@@ -55,7 +57,7 @@ export function thinkingLevelMapForEfforts(
|
||||
|
||||
export interface ThinkingMetadata {
|
||||
thinkingLevelMap: Partial<Record<PiThinkingLevel, string | null>>
|
||||
thinking: {
|
||||
thinking?: {
|
||||
mode: "effort"
|
||||
effortMap: Partial<Record<CommandCodeReasoningEffort, string>>
|
||||
efforts: readonly CommandCodeReasoningEffort[]
|
||||
@@ -64,7 +66,7 @@ export interface ThinkingMetadata {
|
||||
|
||||
export function thinkingMetadataForModel(modelId: string): ThinkingMetadata | undefined {
|
||||
const efforts = MODEL_EFFORTS[modelId]
|
||||
if (!efforts) return undefined
|
||||
if (efforts) {
|
||||
return {
|
||||
thinkingLevelMap: thinkingLevelMapForEfforts(efforts),
|
||||
thinking: {
|
||||
@@ -74,9 +76,16 @@ export function thinkingMetadataForModel(modelId: string): ThinkingMetadata | un
|
||||
},
|
||||
}
|
||||
}
|
||||
if (!isReasoningModel(modelId)) return undefined
|
||||
return { thinkingLevelMap: thinkingLevelMapForEfforts([]) }
|
||||
}
|
||||
|
||||
function isReasoningModel(modelId: string): boolean {
|
||||
return MODEL_EFFORTS[modelId] !== undefined
|
||||
return MODEL_REASONING[modelId] === true
|
||||
}
|
||||
|
||||
function maxOutputTokensForModel(modelId: string, contextLength: number): number {
|
||||
return Math.min(contextLength, MODEL_MAX_OUTPUT_TOKENS[modelId] ?? DEFAULT_MAX_OUTPUT_TOKENS)
|
||||
}
|
||||
|
||||
interface ApiModel {
|
||||
@@ -162,13 +171,15 @@ function parseCachedModel(value: unknown): CommandCodeModel {
|
||||
|
||||
const id = stringField(value, "id")
|
||||
booleanField(value, "reasoning")
|
||||
positiveNumberField(value, "maxTokens")
|
||||
const contextWindow = positiveNumberField(value, "contextWindow")
|
||||
return {
|
||||
id,
|
||||
name: stringField(value, "name"),
|
||||
api: apiForModelId(id),
|
||||
reasoning: isReasoningModel(id),
|
||||
contextWindow: positiveNumberField(value, "contextWindow"),
|
||||
maxTokens: positiveNumberField(value, "maxTokens"),
|
||||
contextWindow,
|
||||
maxTokens: maxOutputTokensForModel(id, contextWindow),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +284,7 @@ export function commandCodeModelsFromApiResponse(value: unknown): readonly Comma
|
||||
api: apiForModelId(model.id),
|
||||
reasoning: isReasoningModel(model.id),
|
||||
contextWindow: model.contextLength,
|
||||
maxTokens: Math.min(model.contextLength, DEFAULT_MAX_OUTPUT_TOKENS),
|
||||
maxTokens: maxOutputTokensForModel(model.id, model.contextLength),
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
+33
-10
@@ -18,7 +18,8 @@ import { startAuthServer } from "./auth-server.ts"
|
||||
|
||||
const STUDIO_BASE_URL = "https://commandcode.ai"
|
||||
const TEN_YEARS_MS = 10 * 365 * 24 * 60 * 60 * 1000 // API keys don't expire
|
||||
const DEFAULT_AUTH_TIMEOUT_MS = 15_000
|
||||
const DEFAULT_AUTH_TIMEOUT_MS = 120_000
|
||||
const DEFAULT_API_BASE = "https://api.commandcode.ai"
|
||||
|
||||
export interface OAuthLoginCallbacks {
|
||||
onAuth(params: { url: string }): void
|
||||
@@ -95,9 +96,34 @@ export function sanitizeApiKey(input: string): string {
|
||||
.trim()
|
||||
}
|
||||
|
||||
export async function validateApiKey(
|
||||
apiKey: string,
|
||||
options: { fetchImpl?: typeof fetch; apiBase?: string } = {},
|
||||
): Promise<void> {
|
||||
let response: Response
|
||||
try {
|
||||
response = await (options.fetchImpl ?? fetch)(
|
||||
`${options.apiBase ?? DEFAULT_API_BASE}/alpha/whoami`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
},
|
||||
)
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Could not validate the Command Code API key: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (response.status === 401) throw new Error("Invalid Command Code API key")
|
||||
if (!response.ok) {
|
||||
throw new Error(`Could not validate the Command Code API key (${response.status})`)
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForApiKey(callbacks: OAuthLoginCallbacks, message: string) {
|
||||
const apiKey = sanitizeApiKey(await callbacks.onPrompt({ message }))
|
||||
if (!apiKey) throw new Error("No Command Code API key provided")
|
||||
await validateApiKey(apiKey)
|
||||
return credentialsFromApiKey(apiKey)
|
||||
}
|
||||
|
||||
@@ -130,9 +156,10 @@ async function chooseLoginFlow(callbacks: OAuthLoginCallbacks): Promise<LoginCho
|
||||
}
|
||||
|
||||
async function browserLogin(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
||||
const stateToken = generateStateToken()
|
||||
let authServer
|
||||
try {
|
||||
authServer = await startAuthServer()
|
||||
authServer = await startAuthServer({ expectedState: stateToken })
|
||||
} catch {
|
||||
return promptForApiKey(
|
||||
callbacks,
|
||||
@@ -140,7 +167,6 @@ async function browserLogin(callbacks: OAuthLoginCallbacks): Promise<OAuthCreden
|
||||
)
|
||||
}
|
||||
|
||||
const stateToken = generateStateToken()
|
||||
const callbackUrl = `http://localhost:${authServer.port}/callback`
|
||||
const authUrl = `${STUDIO_BASE_URL}/studio/auth/cli?callback=${encodeURIComponent(callbackUrl)}&state=${encodeURIComponent(stateToken)}`
|
||||
|
||||
@@ -164,12 +190,6 @@ async function browserLogin(callbacks: OAuthLoginCallbacks): Promise<OAuthCreden
|
||||
throw error
|
||||
}
|
||||
|
||||
// Validate state token to prevent CSRF.
|
||||
if (callback.state !== stateToken) {
|
||||
authServer.server.close()
|
||||
throw new Error("State token mismatch. Authentication may have been tampered with.")
|
||||
}
|
||||
|
||||
return credentialsFromApiKey(callback.apiKey)
|
||||
}
|
||||
|
||||
@@ -182,7 +202,10 @@ async function browserLogin(callbacks: OAuthLoginCallbacks): Promise<OAuthCreden
|
||||
export async function login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
||||
const choice = await chooseLoginFlow(callbacks)
|
||||
|
||||
if (choice.type === "apiKey") return credentialsFromApiKey(choice.apiKey)
|
||||
if (choice.type === "apiKey") {
|
||||
await validateApiKey(choice.apiKey)
|
||||
return credentialsFromApiKey(choice.apiKey)
|
||||
}
|
||||
if (choice.type === "prompt") {
|
||||
return promptForApiKey(callbacks, "Paste your Command Code API key:")
|
||||
}
|
||||
|
||||
+42
-2
@@ -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-22"
|
||||
export const PRICING_LAST_VERIFIED = "2026-08-25"
|
||||
|
||||
export const ZERO_MODEL_COST: CommandCodeModelCost = {
|
||||
input: 0,
|
||||
@@ -40,7 +40,7 @@ export const ZERO_MODEL_COST: CommandCodeModelCost = {
|
||||
export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
|
||||
// Free models
|
||||
"poolside/laguna-s-2.1-free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
"inclusionai/ling-3.0-flash-free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
"stealth/ox-alpha": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
|
||||
// Open and open-weight models
|
||||
"tencent/hy3-paid": { input: 0.14, output: 0.58, cacheRead: 0.035, cacheWrite: 0 },
|
||||
@@ -76,7 +76,14 @@ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
|
||||
cacheRead: 0.007,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
"deepseek/deepseek-v4-flash-vision-exp": {
|
||||
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 },
|
||||
"Qwen/Qwen3.8-27B": { input: 0.4, output: 3, cacheRead: 0.04, cacheWrite: 0 },
|
||||
"Qwen/Qwen3.7-Max": { input: 2.5, output: 7.5, cacheRead: 0.5, cacheWrite: 3.13 },
|
||||
"Qwen/Qwen3.7-Plus": {
|
||||
input: 0.4,
|
||||
@@ -142,6 +149,13 @@ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
|
||||
cacheWrite: 0,
|
||||
},
|
||||
"meta/muse-spark-1.1": { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 },
|
||||
"meta/muse-spark-1.2": { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 },
|
||||
"meta/muse-spark-1.2-contributor": {
|
||||
input: 0.1,
|
||||
output: 0.2,
|
||||
cacheRead: 0.002,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
|
||||
// Anthropic
|
||||
// Introductory pricing through 2026-08-31.
|
||||
@@ -168,6 +182,12 @@ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
|
||||
"gpt-5.4-mini": { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 },
|
||||
|
||||
// Google and xAI
|
||||
"google/gemini-3.7-flash": {
|
||||
input: 0.75,
|
||||
output: 3.75,
|
||||
cacheRead: 0.075,
|
||||
cacheWrite: 0.04167,
|
||||
},
|
||||
"google/gemini-3.6-flash": { input: 1.5, output: 7.5, cacheRead: 0.15, cacheWrite: 0 },
|
||||
"google/gemini-3.5-flash": { input: 1.5, output: 9, cacheRead: 0.15, cacheWrite: 0 },
|
||||
"google/gemini-3.5-flash-lite": {
|
||||
@@ -183,6 +203,21 @@ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
|
||||
cacheWrite: 0,
|
||||
},
|
||||
"xai/grok-4.5": { input: 2, output: 6, cacheRead: 0.5, cacheWrite: 0 },
|
||||
"xai/grok-4.6": {
|
||||
input: 2,
|
||||
output: 6,
|
||||
cacheRead: 0.5,
|
||||
cacheWrite: 0,
|
||||
tiers: [
|
||||
{
|
||||
inputTokensAbove: 200_000,
|
||||
input: 4,
|
||||
output: 12,
|
||||
cacheRead: 1,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const TEMPORARY_PRICING: readonly TemporaryPricing[] = [
|
||||
@@ -191,4 +226,9 @@ export const TEMPORARY_PRICING: readonly TemporaryPricing[] = [
|
||||
expiresOn: "2026-08-31",
|
||||
description: "introductory pricing",
|
||||
},
|
||||
{
|
||||
models: ["google/gemini-3.7-flash"],
|
||||
expiresOn: "2026-12-31",
|
||||
description: "50% promotional pricing",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -45,7 +45,7 @@ export function registerCommandCodeQuota(
|
||||
const apiKey = pickCommandCodeApiKey(registryKey, getConfiguredKey())
|
||||
if (!apiKey) {
|
||||
ctx.ui.notify(
|
||||
"Command Code quota requires an API key. Run /login and select Command Code, or set COMMANDCODE_API_KEY.",
|
||||
"Command Code quota requires an API key. Run /login and select Command Code, or set COMMAND_CODE_API_KEY.",
|
||||
"warning",
|
||||
)
|
||||
return
|
||||
|
||||
@@ -112,6 +112,8 @@ export interface StreamOptions {
|
||||
headers?: Record<string, string>
|
||||
fetch?: typeof fetch
|
||||
maxTokens?: number
|
||||
temperature?: number
|
||||
sessionId?: string
|
||||
/** Resolved pi thinking level; forwarded only through the model's map. */
|
||||
reasoning?: string
|
||||
onPayload?: (payload: unknown, model: ModelLike) => unknown | Promise<unknown>
|
||||
|
||||
+9
-3
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"fetchedAt": "2026-08-22T21:19:37.782Z",
|
||||
"fetchedAt": "2026-08-25T13:32:11.631Z",
|
||||
"source": "https://api.commandcode.ai/provider/v1/models",
|
||||
"modelIds": [
|
||||
"claude-sonnet-5",
|
||||
@@ -18,6 +18,7 @@
|
||||
"gpt-5.4-mini",
|
||||
"deepseek/deepseek-v4-pro",
|
||||
"deepseek/deepseek-v4-flash",
|
||||
"deepseek/deepseek-v4-flash-vision-exp",
|
||||
"moonshotai/Kimi-K3",
|
||||
"moonshotai/Kimi-K2.7-Code",
|
||||
"moonshotai/Kimi-K2.7-Code-Highspeed",
|
||||
@@ -34,6 +35,7 @@
|
||||
"xiaomi/mimo-v2.5-pro",
|
||||
"xiaomi/mimo-v2.5",
|
||||
"Qwen/Qwen3.8-Max",
|
||||
"Qwen/Qwen3.8-27B",
|
||||
"Qwen/Qwen3.7-Max",
|
||||
"Qwen/Qwen3.7-Plus",
|
||||
"Qwen/Qwen3.7-Flash",
|
||||
@@ -42,6 +44,7 @@
|
||||
"stepfun/Step-3.7-Flash",
|
||||
"stepfun/Step-3.5-Flash",
|
||||
"tencent/hy3-paid",
|
||||
"google/gemini-3.7-flash",
|
||||
"google/gemini-3.6-flash",
|
||||
"google/gemini-3.5-flash",
|
||||
"google/gemini-3.5-flash-lite",
|
||||
@@ -50,9 +53,12 @@
|
||||
"nvidia/nemotron-3-ultra-550b-a55b",
|
||||
"thinkingmachines/inkling",
|
||||
"thinkingmachines/inkling-small",
|
||||
"stealth/ox-alpha",
|
||||
"poolside/laguna-s-2.1-free",
|
||||
"inclusionai/ling-3.0-flash-free",
|
||||
"meta/muse-spark-1.1",
|
||||
"xai/grok-4.5"
|
||||
"meta/muse-spark-1.2",
|
||||
"meta/muse-spark-1.2-contributor",
|
||||
"xai/grok-4.5",
|
||||
"xai/grok-4.6"
|
||||
]
|
||||
}
|
||||
|
||||
+19
-12
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"verifiedAt": "2026-08-22",
|
||||
"verifiedAt": "2026-08-25",
|
||||
"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,12 +7,13 @@
|
||||
"Qwen/Qwen3.7-Flash": [
|
||||
[32000, 0.1, 0.4, 0.02, 0.125],
|
||||
[256000, 0.2, 0.8, 0.04, 0.25]
|
||||
]
|
||||
],
|
||||
"xai/grok-4.6": [[200000, 4, 12, 1, 0]]
|
||||
},
|
||||
"costs": {
|
||||
"poolside/laguna-s-2.1-free": [0, 0, 0, 0],
|
||||
"inclusionai/ling-3.0-flash-free": [0, 0, 0, 0],
|
||||
"tencent/hy3-paid": [0.14, 0.58, 0.035, 0],
|
||||
"deepseek/deepseek-v4-pro": [0.66, 1.98, 0.022, 0],
|
||||
"deepseek/deepseek-v4-flash": [0.22, 0.66, 0.007, 0],
|
||||
"deepseek/deepseek-v4-flash-vision-exp": [0.22, 0.66, 0.007, 0],
|
||||
"moonshotai/Kimi-K3": [3, 15, 0.3, 0],
|
||||
"moonshotai/Kimi-K2.7-Code": [0.95, 4, 0.19, 0],
|
||||
"moonshotai/Kimi-K2.7-Code-Highspeed": [1.9, 8, 0.38, 0],
|
||||
@@ -26,9 +27,10 @@
|
||||
"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.66, 1.98, 0.022, 0],
|
||||
"deepseek/deepseek-v4-flash": [0.22, 0.66, 0.007, 0],
|
||||
"xiaomi/mimo-v2.5-pro": [0.435, 0.87, 0.0036, 0],
|
||||
"xiaomi/mimo-v2.5": [0.14, 0.28, 0.0028, 0],
|
||||
"Qwen/Qwen3.8-Max": [2, 6, 0.25, 2.5],
|
||||
"Qwen/Qwen3.8-27B": [0.4, 3, 0.04, 0],
|
||||
"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-Flash": [0.03, 0.13, 0.006, 0.038],
|
||||
@@ -36,13 +38,12 @@
|
||||
"Qwen/Qwen3.6-Plus": [0.5, 3, 0.1, 0],
|
||||
"stepfun/Step-3.7-Flash": [0.2, 1.15, 0.04, 0],
|
||||
"stepfun/Step-3.5-Flash": [0.1, 0.3, 0.02, 0],
|
||||
"xiaomi/mimo-v2.5-pro": [0.435, 0.87, 0.0036, 0],
|
||||
"xiaomi/mimo-v2.5": [0.14, 0.28, 0.0028, 0],
|
||||
"tencent/hy3-paid": [0.14, 0.58, 0.035, 0],
|
||||
"nvidia/nemotron-3-ultra-550b-a55b": [0.6, 2.4, 0.12, 0],
|
||||
"sakana/fugu-ultra": [5, 30, 0.5, 0],
|
||||
"thinkingmachines/inkling": [1, 4.05, 0.17, 0],
|
||||
"thinkingmachines/inkling-small": [0.5, 1.2, 0.1, 0],
|
||||
"meta/muse-spark-1.1": [1.25, 4.25, 0.15, 0],
|
||||
"poolside/laguna-s-2.1-free": [0, 0, 0, 0],
|
||||
"stealth/ox-alpha": [0, 0, 0, 0],
|
||||
"claude-sonnet-5": [2, 10, 0.2, 2.5],
|
||||
"claude-sonnet-4-6": [3, 15, 0.3, 3.75],
|
||||
"claude-fable-5": [10, 50, 1, 12.5],
|
||||
@@ -57,10 +58,16 @@
|
||||
"gpt-5.4": [2.5, 15, 0.25, 0],
|
||||
"gpt-5.3-codex": [2, 8, 0.5, 0],
|
||||
"gpt-5.4-mini": [0.75, 4.5, 0.075, 0],
|
||||
"google/gemini-3.7-flash": [0.75, 3.75, 0.075, 0.04167],
|
||||
"google/gemini-3.6-flash": [1.5, 7.5, 0.15, 0],
|
||||
"google/gemini-3.5-flash": [1.5, 9, 0.15, 0],
|
||||
"google/gemini-3.5-flash-lite": [0.3, 2.5, 0.03, 0],
|
||||
"google/gemini-3.1-flash-lite": [0.25, 1.5, 0.03, 0],
|
||||
"xai/grok-4.5": [2, 6, 0.5, 0]
|
||||
"sakana/fugu-ultra": [5, 30, 0.5, 0],
|
||||
"meta/muse-spark-1.1": [1.25, 4.25, 0.15, 0],
|
||||
"meta/muse-spark-1.2": [1.25, 4.25, 0.15, 0],
|
||||
"meta/muse-spark-1.2-contributor": [0.1, 0.2, 0.002, 0],
|
||||
"xai/grok-4.5": [2, 6, 0.5, 0],
|
||||
"xai/grok-4.6": [2, 6, 0.5, 0]
|
||||
}
|
||||
}
|
||||
|
||||
+10
-3
@@ -21,10 +21,17 @@ async function withAuthFile(
|
||||
}
|
||||
|
||||
describe("getConfiguredApiKey()", () => {
|
||||
it("prefers the environment variable", () => {
|
||||
it("prefers the official environment variable and keeps the legacy alias", () => {
|
||||
assert.equal(
|
||||
getConfiguredApiKey({ env: { COMMANDCODE_API_KEY: "env-key" }, authPaths: [] }),
|
||||
"env-key",
|
||||
getConfiguredApiKey({
|
||||
env: { COMMAND_CODE_API_KEY: "official-key", COMMANDCODE_API_KEY: "legacy-key" },
|
||||
authPaths: [],
|
||||
}),
|
||||
"official-key",
|
||||
)
|
||||
assert.equal(
|
||||
getConfiguredApiKey({ env: { COMMANDCODE_API_KEY: "legacy-key" }, authPaths: [] }),
|
||||
"legacy-key",
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
+128
-12
@@ -27,7 +27,20 @@ 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
|
||||
testProfile === "go"
|
||||
? "generate"
|
||||
: testProfile === "goat" || testProfile === "provider"
|
||||
? "provider"
|
||||
: undefined
|
||||
const expectedPlan =
|
||||
testProfile === "go"
|
||||
? "go"
|
||||
: testProfile === "goat"
|
||||
? "goat"
|
||||
: testProfile === "provider"
|
||||
? "provider"
|
||||
: undefined
|
||||
const goatVisionModel = process.env.COMMANDCODE_E2E_GOAT_VISION_MODEL ?? "google/gemini-3.7-flash"
|
||||
const marker = "commandcode-live-e2e-ok"
|
||||
|
||||
function findPiBinary() {
|
||||
@@ -48,6 +61,7 @@ function findPiBinary() {
|
||||
|
||||
function hasAuthMetadata() {
|
||||
return (
|
||||
Boolean(process.env.COMMAND_CODE_API_KEY) ||
|
||||
Boolean(process.env.COMMANDCODE_API_KEY) ||
|
||||
existsSync(join(homedir(), ".commandcode", "auth.json")) ||
|
||||
existsSync(join(homedir(), ".pi", "agent", "auth.json"))
|
||||
@@ -70,6 +84,7 @@ function safeEnv(overrides = {}) {
|
||||
env.PI_CODING_AGENT_DIR = profileAgentDir
|
||||
env.COMMANDCODE_MODELS_CACHE = join(profileAgentDir, "commandcode-models.json")
|
||||
} else {
|
||||
delete env.COMMAND_CODE_API_KEY
|
||||
delete env.COMMANDCODE_API_KEY
|
||||
}
|
||||
return env
|
||||
@@ -102,7 +117,7 @@ function run(command, args, options = {}) {
|
||||
})
|
||||
}
|
||||
|
||||
async function runRpc(extension, action, timeoutMs = 120_000) {
|
||||
async function runRpc(extension, action, timeoutMs = 120_000, model = testModel) {
|
||||
const child = spawn(
|
||||
piBin,
|
||||
[
|
||||
@@ -114,7 +129,9 @@ async function runRpc(extension, action, timeoutMs = 120_000) {
|
||||
"--provider",
|
||||
"commandcode",
|
||||
"--model",
|
||||
testModel,
|
||||
model,
|
||||
"--thinking",
|
||||
"high",
|
||||
],
|
||||
{ cwd: projectDir, env: safeEnv(), stdio: ["pipe", "pipe", "pipe"] },
|
||||
)
|
||||
@@ -222,8 +239,19 @@ try {
|
||||
await waitFor(
|
||||
(event) => event.type === "response" && event.id === "reasoning-turn-1" && event.success,
|
||||
)
|
||||
await waitFor((event) => event.type === "agent_settled")
|
||||
const firstThinkingDeltas = countThinkingDeltas(firstStart)
|
||||
const firstSettled = await waitFor(
|
||||
(event) => event.type === "agent_settled" && events.indexOf(event) >= firstStart,
|
||||
)
|
||||
const firstSettledIndex = events.indexOf(firstSettled)
|
||||
const firstThinkingDeltas = events
|
||||
.slice(firstStart, firstSettledIndex + 1)
|
||||
.filter(
|
||||
(event) =>
|
||||
event.type === "message_update" &&
|
||||
event.assistantMessageEvent?.type === "thinking_delta" &&
|
||||
typeof event.assistantMessageEvent.delta === "string" &&
|
||||
event.assistantMessageEvent.delta.length > 0,
|
||||
).length
|
||||
|
||||
const secondStart = events.length
|
||||
send({
|
||||
@@ -235,15 +263,16 @@ try {
|
||||
await waitFor(
|
||||
(event) => event.type === "response" && event.id === "reasoning-turn-2" && event.success,
|
||||
)
|
||||
await waitFor((event) => event.type === "agent_settled" && events.indexOf(event) >= secondStart)
|
||||
const secondSettled = await waitFor(
|
||||
(event) => event.type === "agent_settled" && events.indexOf(event) >= secondStart,
|
||||
)
|
||||
const secondThinkingDeltas = countThinkingDeltas(secondStart)
|
||||
assert.ok(events.indexOf(secondSettled) >= secondStart)
|
||||
|
||||
return { firstThinkingDeltas, secondThinkingDeltas, stderr: getStderr() }
|
||||
})
|
||||
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")
|
||||
@@ -281,15 +310,66 @@ try {
|
||||
typeof event.message === "string" &&
|
||||
event.message.includes("source:"),
|
||||
)
|
||||
return { names, refresh: refresh.message, status: status.message, stderr: getStderr() }
|
||||
|
||||
send({ id: "quota", type: "prompt", message: "/commandcode-quota" })
|
||||
await waitFor((event) => event.type === "response" && event.id === "quota" && event.success)
|
||||
const quota = await waitFor(
|
||||
(event) =>
|
||||
event.type === "extension_ui_request" &&
|
||||
event.method === "notify" &&
|
||||
typeof event.message === "string" &&
|
||||
event.message.includes("Plan:"),
|
||||
)
|
||||
return {
|
||||
names,
|
||||
refresh: refresh.message,
|
||||
status: status.message,
|
||||
quota: quota.message,
|
||||
stderr: getStderr(),
|
||||
}
|
||||
})
|
||||
assert.ok(runtime.names.includes("commandcode-refresh"))
|
||||
assert.ok(runtime.names.includes("commandcode-status"))
|
||||
assert.ok(runtime.names.includes("commandcode-quota"))
|
||||
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)
|
||||
if (expectedPlan) assert.match(runtime.quota, new RegExp(`Plan:.*\\b${expectedPlan}\\b`, "i"))
|
||||
assert.doesNotMatch(
|
||||
`${runtime.refresh}\n${runtime.status}\n${runtime.quota}\n${runtime.stderr}`,
|
||||
/Bearer\s+\S+/i,
|
||||
)
|
||||
|
||||
console.log("[live-e2e] live abort through real RPC host")
|
||||
const abortResult = await runRpc(extensionPath, async ({ send, waitFor, events, getStderr }) => {
|
||||
const startIndex = events.length
|
||||
send({
|
||||
id: "abort-turn",
|
||||
type: "prompt",
|
||||
message: "Write a very long detailed explanation of every integer from 1 to 10000.",
|
||||
})
|
||||
await waitFor(
|
||||
(event) => event.type === "response" && event.id === "abort-turn" && event.success,
|
||||
)
|
||||
await waitFor((event) => event.type === "message_update" && events.indexOf(event) >= startIndex)
|
||||
send({ id: "abort", type: "abort" })
|
||||
await waitFor((event) => event.type === "response" && event.id === "abort" && event.success)
|
||||
await waitFor((event) => event.type === "agent_settled" && events.indexOf(event) >= startIndex)
|
||||
return {
|
||||
aborted: events
|
||||
.slice(startIndex)
|
||||
.some(
|
||||
(event) =>
|
||||
event.type === "message_end" &&
|
||||
event.message?.role === "assistant" &&
|
||||
event.message?.stopReason === "aborted",
|
||||
),
|
||||
stderr: getStderr(),
|
||||
}
|
||||
})
|
||||
assert.equal(abortResult.aborted, true)
|
||||
assert.doesNotMatch(abortResult.stderr, /Bearer\s+\S+/i)
|
||||
|
||||
console.log("[live-e2e] live tool-call round trip")
|
||||
const toolRoot = join(tempRoot, "tool-roundtrip")
|
||||
@@ -317,9 +397,45 @@ try {
|
||||
)
|
||||
assert.equal(toolResult.code, 0, toolResult.stderr)
|
||||
assert.match(toolResult.stdout, new RegExp(marker))
|
||||
assert.equal(readFileSync(targetPath, "utf-8"), marker)
|
||||
assert.equal(readFileSync(targetPath, "utf-8").trimEnd(), marker)
|
||||
|
||||
if (testProfile !== "provider") {
|
||||
if (testProfile === "goat") {
|
||||
console.log("[live-e2e] live vision request through Provider API")
|
||||
const vision = await runRpc(
|
||||
extensionPath,
|
||||
async ({ send, waitFor, events, getStderr }) => {
|
||||
const startIndex = events.length
|
||||
send({
|
||||
id: "vision",
|
||||
type: "prompt",
|
||||
message: "Describe the attached image briefly.",
|
||||
images: [
|
||||
{
|
||||
type: "image",
|
||||
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
|
||||
mimeType: "image/png",
|
||||
},
|
||||
],
|
||||
})
|
||||
await waitFor(
|
||||
(event) => event.type === "response" && event.id === "vision" && event.success,
|
||||
)
|
||||
await waitFor(
|
||||
(event) => event.type === "agent_settled" && events.indexOf(event) >= startIndex,
|
||||
)
|
||||
const messageEnd = events
|
||||
.slice(startIndex)
|
||||
.find((event) => event.type === "message_end" && event.message?.role === "assistant")
|
||||
return { messageEnd, stderr: getStderr() }
|
||||
},
|
||||
180_000,
|
||||
goatVisionModel,
|
||||
)
|
||||
assert.notEqual(vision.messageEnd?.message?.stopReason, "error")
|
||||
assert.doesNotMatch(vision.stderr, /Bearer\s+\S+/i)
|
||||
}
|
||||
|
||||
if (testProfile === "go") {
|
||||
console.log("[live-e2e] image rejection through real RPC host")
|
||||
const image = await runRpc(extensionPath, async ({ send, waitFor, events }) => {
|
||||
send({
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
commandCodeModelMetadataFromContents,
|
||||
diffModelMetadata,
|
||||
hasModelMetadataDiff,
|
||||
parseBundleModelCapabilities,
|
||||
parseKnownTextOnlyModelIds,
|
||||
parseModelsReference,
|
||||
parsePackageVersion,
|
||||
@@ -21,7 +22,7 @@ const MODELS_REFERENCE = `
|
||||
`
|
||||
|
||||
const CLI_BUNDLE =
|
||||
'const catalog=new Set(["text-model"]),__name(isKnownTextOnlyModel,"isKnownTextOnlyModel")'
|
||||
'const V={id:"vision-model",inputModalities:["text","image"],reasoning:!0,reasoningEfforts:["low","high"],maxOutputTokens:32768},T={id:"text-model",inputModalities:["text"]},catalog=new Set(["text-model"]),__name(isKnownTextOnlyModel,"isKnownTextOnlyModel")'
|
||||
|
||||
describe("Command Code model metadata checker", () => {
|
||||
it("parses model ids and reasoning efforts from the generated reference", () => {
|
||||
@@ -42,29 +43,39 @@ describe("Command Code model metadata checker", () => {
|
||||
assert.throws(() => parsePackageVersion("latest"), /one semantic version/)
|
||||
})
|
||||
|
||||
it("derives image support by excluding known text-only models", () => {
|
||||
it("derives image, reasoning, effort, and output-limit metadata", () => {
|
||||
assert.deepEqual(parseBundleModelCapabilities(CLI_BUNDLE, ["text-model", "vision-model"]), {
|
||||
reasoningModelIds: ["vision-model"],
|
||||
maxOutputTokens: { "vision-model": 32_768 },
|
||||
})
|
||||
assert.deepEqual(commandCodeModelMetadataFromContents(MODELS_REFERENCE, CLI_BUNDLE), {
|
||||
imageModelIds: ["vision-model"],
|
||||
reasoningModelIds: ["vision-model"],
|
||||
reasoningEfforts: { "vision-model": ["low", "high"] },
|
||||
maxOutputTokens: { "vision-model": 32_768 },
|
||||
})
|
||||
})
|
||||
|
||||
it("reports additions, removals, and changed reasoning efforts", () => {
|
||||
const current: CommandCodeModelMetadata = {
|
||||
imageModelIds: ["removed-image", "stable-image"],
|
||||
reasoningModelIds: ["removed-reasoning", "stable-reasoning"],
|
||||
reasoningEfforts: {
|
||||
"changed-reasoning": ["low"],
|
||||
"removed-reasoning": ["high"],
|
||||
"stable-reasoning": ["low", "high"],
|
||||
"changed-effort": ["low"],
|
||||
"removed-effort": ["high"],
|
||||
"stable-effort": ["low", "high"],
|
||||
},
|
||||
maxOutputTokens: { "changed-output": 1, "removed-output": 2, "stable-output": 3 },
|
||||
}
|
||||
const upstream: CommandCodeModelMetadata = {
|
||||
imageModelIds: ["added-image", "stable-image"],
|
||||
reasoningModelIds: ["added-reasoning", "stable-reasoning"],
|
||||
reasoningEfforts: {
|
||||
"added-reasoning": ["max"],
|
||||
"changed-reasoning": ["low", "high"],
|
||||
"stable-reasoning": ["low", "high"],
|
||||
"added-effort": ["max"],
|
||||
"changed-effort": ["low", "high"],
|
||||
"stable-effort": ["low", "high"],
|
||||
},
|
||||
maxOutputTokens: { "added-output": 4, "changed-output": 5, "stable-output": 3 },
|
||||
}
|
||||
|
||||
const diff = diffModelMetadata(current, upstream)
|
||||
@@ -75,7 +86,12 @@ describe("Command Code model metadata checker", () => {
|
||||
removedImageModelIds: ["removed-image"],
|
||||
addedReasoningModelIds: ["added-reasoning"],
|
||||
removedReasoningModelIds: ["removed-reasoning"],
|
||||
changedReasoningModelIds: ["changed-reasoning"],
|
||||
addedEffortModelIds: ["added-effort"],
|
||||
removedEffortModelIds: ["removed-effort"],
|
||||
changedEffortModelIds: ["changed-effort"],
|
||||
addedMaxOutputModelIds: ["added-output"],
|
||||
removedMaxOutputModelIds: ["removed-output"],
|
||||
changedMaxOutputModelIds: ["changed-output"],
|
||||
})
|
||||
assert.equal(hasModelMetadataDiff(diff), true)
|
||||
})
|
||||
@@ -83,7 +99,9 @@ describe("Command Code model metadata checker", () => {
|
||||
it("reports CLI version drift even when model metadata is unchanged", () => {
|
||||
const metadata: CommandCodeModelMetadata = {
|
||||
imageModelIds: ["vision-model"],
|
||||
reasoningModelIds: ["vision-model"],
|
||||
reasoningEfforts: { "vision-model": ["low"] },
|
||||
maxOutputTokens: { "vision-model": 32_768 },
|
||||
}
|
||||
|
||||
const diff = diffModelMetadata(metadata, metadata, "1.32.2", "1.33.0")
|
||||
@@ -96,10 +114,12 @@ describe("Command Code model metadata checker", () => {
|
||||
assert.equal(
|
||||
renderCommandCodeCatalog("1.33.0", {
|
||||
imageModelIds: ["b-model", "a-model"],
|
||||
reasoningModelIds: ["c-model", "a-model"],
|
||||
reasoningEfforts: {
|
||||
"b-model": ["high", "max"],
|
||||
"a-model": ["low"],
|
||||
},
|
||||
maxOutputTokens: { "b-model": 32_768 },
|
||||
}),
|
||||
`export const COMMAND_CODE_CLI_VERSION = "1.33.0"
|
||||
|
||||
@@ -115,10 +135,19 @@ export const MODEL_INPUT_MODALITIES: Readonly<Record<string, readonly CommandCod
|
||||
"b-model": ["text", "image"],
|
||||
}
|
||||
|
||||
export const MODEL_REASONING: Readonly<Record<string, true>> = {
|
||||
"a-model": true,
|
||||
"c-model": true,
|
||||
}
|
||||
|
||||
export const MODEL_EFFORTS: Readonly<Record<string, readonly CommandCodeReasoningEffort[]>> = {
|
||||
"a-model": ["low"],
|
||||
"b-model": ["high", "max"],
|
||||
}
|
||||
|
||||
export const MODEL_MAX_OUTPUT_TOKENS: Readonly<Record<string, number>> = {
|
||||
"b-model": 32_768,
|
||||
}
|
||||
`,
|
||||
)
|
||||
assert.equal(
|
||||
|
||||
+44
-3
@@ -16,6 +16,8 @@ import {
|
||||
loadCommandCodeModels,
|
||||
MODEL_EFFORTS,
|
||||
MODEL_INPUT_MODALITIES,
|
||||
MODEL_MAX_OUTPUT_TOKENS,
|
||||
MODEL_REASONING,
|
||||
modelSupportsImageInput,
|
||||
thinkingLevelMapForEfforts,
|
||||
thinkingMetadataForModel,
|
||||
@@ -41,7 +43,7 @@ const EXPECTED_MODELS: readonly CommandCodeModel[] = [
|
||||
id: "Qwen/Qwen3.7-Max",
|
||||
name: "Qwen 3.7 Max (CC)",
|
||||
api: "openai-completions",
|
||||
reasoning: false,
|
||||
reasoning: true,
|
||||
contextWindow: 1_000_000,
|
||||
maxTokens: 65_536,
|
||||
},
|
||||
@@ -124,17 +126,55 @@ describe("commandCodeModelsFromApiResponse()", () => {
|
||||
}
|
||||
})
|
||||
|
||||
it("marks only known reasoning models as reasoning-capable", () => {
|
||||
it("tracks reasoning independently from selectable effort levels", () => {
|
||||
const models = commandCodeModelsFromApiResponse({
|
||||
object: "list",
|
||||
data: [
|
||||
{ ...API_RESPONSE.data[0], id: "deepseek/deepseek-v4-flash" },
|
||||
{ ...API_RESPONSE.data[0], id: "moonshotai/Kimi-K3" },
|
||||
{ ...API_RESPONSE.data[0], id: "new-model-without-metadata" },
|
||||
],
|
||||
})
|
||||
|
||||
assert.equal(models[0]?.reasoning, true)
|
||||
assert.equal(models[1]?.reasoning, false)
|
||||
assert.equal(models[1]?.reasoning, true)
|
||||
assert.deepEqual(thinkingMetadataForModel("moonshotai/Kimi-K3"), {
|
||||
thinkingLevelMap: {
|
||||
minimal: null,
|
||||
low: null,
|
||||
medium: null,
|
||||
high: null,
|
||||
xhigh: null,
|
||||
max: null,
|
||||
},
|
||||
})
|
||||
assert.equal(models[2]?.reasoning, false)
|
||||
assert.equal(Object.keys(MODEL_REASONING).length, 48)
|
||||
})
|
||||
|
||||
it("uses model-specific output limits from the CLI catalog", () => {
|
||||
const models = commandCodeModelsFromApiResponse({
|
||||
object: "list",
|
||||
data: [
|
||||
{ ...API_RESPONSE.data[0], id: "Qwen/Qwen3.8-27B", context_length: 262_144 },
|
||||
{ ...API_RESPONSE.data[0], id: "stealth/ox-alpha", context_length: 1_048_576 },
|
||||
{
|
||||
...API_RESPONSE.data[0],
|
||||
id: "poolside/laguna-s-2.1-free",
|
||||
context_length: 256_000,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
assert.deepEqual(
|
||||
models.map(({ id, maxTokens }) => ({ id, maxTokens })),
|
||||
[
|
||||
{ id: "Qwen/Qwen3.8-27B", maxTokens: 32_768 },
|
||||
{ id: "stealth/ox-alpha", maxTokens: 131_072 },
|
||||
{ id: "poolside/laguna-s-2.1-free", maxTokens: 32_768 },
|
||||
],
|
||||
)
|
||||
assert.equal(Object.keys(MODEL_MAX_OUTPUT_TOKENS).length, 3)
|
||||
})
|
||||
|
||||
it(`uses the command-code@${COMMAND_CODE_CLI_VERSION} reasoning effort catalog`, () => {
|
||||
@@ -151,6 +191,7 @@ describe("commandCodeModelsFromApiResponse()", () => {
|
||||
for (const [modelId, efforts] of Object.entries(MODEL_EFFORTS)) {
|
||||
const metadata = thinkingMetadataForModel(modelId)
|
||||
assert.ok(metadata, `${modelId} should have reasoning metadata`)
|
||||
assert.ok(metadata.thinking)
|
||||
assert.equal(metadata.thinking.mode, "effort")
|
||||
assert.deepEqual(metadata.thinking.efforts, efforts)
|
||||
assert.deepEqual(
|
||||
|
||||
+99
-19
@@ -9,7 +9,7 @@ import assert from "node:assert/strict"
|
||||
import { describe, it } from "node:test"
|
||||
|
||||
import { startAuthServer, type AuthCallback } from "../src/auth-server.ts"
|
||||
import { getApiKey, login, refreshToken, sanitizeApiKey } from "../src/oauth.ts"
|
||||
import { getApiKey, login, refreshToken, sanitizeApiKey, validateApiKey } from "../src/oauth.ts"
|
||||
|
||||
/**
|
||||
* Helper: wait for an HTTP server to close, or resolve immediately if already closed.
|
||||
@@ -24,9 +24,27 @@ function waitForClose(server: {
|
||||
})
|
||||
}
|
||||
|
||||
async function withValidApiKeyFetch<T>(run: () => Promise<T>): Promise<T> {
|
||||
const originalFetch = globalThis.fetch
|
||||
globalThis.fetch = (input, init) => {
|
||||
if (String(input).endsWith("/alpha/whoami")) {
|
||||
return Promise.resolve(new Response(JSON.stringify({ user: {} }), { status: 200 }))
|
||||
}
|
||||
return originalFetch(input, init)
|
||||
}
|
||||
try {
|
||||
return await run()
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch
|
||||
}
|
||||
}
|
||||
|
||||
describe("startAuthServer()", () => {
|
||||
it("starts on a localhost port and accepts a valid callback POST", async () => {
|
||||
const { server, port, waitForCallback } = await startAuthServer({ startPort: 0 })
|
||||
const { server, port, waitForCallback } = await startAuthServer({
|
||||
startPort: 0,
|
||||
expectedState: "test-state-token",
|
||||
})
|
||||
|
||||
const callbackData: AuthCallback = {
|
||||
apiKey: "user_testKey123",
|
||||
@@ -57,6 +75,42 @@ describe("startAuthServer()", () => {
|
||||
await waitForClose(server)
|
||||
})
|
||||
|
||||
it("rejects a mismatched state without closing the callback server", async () => {
|
||||
const { server, port, waitForCallback } = await startAuthServer({
|
||||
startPort: 0,
|
||||
expectedState: "correct-state",
|
||||
})
|
||||
|
||||
const invalidResponse = await fetch(`http://127.0.0.1:${port}/callback`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Origin: "https://commandcode.ai" },
|
||||
body: JSON.stringify({
|
||||
apiKey: "user_badState",
|
||||
state: "wrong-state",
|
||||
userId: "user_789",
|
||||
userName: "Attacker",
|
||||
keyName: "evil-key",
|
||||
}),
|
||||
})
|
||||
assert.equal(invalidResponse.status, 403)
|
||||
assert.equal(server.listening, true)
|
||||
|
||||
const validResponse = await fetch(`http://127.0.0.1:${port}/callback`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Origin: "https://commandcode.ai" },
|
||||
body: JSON.stringify({
|
||||
apiKey: "user_valid",
|
||||
state: "correct-state",
|
||||
userId: "user_123",
|
||||
userName: "Valid User",
|
||||
keyName: "valid-key",
|
||||
}),
|
||||
})
|
||||
assert.equal(validResponse.status, 200)
|
||||
assert.equal((await waitForCallback).apiKey, "user_valid")
|
||||
await waitForClose(server)
|
||||
})
|
||||
|
||||
it("rejects when the callback indicates access_denied", async () => {
|
||||
const { server, port, waitForCallback } = await startAuthServer({ startPort: 0 })
|
||||
|
||||
@@ -176,6 +230,18 @@ describe("OAuth functions", () => {
|
||||
it("sanitizeApiKey removes paste markers, control chars, and whitespace", () => {
|
||||
assert.equal(sanitizeApiKey("\u001b[200~ user_manualKey\n\u001b[201~"), "user_manualKey")
|
||||
})
|
||||
|
||||
it("validates manual API keys through whoami", async () => {
|
||||
await validateApiKey("valid-key", {
|
||||
fetchImpl: () => Promise.resolve(new Response(JSON.stringify({ user: {} }), { status: 200 })),
|
||||
})
|
||||
await assert.rejects(
|
||||
validateApiKey("invalid-key", {
|
||||
fetchImpl: () => Promise.resolve(new Response("unauthorized", { status: 401 })),
|
||||
}),
|
||||
/Invalid Command Code API key/,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("login()", () => {
|
||||
@@ -242,7 +308,8 @@ describe("login()", () => {
|
||||
const promptMessages: string[] = []
|
||||
|
||||
try {
|
||||
const result = await login({
|
||||
const result = await withValidApiKeyFetch(() =>
|
||||
login({
|
||||
onAuth(params: { url: string }) {
|
||||
authUrl = params.url
|
||||
},
|
||||
@@ -250,7 +317,8 @@ describe("login()", () => {
|
||||
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(promptMessages[1] ?? "", /Paste your Command Code API key/)
|
||||
@@ -265,14 +333,16 @@ describe("login()", () => {
|
||||
|
||||
it("accepts a directly pasted API key", async () => {
|
||||
let authOpened = false
|
||||
const result = await login({
|
||||
const result = await withValidApiKeyFetch(() =>
|
||||
login({
|
||||
onAuth() {
|
||||
authOpened = true
|
||||
},
|
||||
onPrompt(): Promise<string> {
|
||||
return Promise.resolve("user_directApiKey")
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
assert.equal(authOpened, false)
|
||||
assert.equal(result.access, "user_directApiKey")
|
||||
@@ -280,7 +350,8 @@ describe("login()", () => {
|
||||
|
||||
it("offers an explicit API key prompt", async () => {
|
||||
let promptCount = 0
|
||||
const result = await login({
|
||||
const result = await withValidApiKeyFetch(() =>
|
||||
login({
|
||||
onAuth() {
|
||||
throw new Error("browser should not open")
|
||||
},
|
||||
@@ -288,13 +359,14 @@ describe("login()", () => {
|
||||
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("keeps waiting after a state mismatch and accepts the legitimate callback", async () => {
|
||||
let authUrl = ""
|
||||
const callbacks = {
|
||||
onAuth(params: { url: string }) {
|
||||
@@ -305,12 +377,7 @@ describe("login()", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const loginPromise: Promise<string> = login(callbacks).then(
|
||||
() => {
|
||||
throw new Error("Expected login to reject")
|
||||
},
|
||||
(e: Error) => e.message,
|
||||
)
|
||||
const loginPromise = login(callbacks)
|
||||
|
||||
// Wait for onAuth to be called asynchronously
|
||||
while (!authUrl) await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
@@ -318,8 +385,8 @@ describe("login()", () => {
|
||||
const url = new URL(authUrl)
|
||||
const port = parseInt(url.searchParams.get("callback")?.match(/localhost:(\d+)/)?.[1] ?? "0")
|
||||
|
||||
// Post back with a wrong state token
|
||||
await fetch(`http://127.0.0.1:${port}/callback`, {
|
||||
// Post back with a wrong state token.
|
||||
const invalidResponse = await fetch(`http://127.0.0.1:${port}/callback`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Origin: "https://commandcode.ai" },
|
||||
body: JSON.stringify({
|
||||
@@ -331,7 +398,20 @@ describe("login()", () => {
|
||||
}),
|
||||
})
|
||||
|
||||
const errorMsg = await loginPromise
|
||||
assert.match(errorMsg, /State token mismatch/)
|
||||
assert.equal(invalidResponse.status, 403)
|
||||
|
||||
const validResponse = await fetch(`http://127.0.0.1:${port}/callback`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Origin: "https://commandcode.ai" },
|
||||
body: JSON.stringify({
|
||||
apiKey: "user_goodState",
|
||||
state: url.searchParams.get("state"),
|
||||
userId: "user_123",
|
||||
userName: "Real User",
|
||||
keyName: "real-key",
|
||||
}),
|
||||
})
|
||||
assert.equal(validResponse.status, 200)
|
||||
assert.equal((await loginPromise).access, "user_goodState")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -133,7 +133,7 @@ function runOmp(args, timeoutMs = 30_000) {
|
||||
HOME: tempHome,
|
||||
USERPROFILE: tempHome,
|
||||
PI_CODING_AGENT_DIR: join(tempHome, ".omp", "agent"),
|
||||
COMMANDCODE_API_KEY: "mock-key",
|
||||
COMMAND_CODE_API_KEY: "mock-key",
|
||||
COMMANDCODE_API_BASE: `${apiBase}/provider/v1`,
|
||||
COMMANDCODE_MODELS_URL: `${apiBase}/provider/v1/models`,
|
||||
},
|
||||
|
||||
@@ -22,7 +22,7 @@ const { writeFileSync } = require("node:fs")
|
||||
writeFileSync(process.env.FAKE_PI_LOG, JSON.stringify({
|
||||
args: process.argv.slice(2),
|
||||
agentDir: process.env.PI_CODING_AGENT_DIR ?? null,
|
||||
apiKey: process.env.COMMANDCODE_API_KEY ?? null,
|
||||
apiKey: process.env.COMMAND_CODE_API_KEY ?? process.env.COMMANDCODE_API_KEY ?? null,
|
||||
skipVersionCheck: process.env.PI_SKIP_VERSION_CHECK,
|
||||
}))
|
||||
NODE
|
||||
@@ -38,7 +38,8 @@ NODE
|
||||
PATH: `${fakeBin}${delimiter}${process.env.PATH ?? ""}`,
|
||||
FAKE_PI_LOG: logPath,
|
||||
PI_CODING_AGENT_DIR: "/existing/pi-agent",
|
||||
COMMANDCODE_API_KEY: "existing-key",
|
||||
COMMAND_CODE_API_KEY: "official-existing-key",
|
||||
COMMANDCODE_API_KEY: "legacy-existing-key",
|
||||
},
|
||||
encoding: "utf8",
|
||||
})
|
||||
|
||||
@@ -26,7 +26,8 @@ appendFileSync(process.env.FAKE_PI_LOG, JSON.stringify({
|
||||
skipVersionCheck: process.env.PI_SKIP_VERSION_CHECK,
|
||||
home: process.env.HOME,
|
||||
userProfile: process.env.USERPROFILE,
|
||||
inheritedApiKey: process.env.COMMANDCODE_API_KEY ?? null,
|
||||
inheritedApiKey:
|
||||
process.env.COMMAND_CODE_API_KEY ?? process.env.COMMANDCODE_API_KEY ?? null,
|
||||
}) + "\\n")
|
||||
NODE
|
||||
if [ "$1" = "install" ]; then exit 0; fi
|
||||
@@ -42,7 +43,8 @@ exit ${exitStatus}
|
||||
...process.env,
|
||||
PATH: `${fakeBin}${delimiter}${process.env.PATH ?? ""}`,
|
||||
FAKE_PI_LOG: logPath,
|
||||
COMMANDCODE_API_KEY: "must-not-leak",
|
||||
COMMAND_CODE_API_KEY: "must-not-leak-official",
|
||||
COMMANDCODE_API_KEY: "must-not-leak-legacy",
|
||||
},
|
||||
encoding: "utf8",
|
||||
})
|
||||
|
||||
@@ -215,8 +215,8 @@ const env = {
|
||||
PI_CODING_AGENT_DIR: agentDir,
|
||||
PI_CODING_AGENT_SESSION_DIR: join(tempHome, "sessions"),
|
||||
COMMANDCODE_API_BASE: `${apiBase}/provider/v1`,
|
||||
COMMANDCODE_API_KEY: "mock-key",
|
||||
COMMANDCODE_ZDR: "1",
|
||||
COMMAND_CODE_API_KEY: "mock-key",
|
||||
CMD_ZDR: "1",
|
||||
COMMANDCODE_MODELS_URL: `${apiBase}/provider/v1/models`,
|
||||
}
|
||||
|
||||
|
||||
+30
-3
@@ -27,7 +27,7 @@ const fixtureUrl = new URL("./fixtures/commandcode-model-ids.json", import.meta.
|
||||
const fixture = JSON.parse(await readFile(fixtureUrl, "utf-8")) as ModelCatalogSnapshot
|
||||
const pricingFixtureUrl = new URL("./fixtures/commandcode-pricing.json", import.meta.url)
|
||||
const pricingFixture = JSON.parse(await readFile(pricingFixtureUrl, "utf-8")) as PricingSnapshot
|
||||
const freeModels = new Set(["poolside/laguna-s-2.1-free", "inclusionai/ling-3.0-flash-free"])
|
||||
const freeModels = new Set(["poolside/laguna-s-2.1-free", "stealth/ox-alpha"])
|
||||
|
||||
function assertCost(
|
||||
modelId: string,
|
||||
@@ -50,7 +50,7 @@ function assertCost(
|
||||
describe("MODEL_COSTS pricing overlay", () => {
|
||||
it("covers the current Command Code model catalog snapshot", () => {
|
||||
assert.equal(fixture.source, "https://api.commandcode.ai/provider/v1/models")
|
||||
assert.match(fixture.fetchedAt, /^2026-08-22T/)
|
||||
assert.match(fixture.fetchedAt, /^2026-08-25T/)
|
||||
|
||||
const catalogIds = [...fixture.modelIds].sort()
|
||||
const pricedIds = Object.keys(MODEL_COSTS).sort()
|
||||
@@ -138,6 +138,24 @@ describe("MODEL_COSTS pricing overlay", () => {
|
||||
cacheRead: 0.03,
|
||||
cacheWrite: 0,
|
||||
})
|
||||
assertCost("Qwen/Qwen3.8-27B", {
|
||||
input: 0.4,
|
||||
output: 3,
|
||||
cacheRead: 0.04,
|
||||
cacheWrite: 0,
|
||||
})
|
||||
assertCost("google/gemini-3.7-flash", {
|
||||
input: 0.75,
|
||||
output: 3.75,
|
||||
cacheRead: 0.075,
|
||||
cacheWrite: 0.04167,
|
||||
})
|
||||
assertCost("meta/muse-spark-1.2-contributor", {
|
||||
input: 0.1,
|
||||
output: 0.2,
|
||||
cacheRead: 0.002,
|
||||
cacheWrite: 0,
|
||||
})
|
||||
})
|
||||
|
||||
it("uses the documented base rates for context-dependent models", () => {
|
||||
@@ -165,11 +183,20 @@ describe("MODEL_COSTS pricing overlay", () => {
|
||||
cacheRead: 0.02,
|
||||
cacheWrite: 0.25,
|
||||
})
|
||||
assert.deepEqual(MODEL_COSTS["xai/grok-4.6"]?.tiers, [
|
||||
{
|
||||
inputTokensAbove: 200_000,
|
||||
input: 4,
|
||||
output: 12,
|
||||
cacheRead: 1,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("tracks pricing provenance", () => {
|
||||
assert.equal(PRICING_SOURCE_URL, "https://commandcode.ai/docs/resources/pricing-limits")
|
||||
assert.equal(PRICING_LAST_VERIFIED, "2026-08-22")
|
||||
assert.equal(PRICING_LAST_VERIFIED, "2026-08-25")
|
||||
})
|
||||
|
||||
it("fails once temporary pricing needs review", () => {
|
||||
|
||||
@@ -27,8 +27,18 @@ import { redactCommandCodeErrorText } from "../src/overflow.ts"
|
||||
import { objectAt } from "./helpers.ts"
|
||||
|
||||
describe("getApiKey()", () => {
|
||||
it("uses COMMANDCODE_API_KEY from provided env", () => {
|
||||
assert.equal(getApiKey({ env: { COMMANDCODE_API_KEY: "env-key" }, authPaths: [] }), "env-key")
|
||||
it("uses the official API key env var before the legacy alias", () => {
|
||||
assert.equal(
|
||||
getApiKey({
|
||||
env: { COMMAND_CODE_API_KEY: "official-key", COMMANDCODE_API_KEY: "legacy-key" },
|
||||
authPaths: [],
|
||||
}),
|
||||
"official-key",
|
||||
)
|
||||
assert.equal(
|
||||
getApiKey({ env: { COMMANDCODE_API_KEY: "legacy-key" }, authPaths: [] }),
|
||||
"legacy-key",
|
||||
)
|
||||
})
|
||||
|
||||
it("reads apiKey, commandcode, pi OAuth, and official CLI credential fields", () => {
|
||||
@@ -109,11 +119,14 @@ describe("error redaction", () => {
|
||||
|
||||
describe("pickCommandCodeApiKey()", () => {
|
||||
it("falls back to the host key for a placeholder registry value", () => {
|
||||
assert.equal(pickCommandCodeApiKey("$COMMAND_CODE_API_KEY", "file-key"), "file-key")
|
||||
assert.equal(pickCommandCodeApiKey("COMMAND_CODE_API_KEY", "file-key"), "file-key")
|
||||
assert.equal(pickCommandCodeApiKey("$COMMANDCODE_API_KEY", "file-key"), "file-key")
|
||||
assert.equal(pickCommandCodeApiKey("COMMANDCODE_API_KEY", "file-key"), "file-key")
|
||||
})
|
||||
|
||||
it("returns undefined when only a placeholder is provided (no fallback)", () => {
|
||||
assert.equal(pickCommandCodeApiKey("$COMMAND_CODE_API_KEY", undefined), undefined)
|
||||
assert.equal(pickCommandCodeApiKey("$COMMANDCODE_API_KEY", undefined), undefined)
|
||||
})
|
||||
|
||||
@@ -199,6 +212,12 @@ describe("textContent()", () => {
|
||||
)
|
||||
})
|
||||
|
||||
it("normalizes malformed string and object content", () => {
|
||||
assert.equal(textContent({ content: "raw result" }), "raw result")
|
||||
assert.equal(textContent({ content: { ok: true } }), '{"ok":true}')
|
||||
assert.equal(textContent({ content: null }), "")
|
||||
})
|
||||
|
||||
it("handles empty or missing content", () => {
|
||||
assert.equal(textContent({ content: [] }), "")
|
||||
assert.equal(textContent({}), "")
|
||||
@@ -531,6 +550,23 @@ describe("messagesToCC()", () => {
|
||||
assert.equal(objectAt(result, ["2", "content", "0", "output", "value"]), "hello\nworld")
|
||||
})
|
||||
|
||||
it("preserves malformed string tool results instead of sending empty output", () => {
|
||||
const result = messagesToCC([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "toolCall", id: "c1", name: "read", arguments: {} }],
|
||||
},
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "c1",
|
||||
toolName: "read",
|
||||
content: "raw result",
|
||||
},
|
||||
])
|
||||
|
||||
assert.equal(objectAt(result, ["1", "content", "0", "output", "value"]), "raw result")
|
||||
})
|
||||
|
||||
it("serializes image inputs in the current Command Code wire format", () => {
|
||||
assert.deepEqual(
|
||||
messagesToCC(
|
||||
@@ -632,7 +668,7 @@ describe("messagesToCC()", () => {
|
||||
])
|
||||
})
|
||||
|
||||
it("drops orphaned tool calls that have no matching tool result", () => {
|
||||
it("synthesizes missing results for orphaned tool calls", () => {
|
||||
const result = messagesToCC([
|
||||
{ role: "user", content: "edit a file" },
|
||||
{
|
||||
@@ -651,7 +687,12 @@ describe("messagesToCC()", () => {
|
||||
|
||||
assert.equal(objectAt(result, ["1", "role"]), "assistant")
|
||||
assert.equal(objectAt(result, ["1", "content", "0", "type"]), "text")
|
||||
assert.equal(objectAt(result, ["1", "content", "1"]), undefined)
|
||||
assert.equal(objectAt(result, ["1", "content", "1", "type"]), "tool-call")
|
||||
assert.equal(objectAt(result, ["2", "role"]), "tool")
|
||||
assert.match(
|
||||
String(objectAt(result, ["2", "content", "0", "output", "value"])),
|
||||
/did not complete/,
|
||||
)
|
||||
})
|
||||
|
||||
it("handles empty conversations", () => {
|
||||
|
||||
@@ -68,7 +68,7 @@ describe("commandcode-quota command", () => {
|
||||
})
|
||||
|
||||
assert.ok(pi.handler)
|
||||
const ctx = context("$COMMANDCODE_API_KEY")
|
||||
const ctx = context("$COMMAND_CODE_API_KEY")
|
||||
await pi.handler("", ctx.value)
|
||||
assert.equal(ctx.waited(), true)
|
||||
assert.equal(requestKey, "fallback-key")
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* 3. Can complete a simple prompt (requires Command Code auth)
|
||||
*
|
||||
* Run with: node tests/test-smoke.mjs
|
||||
* Requires: pi on PATH plus COMMANDCODE_API_KEY or live pi auth files.
|
||||
* Requires: pi on PATH plus COMMAND_CODE_API_KEY (or legacy COMMANDCODE_API_KEY) or live pi auth files.
|
||||
*/
|
||||
|
||||
import { spawn } from "node:child_process"
|
||||
@@ -48,6 +48,7 @@ const RPC_QUERY_TIMEOUT = 60_000
|
||||
|
||||
function hasCommandCodeAuth() {
|
||||
return (
|
||||
!!process.env.COMMAND_CODE_API_KEY ||
|
||||
!!process.env.COMMANDCODE_API_KEY ||
|
||||
existsSync(join(homedir(), ".commandcode", "auth.json")) ||
|
||||
existsSync(join(homedir(), ".pi", "agent", "auth.json"))
|
||||
|
||||
+119
-2
@@ -77,6 +77,23 @@ describe("streamCommandCode — auth", () => {
|
||||
)
|
||||
})
|
||||
|
||||
it("accepts the official CLI API key environment variable", async () => {
|
||||
server.mockResponse({
|
||||
type: "success",
|
||||
events: [JSON.stringify({ type: "finish", finishReason: "stop" })],
|
||||
})
|
||||
const { streamCommandCode } = createTestDeps({
|
||||
apiBase: server.baseUrl(),
|
||||
env: { COMMAND_CODE_API_KEY: "official-env-key" },
|
||||
})
|
||||
|
||||
await collectEvents(
|
||||
streamCommandCode(makeModel(), makeContext(), { apiKey: "$COMMAND_CODE_API_KEY" }),
|
||||
)
|
||||
|
||||
assert.equal(server.lastRequestHeaders().authorization, "Bearer official-env-key")
|
||||
})
|
||||
|
||||
it("uses options.apiKey in the Authorization header", async () => {
|
||||
server.mockResponse({
|
||||
type: "success",
|
||||
@@ -555,7 +572,7 @@ describe("streamCommandCode — request serialization", () => {
|
||||
assert.equal(objectAt(body, ["params", "stream"]), true)
|
||||
assert.equal(objectAt(body, ["params", "max_tokens"]), 500)
|
||||
assert.equal(objectAt(body, ["params", "reasoning_effort"]), undefined)
|
||||
assert.equal(objectAt(body, ["params", "temperature"]), 0.3)
|
||||
assert.equal(objectAt(body, ["params", "temperature"]), undefined)
|
||||
assert.equal(objectAt(body, ["params", "system"]), "You are a test assistant.")
|
||||
assert.equal(objectAt(body, ["memory"]), null)
|
||||
assert.equal(objectAt(body, ["taste"]), null)
|
||||
@@ -573,10 +590,53 @@ describe("streamCommandCode — request serialization", () => {
|
||||
assert.equal(headers["x-command-code-version"], COMMAND_CODE_CLI_VERSION)
|
||||
assert.equal(headers["x-project-slug"], "repo")
|
||||
assert.equal(headers["x-taste-learning"], "true")
|
||||
assert.equal(headers["x-co-flag"], "false")
|
||||
assert.equal(headers["user-agent"], "cli")
|
||||
assert.equal(headers["x-co-flag"], undefined)
|
||||
assert.equal(headers["x-session-id"], undefined)
|
||||
})
|
||||
|
||||
it("forwards explicit temperature and stable session metadata", async () => {
|
||||
server.mockResponse({
|
||||
type: "success",
|
||||
events: [JSON.stringify({ type: "finish", finishReason: "stop" })],
|
||||
})
|
||||
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
|
||||
|
||||
await collectEvents(
|
||||
streamCommandCode(makeModel(), makeContext(), {
|
||||
apiKey: "mock-key",
|
||||
temperature: 0.7,
|
||||
sessionId: "11111111-1111-4111-8111-111111111111",
|
||||
}),
|
||||
)
|
||||
|
||||
const body = server.lastRequestBody()
|
||||
assert.equal(objectAt(body, ["params", "temperature"]), 0.7)
|
||||
assert.equal(objectAt(body, ["threadId"]), "11111111-1111-4111-8111-111111111111")
|
||||
assert.equal(
|
||||
server.lastRequestHeaders()["x-session-id"],
|
||||
"11111111-1111-4111-8111-111111111111",
|
||||
)
|
||||
})
|
||||
|
||||
it("omits non-UUID session ids from the generate thread id", async () => {
|
||||
server.mockResponse({
|
||||
type: "success",
|
||||
events: [JSON.stringify({ type: "finish", finishReason: "stop" })],
|
||||
})
|
||||
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
|
||||
|
||||
await collectEvents(
|
||||
streamCommandCode(makeModel(), makeContext(), {
|
||||
apiKey: "mock-key",
|
||||
sessionId: "human-readable-session",
|
||||
}),
|
||||
)
|
||||
|
||||
assert.equal(objectAt(server.lastRequestBody(), ["threadId"]), undefined)
|
||||
assert.equal(server.lastRequestHeaders()["x-session-id"], "human-readable-session")
|
||||
})
|
||||
|
||||
it("accepts the legacy OMP nested reasoning map", async () => {
|
||||
server.mockResponse({
|
||||
type: "success",
|
||||
@@ -819,6 +879,63 @@ describe("streamCommandCode — upstream errors and malformed streams", () => {
|
||||
assert.equal(error.error.errorMessage, "provider failed")
|
||||
})
|
||||
|
||||
it("rejects a truncated stream without a finish event", async () => {
|
||||
server.mockResponse({
|
||||
type: "success",
|
||||
events: [JSON.stringify({ type: "text-delta", text: "truncated" })],
|
||||
})
|
||||
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
|
||||
|
||||
const events = await collectEvents(
|
||||
streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }),
|
||||
)
|
||||
|
||||
const error = events.at(-1)
|
||||
assert.equal(error?.type, "error")
|
||||
if (error?.type !== "error") throw new Error("expected error")
|
||||
assert.match(error.error.errorMessage ?? "", /no finish event/i)
|
||||
})
|
||||
|
||||
it("maps an upstream abort event to an aborted request", async () => {
|
||||
server.mockResponse({
|
||||
type: "success",
|
||||
events: [JSON.stringify({ type: "abort" })],
|
||||
})
|
||||
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
|
||||
|
||||
const events = await collectEvents(
|
||||
streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }),
|
||||
)
|
||||
|
||||
const error = events.at(-1)
|
||||
assert.equal(error?.type, "error")
|
||||
if (error?.type !== "error") throw new Error("expected error")
|
||||
assert.equal(error.reason, "aborted")
|
||||
})
|
||||
|
||||
it("rejects terminal upstream network failure reasons", async () => {
|
||||
server.mockResponse({
|
||||
type: "success",
|
||||
events: [
|
||||
JSON.stringify({
|
||||
type: "finish",
|
||||
finishReason: "stop",
|
||||
rawFinishReason: "upstream_error",
|
||||
}),
|
||||
],
|
||||
})
|
||||
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
|
||||
|
||||
const events = await collectEvents(
|
||||
streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }),
|
||||
)
|
||||
|
||||
const error = events.at(-1)
|
||||
assert.equal(error?.type, "error")
|
||||
if (error?.type !== "error") throw new Error("expected error")
|
||||
assert.match(error.error.errorMessage ?? "", /upstream connection failed/i)
|
||||
})
|
||||
|
||||
it("handles SSE lines, malformed lines, split chunks, and final line without newline", async () => {
|
||||
const textEvent = `data: ${JSON.stringify({ type: "text-delta", text: "split" })}\n`
|
||||
const finishEvent = JSON.stringify({
|
||||
|
||||
Reference in New Issue
Block a user