Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Breaking changes

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-1.0 breaking change → minor bump (0.8.0). Consider naming the target version here or in the PR so the release PR cannot accidentally ship it as a patch. (#163's RELEASE.md text says the same.)


- Require documented Provider API access. `403 upgrade_required` now returns a clear entitlement/migration error instead of automatically calling the internal `/alpha/generate` endpoint. Go-plan users must use the official CLI within its supported plan or an account with Provider API entitlement; signing in alone does not grant API access. Existing credentials and sessions are preserved. See the README migration section. The Go live-test profile is removed; mock Pi/OMP tests assert that no generation fallback request occurs.

## 0.7.8 - 2026-10-15

- Redact sensitive text in direct stream errors and normalized context-overflow messages before returning them to the host. This closes two error-reporting paths that did not use the shared redactor; it is defensive hardening, not a claim of a confirmed credential leak ([#155](https://github.com/patlux/pi-commandcode-provider/pull/155)).
Expand Down
3 changes: 1 addition & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,11 @@ Both commands accept additional pi arguments after `--`, for example `npm run pi
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 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.
Use `npm run test:e2e:live:all` with the GOAT and Provider file variables to run both entitled profiles sequentially. The Go profile is removed: a mock `403 upgrade_required` must produce the migration error without an internal generation request on both real hosts. 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.

### pi end-to-end

Expand Down
29 changes: 19 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ A custom provider for [pi](https://github.com/earendil-works/pi) that connects t

> **Disclaimer:** This is an unofficial, community-maintained integration. It is not affiliated with, endorsed by, or supported by Command Code. You need your own Command Code account, API key, and a plan with Provider API access. Command Code's terms, availability, and pricing apply.

## Breaking migration: Provider API access required

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README is what npm renders for the released version. "The next breaking release removes…" will be wrong the moment this is released, and the section sits above Install. Write it as the state of the code ("This extension requires Provider API access. 403 upgrade_required stops with …") and keep the unreleased/proposal status in CHANGELOG and the PR.


The next breaking release removes automatic `/alpha/generate` fallback. A `403 upgrade_required` now stops with an actionable error; it never retries your prompt against the internal CLI endpoint. The [official Provider API documentation](https://commandcode.ai/docs/provider) excludes API use from the Go plan. A working login or quota response does not establish API entitlement.

If you used the Go fallback:

1. Check the current documented plans and model access with Command Code. Use an account with Provider API entitlement, or use the official CLI within its supported plan.
2. After changing accounts, run `/login` again and reload the host. Existing sessions, credentials and model selections are not deleted or migrated by this change.
3. Retry a small request and check `/commandcode-status`. There is no switch to re-enable internal generation. Do not use alternate endpoint overrides to bypass plan restrictions.

GOAT/Provider accounts with API entitlement continue to use the same documented inference endpoints. Quota reporting is unchanged. This is a breaking-change proposal, not a claim that the version on npm already includes it.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things in this paragraph:

  • "This is a breaking-change proposal, not a claim that the version on npm already includes it." — same as above, this sentence must not land on main.
  • "Quota reporting is unchanged": /commandcode-quota and the usage provider still call /alpha/whoami, /alpha/billing/*, /alpha/usage/summary — the same undocumented surface Use Command Code Provider API Instead #5 complains about. The PR's principle ("documented Provider API only") is not applied there. If that is deliberate (read-only, no plan bypass), say so explicitly; if not, it belongs in the same breaking release rather than a second one.


## Install

```sh
Expand Down Expand Up @@ -98,7 +110,7 @@ A model whose catalog entry omits `supported_endpoints` keeps Chat Completions,

### 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 register a model-specific `thinkingLevelMap`, so pi and OMP expose only valid levels. For a few reasoning models the CLI catalog ships no effort levels although the endpoint accepts `reasoning_effort`; `src/commandcode-catalog-overrides.ts` adds a manual level set for those on top of the generated catalog (currently empty because upstream publishes every level). The catalog sync removes an override as soon as upstream publishes its own 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.
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 register a model-specific `thinkingLevelMap`, so pi and OMP expose only valid levels. For a few reasoning models the CLI catalog ships no effort levels although the endpoint accepts `reasoning_effort`; `src/commandcode-catalog-overrides.ts` adds a manual level set for those on top of the generated catalog (currently empty because upstream publishes every level). The catalog sync removes an override as soon as upstream publishes its own levels. Pi's native OpenAI- and Anthropic-compatible providers translate the selected level for Provider API accounts.

List Command Code models from the terminal:

Expand Down Expand Up @@ -155,11 +167,11 @@ The provider advertises image input only for models marked with the `image` inpu

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.

The legacy generate transport resolves image support from the host's `model.input` when the host supplies it, and falls back to the capability snapshot otherwise. Because the snapshot is generated from a single CLI release, a host that marks a model as `["text", "image"]` — for example through a `models.yml` or `models.json` model override — can send images on both transports before the catalog catches up. A host that narrows a catalogued vision model to text is likewise honored.
The native host adapter uses the registered model's input capabilities on the Provider API. A host model override can advertise image support before the catalog catches up, but cannot grant missing upstream model or plan access.

## Pricing display

The Command Code Provider API does not currently include prices in its model catalog. This extension therefore keeps a static table for models with known prices so pi and OMP can display estimated request costs. DeepSeek V4 uses time-dependent rates: the extension applies the documented 2x peak rates on weekdays from 01:00-04:00 and 06:00-10:00 UTC, and off-peak rates at all other times, including the entire weekend. The same request-time selection is used by the native Provider API path and the legacy generate fallback.
The Command Code Provider API does not currently include prices in its model catalog. This extension therefore keeps a static table for models with known prices so pi and OMP can display estimated request costs. DeepSeek V4 uses time-dependent rates: the extension applies the documented 2x peak rates on weekdays from 01:00-04:00 and 06:00-10:00 UTC, and off-peak rates at all other times, including the entire weekend. These rates are selected at request time on the Provider API path.

Models missing from that table display zero cost in pi. This does **not** mean that Command Code will bill the request at zero. The Command Code Usage page remains authoritative for each request. Check the current [Command Code pricing](https://commandcode.ai/docs/resources/pricing-limits) before relying on the displayed value.

Expand Down Expand Up @@ -209,26 +221,23 @@ Both commands accept additional pi arguments after `--`, for example `npm run pi

### Live transport tests

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:
Keep GOAT- and 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 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.
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. Both profiles must select `provider`; GOAT also completes a live vision request. Go entitlement denial and the absence of any internal generation request are covered by mock Pi/OMP tests, not paid live probes. The profile-specific `*_API_KEY` environment variables are also supported for CI secrets, but key files are preferred for local use.

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`. The GOAT vision phase defaults to GPT-5.6 Luna and can be overridden with `COMMANDCODE_E2E_GOAT_VISION_MODEL`. A successful live Anthropic `/provider/v1/messages` test requires a paid account whose plan includes the selected Claude model.
The Provider 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_GOAT_MODEL` or `COMMANDCODE_E2E_PROVIDER_MODEL`. The GOAT vision phase defaults to GPT-5.6 Luna and can be overridden with `COMMANDCODE_E2E_GOAT_VISION_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.

Expand Down
19 changes: 1 addition & 18 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ import { join } from "node:path"

import { getConfiguredApiKey } from "./src/api-key.ts"
import { pickCommandCodeApiKey, withResolvedCommandCodeApiKey } from "./src/converters.ts"
import { createStreamCommandCode } from "./src/core.ts"
import { calculateCommandCodeCost, commandCodeCostRatesAt } from "./src/cost.ts"
import { commandCodeCostRatesAt } from "./src/cost.ts"

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generate client stays in the shipped package: src/core.ts (836 lines), src/transcript.ts, the converter paths and their tests remain, and files: ["src/"] publishes them. For a breaking change whose rationale is "do not touch the internal endpoint", shipping an unreachable /alpha/generate client is contradictory, and it leaves ~1.3k lines that are tested but never run in production.

This is already a feat!; remove it here (plus tests/test-core*, converter/transcript tests, calculateCommandCodeCost if it becomes unused), or open the follow-up and make it a blocker for the 0.8.0 release.

import {
apiForModelId,
baseUrlForModel,
Expand All @@ -41,7 +40,6 @@ import { registerCommandCodeQuota } from "./src/quota-command.ts"
import { normalizeCommandCodeResponsesResponse } from "./src/responses-stream.ts"
import { createCommandCodeRuntime } from "./src/runtime.ts"
import { createCommandCodeUsageProvider, type UsageProvider } from "./src/usage.ts"
import { transcriptReadersFrom, withTranscriptPromptAndTools } from "./src/transcript.ts"
import { createCommandCodeTransportRouter } from "./src/transport.ts"

const COMMAND_CODE_API = "commandcode-custom"
Expand Down Expand Up @@ -204,17 +202,8 @@ export default async function (pi: ExtensionAPI) {
const modelsTimeoutMs = getModelsTimeoutMs()
const modelsCachePath =
process.env.COMMANDCODE_MODELS_CACHE ?? join(getAgentDir(), "commandcode-models.json")
const streamGenerate = createStreamCommandCode({
createStream: () => new AssistantMessageEventStream(),
calculateCost: calculateCommandCodeCost,
apiBase: legacyApiBase(apiBase),
})
const resolveStreamOptions = (options?: Parameters<typeof streamNativeProvider>[2]) =>
withResolvedCommandCodeApiKey(options, getConfiguredApiKey())
// The provider transport delegates to pi-ai, which reads the transcript
// itself. The generate transport builds its own request body and needs the
// flat prompt and tool fields that pi 0.86+ no longer passes.
const transcriptReaders = transcriptReadersFrom(piAiCompat)
// The host dispatches every Command Code model through one custom api id, so
// the wire each model answers on is resolved here from the catalog that
// `createProviderConfig` registered. Unknown ids fall back to the id rule.
Expand Down Expand Up @@ -252,12 +241,6 @@ export default async function (pi: ExtensionAPI) {
nativeOptions,
)
},
streamGenerate: (model, context, options) =>
streamGenerate(
model,
withTranscriptPromptAndTools(context, transcriptReaders),
resolveStreamOptions(options),
),
})

// pi dispatches the main chat through the registered provider, but sibling
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,9 @@
"test:release": "node --test tests/test-release.mjs",
"test:release-package": "node tests/test-release-package.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 goat",
"test:e2e:live:all": "node scripts/live-e2e-profile.mjs goat provider",
"test:cost": "tsx tests/test-cost.ts"
},
"pi": {
Expand Down
19 changes: 6 additions & 13 deletions scripts/live-e2e-profile.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,16 @@ const profiles = process.argv.slice(2)

if (
profiles.length === 0 ||
profiles.some((profile) => profile !== "go" && profile !== "goat" && profile !== "provider")
profiles.some((profile) => profile !== "goat" && profile !== "provider")
) {
console.error("Usage: node scripts/live-e2e-profile.mjs <go|goat|provider> [go|goat|provider]")
console.error(
"Usage: node scripts/live-e2e-profile.mjs <goat|provider> [goat|provider] (Go has no Provider API access)",
)
process.exit(2)
}

async function credentialFor(profile) {
const prefix =
profile === "go"
? "COMMANDCODE_E2E_GO"
: profile === "goat"
? "COMMANDCODE_E2E_GOAT"
: "COMMANDCODE_E2E_PROVIDER"
const prefix = profile === "goat" ? "COMMANDCODE_E2E_GOAT" : "COMMANDCODE_E2E_PROVIDER"
const direct = process.env[`${prefix}_API_KEY`]?.trim()
const file = process.env[`${prefix}_API_KEY_FILE`]

Expand All @@ -40,11 +37,7 @@ async function credentialFor(profile) {

function runProfile(profile, apiKey) {
const modelVariable =
profile === "go"
? "COMMANDCODE_E2E_GO_MODEL"
: profile === "goat"
? "COMMANDCODE_E2E_GOAT_MODEL"
: "COMMANDCODE_E2E_PROVIDER_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")
Expand Down
4 changes: 2 additions & 2 deletions src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ export interface CommandCodeRuntimeOptions<TProviderConfig> {
/** Cached catalog only; resolves to an empty list when no valid cache exists. */
loadCachedModels: () => Promise<readonly CommandCodeModel[]>
createProviderConfig: (models: readonly CommandCodeModel[]) => TProviderConfig
getTransport?: () => "unknown" | "provider" | "generate"
getTransport?: () => "unknown" | "provider"
getStreamWarning?: () => string | undefined
now?: () => number
logWarning?: (message: string) => void
}

export interface CommandCodeRuntimeStatus {
transport: "unknown" | "provider" | "generate"
transport: "unknown" | "provider"
source: LoadCommandCodeModelsResult["source"]
modelCount: number
lastSuccess?: number
Expand Down
Loading
Loading