Compare commits

..

2 Commits

6 changed files with 52 additions and 47 deletions

View File

@ -45,7 +45,7 @@ src/transform.ts LanguageModelV3CallOptions -> /alpha/generate envelope (return
src/events.ts Async iterator over the NDJSON/SSE response body.
src/usage.ts finish event -> LanguageModelV3Usage; finish-reason unification.
src/redact.ts Credential scrubbing for error surfaces.
src/log.ts Opt-in tracing (COMMANDCODE_DEBUG) to a log file.
src/log.ts Opt-in tracing (toggle file) to a log file.
src/toggles.ts Shared toggle file (~/.config/opencode/commandcode-toggles.json).
src/quota.ts Live 5-hour/weekly/monthly quota from the CommandCode alpha billing API.
src/tui.tsx opencode TUI plugin: /cc-zdr, /cc-debug, /cc-status, /cc-usage + sidebar.
@ -159,9 +159,9 @@ These are load-bearing. Breaking one causes silent failures in opencode.
the same `namespace: "palette"` commands the palette lists (both filter out `hidden: true`).
A slash-only entry is not expressible; registering `/cc-*` also adds them to `Ctrl+P`.
15. **Toggles are resolved per request, never at load.** `zdr` and `debug` are read in
`model.ts`/`log.ts` on every call from the toggle file / env, so `/cc-*` changes take effect
`model.ts`/`log.ts` on every call from the toggle file, so `/cc-*` changes take effect
without restarting opencode. Precedence for `zdr`: `providerOptions.commandcode.zdr` >
`x-cmd-zdr` header > toggle file > `COMMANDCODE_ZDR`. Do not reintroduce load-time consts.
`x-cmd-zdr` header > toggle file. Do not reintroduce load-time consts.
16. **Quota is live from the alpha billing API, not `server.py`.** `src/quota.ts` reads
`/alpha/whoami`, `/alpha/billing/credits`, `/alpha/billing/subscriptions`, and
`/alpha/usage/summary` (the same endpoints the `cmd` CLI `/usage` uses). `server.py` predates

View File

@ -146,22 +146,33 @@ global config (`baseURL`/`ccVersion` are read from there too).
`options.headers` (with a leading `Bearer ` stripped). If neither is present, requests are sent
unauthenticated and CommandCode will reject them.
## Environment variables
All environment variables read by the provider, its scripts, and its TUI plugin. String values in
`options` also accept `{env:NAME}`, which opencode/the TUI expand generically (not a fixed set).
CommandCode-specific:
| Name | Purpose | Default |
| --- | --- | --- |
| `COMMANDCODE_API_KEY` | CommandCode API key for `smoke`/`quota`/`sync-models` and the TUI's key resolution; referenced from config as `{env:COMMANDCODE_API_KEY}`. | — |
| `COMMANDCODE_TOGGLES_FILE` | Overrides the shared toggle-file path. | `$XDG_CONFIG_HOME/opencode/commandcode-toggles.json` |
| `COMMANDCODE_QUOTA_INTERVAL_MS` | Sidebar fallback quota refresh interval in ms. | `300000` (floor `120000`) |
Standard:
| Name | Purpose | Default |
| --- | --- | --- |
| `XDG_CONFIG_HOME` | Base config dir for the toggle file. | `~/.config` |
| `NO_COLOR` | Disables ANSI colors in the `sync-models` picker (any value). | unset |
| `TERM` | `dumb` disables ANSI colors in the `sync-models` picker. | — |
## Debug tracing
Set `COMMANDCODE_DEBUG=1` to write a trace of every request and stream event to a log file.
Silent (and no file is created) unless enabled. Every line is passed through `redact()` so
credentials never reach disk. It can also be flipped at runtime with `/cc-debug` (see
[Runtime toggles](#runtime-toggles)).
```powershell
$env:COMMANDCODE_DEBUG = "1"
opencode run "Reply with exactly: pong" -m commandcode/deepseek/deepseek-v4.1-flash
```
| Env var | Default | Description |
| --- | --- | --- |
| `COMMANDCODE_DEBUG` | unset | `1`/`true`/`yes` enables tracing. |
| `COMMANDCODE_DEBUG_FILE` | `<os tempdir>/commandcode-debug.log` | Where the trace is appended. |
Flip the `debug` key in the shared toggle file (via `/cc-debug`) to write a trace of every
request and stream event to a log file. Silent (and no file is created) unless enabled; the log
path is `<os tempdir>/commandcode-debug.log`. Every line is passed through `redact()` so
credentials never reach disk.
The log captures the request (model id, body byte length), each HTTP attempt (status, retry
wait, elapsed ms), every stream event payload, and the terminal finish reason + usage. It is
@ -170,8 +181,8 @@ useful for diagnosing model selection, retry, tool-call, and finish-reason issue
## ZDR header toggle
The `x-cmd-zdr: 1` request header is **omitted by default** because it is rejected by some
models. It can be enabled with the `COMMANDCODE_ZDR` environment variable, the `/cc-zdr` slash
command, or `providerOptions.commandcode.zdr`. The value is resolved per request, so toggling it
models. It can be enabled with the `/cc-zdr` slash command or
`providerOptions.commandcode.zdr`. The value is resolved per request, so toggling it
does not require restarting opencode.
Precedence, highest first:
@ -179,7 +190,6 @@ Precedence, highest first:
1. `providerOptions.commandcode.zdr` (`true`/`false`), e.g. a model variant or agent option.
2. An explicit `x-cmd-zdr` header in `opencode.json` `options.headers` or the call's `headers`.
3. The toggle file (`zdr` key — see [Runtime toggles](#runtime-toggles)).
4. `COMMANDCODE_ZDR=1|true|yes` in the environment.
With tracing enabled, the trace logs a `zdr on|off` line per request so the toggle state is
observable.
@ -189,7 +199,7 @@ observable.
`/cc-zdr` and `/cc-debug` flip the `zdr` and `debug` values in a small JSON file the provider
reads on every request, so both settings change without restarting opencode. `/cc-status` shows
the current state. The file is `~/.config/opencode/commandcode-toggles.json` (overridable with
`COMMANDCODE_TOGGLES_FILE`); the environment variables above are used when a key is absent.
`COMMANDCODE_TOGGLES_FILE` — see [Environment variables](#environment-variables)).
These commands come from a small TUI plugin shipped in this package (`src/tui.tsx`). It is loaded
from **source** — opencode compiles the TSX and provides the Solid runtime itself — so there is no
@ -244,7 +254,8 @@ The sidebar refreshes after each completed turn (`session.idle`, plus `session.s
`session.updated`, and `message.updated` as fallbacks since `session.idle` is a server-plugin
event that may never reach the TUI bus — all debounced) and when the active session changes,
but never more than once every 2 minutes; every 5 minutes as a fallback
(`COMMANDCODE_QUOTA_INTERVAL_MS` overrides the fallback interval) and the
(`COMMANDCODE_QUOTA_INTERVAL_MS` overrides the fallback interval — see
[Environment variables](#environment-variables)) and the
countdown ticks every 30 seconds. Key resolution and fetch failures are redacted and shown
inline; the panel never stays on `loading…` and never blocks the provider. Extra trace lines
(`tui-quota`) are appended to the debug log only when debug tracing is on.
@ -314,7 +325,7 @@ src/transform.ts LanguageModelV3CallOptions -> /alpha/generate envelope (JSON s
src/events.ts NDJSON/SSE line iterator over the upstream response body.
src/usage.ts finish event -> V3 usage; finish-reason unification.
src/redact.ts Credential scrubbing for error surfaces.
src/log.ts Opt-in tracing (COMMANDCODE_DEBUG) to a log file.
src/log.ts Opt-in tracing (toggle file) to a log file.
src/toggles.ts Shared toggle file read by the provider and written by the TUI plugin.
src/quota.ts Live 5-hour/weekly/monthly quota from the alpha billing endpoints.
src/tui.tsx opencode TUI plugin: /cc-zdr, /cc-debug, /cc-status, /cc-usage + sidebar panel.
@ -380,7 +391,7 @@ opencode run "Use the glob tool to list *.mjs and report the filenames." -m comm
| `stream ended ... no finish` / truncated output | Upstream closed the connection early. The provider emits a synthetic `finish`, but the response is incomplete; retry the turn. |
| Images are ignored by the model | The selected model is not vision-capable. Mark it with `"attachment": true` and `modalities.input: ["text","image"]` in `models`, and pick a vision model id. |
| Upstream 400 about tool calls | A `tool-call` or `tool-result` without a matching pair slipped through. Pairing is enforced in `src/transform.ts`; report a repro if it still occurs. |
| Need to see what the provider sends/receives | Set `COMMANDCODE_DEBUG=1` and read the appended log file (see [Debug tracing](#debug-tracing)). |
| Need to see what the provider sends/receives | Flip `/cc-debug` and read the appended log file (see [Debug tracing](#debug-tracing)). |
| `tool_choice` seemingly ignored | Expected for `required`; upstream cannot force a call. `none` and named-tool are emulated via the tool list. |
| Config change had no effect | opencode reads config once at startup. Restart it. |
| `/cc-*` commands and the sidebar panel are missing | TUI plugin not registered: add a direct `file://` URL to `src/tui.tsx` in `tui.json` (see [Runtime toggles](#runtime-toggles)). A repo-root or `src/tui.tsx`-less entry does not load. |

View File

@ -1,4 +1,4 @@
// Opt-in tracing to a log file. Gated on COMMANDCODE_DEBUG=1 or the shared toggle file;
// Opt-in tracing to a log file. Gated on the shared toggle file (debug key);
// silent (and no file created) by default. Uses only Node built-ins so the built output
// stays dependency-free.
@ -6,9 +6,9 @@ import { appendFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { redact } from "./redact.js";
import { debugFromEnvOrFile } from "./toggles.js";
import { debugEnabled } from "./toggles.js";
const FILE = process.env["COMMANDCODE_DEBUG_FILE"] ?? join(tmpdir(), "commandcode-debug.log");
const FILE = join(tmpdir(), "commandcode-debug.log");
function serialize(value: unknown): string {
if (typeof value === "string") return value;
@ -34,14 +34,14 @@ function emit(scope: string, args: unknown[]): void {
}
}
/** Effective default tracing state (env var or toggle file). Re-read on every call. */
/** Effective default tracing state (toggle file). Re-read on every call. */
export function isDebugEnabled(): boolean {
return debugFromEnvOrFile();
return debugEnabled();
}
/** Write a trace line (appended) if tracing is enabled. Values are redacted before write. */
export function debug(scope: string, ...args: unknown[]): void {
if (!debugFromEnvOrFile()) return;
if (!debugEnabled()) return;
emit(scope, args);
}

View File

@ -21,12 +21,12 @@ import {
import { iterateEvents } from "./events.js";
import { debugWhen, isDebugEnabled } from "./log.js";
import { redact } from "./redact.js";
import { zdrFromEnvOrFile } from "./toggles.js";
import { zdrEnabled } from "./toggles.js";
import { transform } from "./transform.js";
import { finishReasonFrom, usageFromFinish, costFromFinishStep, costFromProviderMetadata, type FinishEvent, type FinishStepEvent, type ProviderMetadataEvent } from "./usage.js";
// x-cmd-zdr breaks some models; off by default (server.py never sends it).
// Precedence: providerOptions.commandcode.zdr > x-cmd-zdr header > toggle file > COMMANDCODE_ZDR.
// Precedence: providerOptions.commandcode.zdr > x-cmd-zdr header > toggle file.
// "undefined" means the header is omitted entirely.
function providerScoped(options: LanguageModelV3CallOptions): Record<string, unknown> | undefined {
return options.providerOptions?.["commandcode"] as Record<string, unknown> | undefined;
@ -44,7 +44,7 @@ function resolveZdr(
if (typeof scoped === "boolean") return scoped ? "1" : "0";
const header = headerValue({ ...providerHeaders, ...(options.headers ?? {}) }, "x-cmd-zdr");
if (header !== undefined) return truthyHeader(header) ? "1" : "0";
return zdrFromEnvOrFile() ? "1" : undefined;
return zdrEnabled() ? "1" : undefined;
}
function resolveDebug(options: LanguageModelV3CallOptions): boolean {

View File

@ -45,17 +45,11 @@ export function toggle(name: ToggleName): Toggles {
return writeToggles({ [name]: current[name] !== true });
}
export function envFlag(name: string): boolean {
return /^(1|true|yes)$/i.test(process.env[name] ?? "");
/** Toggle state from the shared flag file; an absent key means off. */
export function zdrEnabled(): boolean {
return readToggles().zdr === true;
}
/** Flag file wins over the environment; unset file falls back to the env var. */
export function zdrFromEnvOrFile(): boolean {
const file = readToggles().zdr;
return typeof file === "boolean" ? file : envFlag("COMMANDCODE_ZDR");
}
export function debugFromEnvOrFile(): boolean {
const file = readToggles().debug;
return typeof file === "boolean" ? file : envFlag("COMMANDCODE_DEBUG");
export function debugEnabled(): boolean {
return readToggles().debug === true;
}

View File

@ -21,7 +21,7 @@ import { createSignal } from "solid-js";
import { fetchQuota, formatQuota, formatReset, percent, quotaBar, type QuotaResult, type QuotaWindow } from "./quota.js";
import { redact } from "./redact.js";
import { debugFromEnvOrFile, readToggles, toggle, type ToggleName, type Toggles } from "./toggles.js";
import { debugEnabled, readToggles, toggle, type ToggleName, type Toggles } from "./toggles.js";
type ToastVariant = "info" | "success" | "warning" | "error";
@ -86,8 +86,8 @@ function safeEnv(name: string): string | undefined {
/** Debug-only trace to the shared debug file. Silent unless debug is on; never logs the key. */
function trace(...args: unknown[]): void {
if (!debugFromEnvOrFile()) return;
const file = safeEnv("COMMANDCODE_DEBUG_FILE") ?? join(tmpdir(), "commandcode-debug.log");
if (!debugEnabled()) return;
const file = join(tmpdir(), "commandcode-debug.log");
const parts = args.map((value) => (typeof value === "string" ? value : safeJson(value)));
const line = `[commandcode] ${new Date().toISOString()} [tui-quota] ${parts.join(" ")}`;
try {