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/events.ts Async iterator over the NDJSON/SSE response body.
src/usage.ts finish event -> LanguageModelV3Usage; finish-reason unification. src/usage.ts finish event -> LanguageModelV3Usage; finish-reason unification.
src/redact.ts Credential scrubbing for error surfaces. 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/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/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. 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`). 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`. 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 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` > 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 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/whoami`, `/alpha/billing/credits`, `/alpha/billing/subscriptions`, and
`/alpha/usage/summary` (the same endpoints the `cmd` CLI `/usage` uses). `server.py` predates `/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 `options.headers` (with a leading `Bearer ` stripped). If neither is present, requests are sent
unauthenticated and CommandCode will reject them. 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 ## Debug tracing
Set `COMMANDCODE_DEBUG=1` to write a trace of every request and stream event to a log file. Flip the `debug` key in the shared toggle file (via `/cc-debug`) to write a trace of every
Silent (and no file is created) unless enabled. Every line is passed through `redact()` so request and stream event to a log file. Silent (and no file is created) unless enabled; the log
credentials never reach disk. It can also be flipped at runtime with `/cc-debug` (see path is `<os tempdir>/commandcode-debug.log`. Every line is passed through `redact()` so
[Runtime toggles](#runtime-toggles)). credentials never reach disk.
```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. |
The log captures the request (model id, body byte length), each HTTP attempt (status, retry 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 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 ## ZDR header toggle
The `x-cmd-zdr: 1` request header is **omitted by default** because it is rejected by some 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 models. It can be enabled with the `/cc-zdr` slash command or
command, or `providerOptions.commandcode.zdr`. The value is resolved per request, so toggling it `providerOptions.commandcode.zdr`. The value is resolved per request, so toggling it
does not require restarting opencode. does not require restarting opencode.
Precedence, highest first: Precedence, highest first:
@ -179,7 +190,6 @@ Precedence, highest first:
1. `providerOptions.commandcode.zdr` (`true`/`false`), e.g. a model variant or agent option. 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`. 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)). 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 With tracing enabled, the trace logs a `zdr on|off` line per request so the toggle state is
observable. observable.
@ -189,7 +199,7 @@ observable.
`/cc-zdr` and `/cc-debug` flip the `zdr` and `debug` values in a small JSON file the provider `/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 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 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 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 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 `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, 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 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 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 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. (`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/events.ts NDJSON/SSE line iterator over the upstream response body.
src/usage.ts finish event -> V3 usage; finish-reason unification. src/usage.ts finish event -> V3 usage; finish-reason unification.
src/redact.ts Credential scrubbing for error surfaces. 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/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/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. 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. | | `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. | | 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. | | 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. | | `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. | | 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. | | `/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 // silent (and no file created) by default. Uses only Node built-ins so the built output
// stays dependency-free. // stays dependency-free.
@ -6,9 +6,9 @@ import { appendFileSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { redact } from "./redact.js"; 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 { function serialize(value: unknown): string {
if (typeof value === "string") return value; 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 { export function isDebugEnabled(): boolean {
return debugFromEnvOrFile(); return debugEnabled();
} }
/** Write a trace line (appended) if tracing is enabled. Values are redacted before write. */ /** Write a trace line (appended) if tracing is enabled. Values are redacted before write. */
export function debug(scope: string, ...args: unknown[]): void { export function debug(scope: string, ...args: unknown[]): void {
if (!debugFromEnvOrFile()) return; if (!debugEnabled()) return;
emit(scope, args); emit(scope, args);
} }

View File

@ -21,12 +21,12 @@ import {
import { iterateEvents } from "./events.js"; import { iterateEvents } from "./events.js";
import { debugWhen, isDebugEnabled } from "./log.js"; import { debugWhen, isDebugEnabled } from "./log.js";
import { redact } from "./redact.js"; import { redact } from "./redact.js";
import { zdrFromEnvOrFile } from "./toggles.js"; import { zdrEnabled } from "./toggles.js";
import { transform } from "./transform.js"; import { transform } from "./transform.js";
import { finishReasonFrom, usageFromFinish, costFromFinishStep, costFromProviderMetadata, type FinishEvent, type FinishStepEvent, type ProviderMetadataEvent } from "./usage.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). // 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. // "undefined" means the header is omitted entirely.
function providerScoped(options: LanguageModelV3CallOptions): Record<string, unknown> | undefined { function providerScoped(options: LanguageModelV3CallOptions): Record<string, unknown> | undefined {
return options.providerOptions?.["commandcode"] as 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"; if (typeof scoped === "boolean") return scoped ? "1" : "0";
const header = headerValue({ ...providerHeaders, ...(options.headers ?? {}) }, "x-cmd-zdr"); const header = headerValue({ ...providerHeaders, ...(options.headers ?? {}) }, "x-cmd-zdr");
if (header !== undefined) return truthyHeader(header) ? "1" : "0"; if (header !== undefined) return truthyHeader(header) ? "1" : "0";
return zdrFromEnvOrFile() ? "1" : undefined; return zdrEnabled() ? "1" : undefined;
} }
function resolveDebug(options: LanguageModelV3CallOptions): boolean { function resolveDebug(options: LanguageModelV3CallOptions): boolean {

View File

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

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 { fetchQuota, formatQuota, formatReset, percent, quotaBar, type QuotaResult, type QuotaWindow } from "./quota.js";
import { redact } from "./redact.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"; 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. */ /** Debug-only trace to the shared debug file. Silent unless debug is on; never logs the key. */
function trace(...args: unknown[]): void { function trace(...args: unknown[]): void {
if (!debugFromEnvOrFile()) return; if (!debugEnabled()) return;
const file = safeEnv("COMMANDCODE_DEBUG_FILE") ?? join(tmpdir(), "commandcode-debug.log"); const file = join(tmpdir(), "commandcode-debug.log");
const parts = args.map((value) => (typeof value === "string" ? value : safeJson(value))); const parts = args.map((value) => (typeof value === "string" ? value : safeJson(value)));
const line = `[commandcode] ${new Date().toISOString()} [tui-quota] ${parts.join(" ")}`; const line = `[commandcode] ${new Date().toISOString()} [tui-quota] ${parts.join(" ")}`;
try { try {