Skip to content
Merged
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) - SemVer.

## [Unreleased]

## [1.1.0] - 2026-10-31

### Added
- Multi-key rotation (opt-in). With two or more keys from
`COMMANDCODE_API_KEYS`, `COMMANDCODE_KEYS_FILE` or
`~/.config/commandcode/keys.txt`, the plugin picks the first key that is not
cooling down, so a dead account does not end the turn. `402` and `401`/`403`
block a key for 24h, `429` honours `Retry-After` up to 6h (5m by default), and
`5xx` never blames the key. The soonest recovery wins when every key is
cooling down. State is labels-only (`commandcode-keys-state.json`, 0600) and
every cooldown is logged to `commandcode-rotation.log` without secrets.
Single-key mode keeps the previous behaviour: the plugin never overrides an
`Authorization` header the host already set.

## [1.0.7] - 2026-10-31

### Added
Expand Down
31 changes: 31 additions & 0 deletions README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,32 @@ substring para credenciales: filtraria la key a hosts parecidos como
`/provider/v1/chat/completions` en su URL. Una cabecera `authorization` existente
jamas se sobrescribe.

### Rotacion multi-key (opt-in)

Se pueden agrupar varias keys de Command Code para que una cuenta muerta no corte
el turno. Dos o mas keys activan la rotacion; con una sola se mantiene el camino
de key unica sin cambios.

```sh
# inline
export COMMANDCODE_API_KEYS="key_one,key_two"

# o archivo, una por linea (`label=key` o key pelada, comentarios con `#`)
export COMMANDCODE_KEYS_FILE="$HOME/.config/commandcode/keys.txt"
```

Los cooldowns se registran por key segun la respuesta que culpa a la credencial:
`402` (creditos insuficientes) y `401`/`403` bloquean 24h, `429` respeta
`Retry-After` hasta 6h (5m por defecto), y `5xx` nunca culpa a la key. Si todas
estan en cooldown gana la que se recupera antes. El estado guarda solo etiquetas
(`<state>/commandcode-keys-state.json`, modo 0600) y cada cooldown se registra en
`<state>/commandcode-rotation.log`.

En modo rotacion el plugin es dueno de la cabecera `Authorization` para
`api.commandcode.ai`, porque la key que inyecto el host puede ser justo la que
devolvio `402`. En modo key unica se mantiene la regla anterior: inyectar solo si
el request no trae cabecera.

### Estabilidad de sesion

Cuatro hooks independientes, todos fail-open (un error del plugin nunca puede romper
Expand Down Expand Up @@ -218,6 +244,9 @@ Cada ajuste es una variable de entorno con default seguro. Referencia completa:
| --- | --- | --- |
| `COMMAND_CODE_API_KEY` | - | Resolucion de la key, maxima prioridad |
| `COMMANDCODE_KEY_FILE` | - | Ruta absoluta a un archivo con la key |
| `COMMANDCODE_API_KEYS` | - | Pool de keys; dos o mas activan rotacion |
| `COMMANDCODE_KEYS_FILE` | - | Archivo pool (`label=key` o key pelada por linea) |
| `CMD_ZDR` | - | `1` para zero data retention (`x-cmd-zdr: 1`) |
| `COMMANDCODE_ALLOWED_HOSTS` | - | Hosts extra separados por coma autorizados a recibir la key |
| `CC_NO_CONNECTION_CLOSE` | `0` | Poner en `1` para dejar de inyectar `Connection: close` |
| `COMMANDCODE_STATE_DIR` | `$XDG_STATE_HOME/opencode` | Donde viven los logs y la cache de modelos |
Expand All @@ -236,6 +265,8 @@ Kill switch en runtime para la higiene de conexion: crea un archivo centinela ll
| `<state>/commandcode-sanitizer.log` | Cada reparacion de payload en vuelo, con las correcciones aplicadas |
| `<state>/commandcode-watchdog.log` | Streams SSE abortados por inactividad |
| `<state>/commandcode-retries.log` | Reintentos de transporte de Command Code: sesion, agente, proveedor, modelo, tipo de error, status HTTP, numero de intento, delay del backoff y mensaje |
| `<state>/commandcode-rotation.log` | Cooldowns de keys: etiqueta, status HTTP, motivo y hora de reset |
| `<state>/commandcode-keys-state.json` | Estado de cooldown por etiqueta (solo etiquetas, 0600) |
| `<state>/commandcode-models-cache.json` | Ultimo catalogo de modelos bueno (TTL 6h) |

`<state>` por defecto es `~/.local/state/opencode`.
Expand Down
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,31 @@ it would leak the key to lookalike hosts such as `api.commandcode.ai.evil.com`
or to any server that merely carries `/provider/v1/chat/completions` in its URL.
An existing `authorization` header is never overwritten.

### Multi-key rotation (opt-in)

Several Command Code keys can be pooled so a dead account does not end the turn.
Two or more keys enable rotation; one key keeps the single-key path untouched.

```sh
# inline
export COMMANDCODE_API_KEYS="key_one,key_two"

# or a file, one per line (`label=key` or bare key, `#` comments)
export COMMANDCODE_KEYS_FILE="$HOME/.config/commandcode/keys.txt"
```

Cooldowns are recorded per key from the response that blamed the credential:
`402` (insufficient credits) and `401`/`403` block for 24h, `429` honours
`Retry-After` up to 6h (5m by default), and `5xx` never blames the key. When all
keys are cooling down the soonest recovery wins. State is labels-only
(`<state>/commandcode-keys-state.json`, mode 0600) and every cooldown is logged
to `<state>/commandcode-rotation.log`.

In rotation mode the plugin owns the `Authorization` header for
`api.commandcode.ai`, because the key that the host config injected may be the
one that just returned `402`. Single-key mode keeps the previous rule: inject
only when the request has no header.

### Session stability

Four independent hooks, all fail-open (an error in the plugin can never break a
Expand Down Expand Up @@ -212,6 +237,9 @@ Every setting is an environment variable with a safe default. Full reference:
| --- | --- | --- |
| `COMMAND_CODE_API_KEY` | - | Key resolution, highest priority |
| `COMMANDCODE_KEY_FILE` | - | Absolute path to a key file |
| `COMMANDCODE_API_KEYS` | - | Pool of keys; two or more enable rotation |
| `COMMANDCODE_KEYS_FILE` | - | Pool file (`label=key` or bare key per line) |
| `CMD_ZDR` | - | Set to `1` for zero data retention (`x-cmd-zdr: 1`) |
| `COMMANDCODE_ALLOWED_HOSTS` | - | Comma-separated extra hosts allowed to receive the key |
| `CC_NO_CONNECTION_CLOSE` | `0` | Set to `1` to stop injecting `Connection: close` |
| `COMMANDCODE_STATE_DIR` | `$XDG_STATE_HOME/opencode` | Where logs and the model cache live |
Expand All @@ -230,6 +258,8 @@ Runtime kill switch for connection hygiene: create a sentinel file named
| `<state>/commandcode-sanitizer.log` | Every in-flight payload repair, with the fixes applied |
| `<state>/commandcode-watchdog.log` | Aborted stalled SSE streams |
| `<state>/commandcode-retries.log` | Command Code transport retries: session, agent, provider, model, error type, HTTP status, attempt number, backoff delay and message |
| `<state>/commandcode-rotation.log` | Key cooldowns: label, HTTP status, reason and reset time |
| `<state>/commandcode-keys-state.json` | Per-key cooldown state (labels only, 0600) |
| `<state>/commandcode-models-cache.json` | Last known good model catalog (6h TTL) |

`<state>` defaults to `~/.local/state/opencode`.
Expand Down
31 changes: 31 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ hook.
| --- | --- | --- |
| `COMMAND_CODE_API_KEY` | unset | Highest priority. Used as-is (trimmed, surrounding quotes stripped). |
| `COMMANDCODE_KEY_FILE` | unset | Absolute path to a file containing only the key. Ignored when the file is missing or empty, so the cascade keeps going. |
| `COMMANDCODE_API_KEYS` | unset | Comma or whitespace separated pool. Two or more keys enable rotation. |
| `COMMANDCODE_KEYS_FILE` | unset | Pool file, one `label=key` or bare key per line. `#` starts a comment. |
| `CMD_ZDR` | unset | Set to `1` to send `x-cmd-zdr: 1` (zero data retention). |
| `XDG_CONFIG_HOME` | `~/.config` | Base for the third resolution step: `$XDG_CONFIG_HOME/commandcode/key.txt`. |

### Resolution order
Expand Down Expand Up @@ -43,6 +46,32 @@ Code traffic. Every host added here can read the API key.
COMMANDCODE_ALLOWED_HOSTS=gateway.internal,proxy.lan
```

### Key rotation

A pool of two or more keys activates rotation. With one key (or none) the plugin
keeps the single-key behaviour and never touches an `Authorization` header the
host already set.

| Source | Format |
| --- | --- |
| `COMMANDCODE_API_KEYS` | `key1,key2` or whitespace separated |
| `COMMANDCODE_KEYS_FILE` | one `label=key` or bare key per line, `#` comments |
| `~/.config/commandcode/keys.txt` | same format, used when neither variable is set |

Cooldown recorded per key after a response that blames the credential:

| Status | Cooldown |
| --- | --- |
| `402` insufficient credits | 24h |
| `401` / `403` invalid or not entitled | 24h |
| `429` rate limited | `Retry-After` when present and at most 6h, else 5m |
| `5xx` | none (not the key's fault) |

When every key is cooling down the one that recovers first is used, so the
request still reaches upstream. State lives in
`<state>/commandcode-keys-state.json` (mode 0600, labels only, never keys) and
each cooldown is appended to `<state>/commandcode-rotation.log`.

## Transport

| Variable | Default | Meaning |
Expand Down Expand Up @@ -70,6 +99,8 @@ Files written under the state directory:
| `commandcode-sanitizer.log` | append | JSON lines, one per repaired payload |
| `commandcode-watchdog.log` | append | JSON lines, one per aborted stream |
| `commandcode-retries.log` | append | JSON lines, Command Code retries only |
| `commandcode-rotation.log` | append | JSON lines, one per key cooldown (label, status, reason, reset time) |
| `commandcode-keys-state.json` | overwrite | Cooldown state per key label (0600, no secrets) |
| `commandcode-models-cache.json` | overwrite | `{ at, models[] }`, 6h TTL |

Logs are append-only and never rotated by the plugin. At the observed event rate
Expand Down
26 changes: 26 additions & 0 deletions docs/es/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ todos los hooks.
| --- | --- | --- |
| `COMMAND_CODE_API_KEY` | sin definir | Maxima prioridad. Se usa tal cual (recortada y sin comillas envolventes). |
| `COMMANDCODE_KEY_FILE` | sin definir | Ruta absoluta a un archivo que contenga solo la key. Se ignora si el archivo falta o esta vacio, para que la cascada continue. |
| `COMMANDCODE_API_KEYS` | sin definir | Pool separado por comas o espacios. Dos o mas keys activan la rotacion. |
| `COMMANDCODE_KEYS_FILE` | sin definir | Archivo pool, una `label=key` o key pelada por linea. `#` inicia comentario. |
| `CMD_ZDR` | sin definir | `1` envia `x-cmd-zdr: 1` (zero data retention). |
| `XDG_CONFIG_HOME` | `~/.config` | Base del tercer paso de resolucion: `$XDG_CONFIG_HOME/commandcode/key.txt`. |

### Orden de resolucion
Expand Down Expand Up @@ -39,6 +42,29 @@ tests. `resolveApiKey()` devuelve solo el string.
Usala solo para un gateway propio que realmente termine tu trafico hacia Command Code.
Cada host que agregues aqui puede leer la API key.

### Rotacion de keys

Un pool de dos o mas keys activa la rotacion. Con una sola key (o ninguna) el plugin
mantiene el comportamiento de key unica y nunca toca una cabecera `Authorization` que
el host ya haya puesto.

| Fuente | Formato |
| --- | --- |
| `COMMANDCODE_API_KEYS` | `key1,key2` o separadas por espacios |
| `COMMANDCODE_KEYS_FILE` | una `label=key` o key pelada por linea, comentarios con `#` |
| `~/.config/commandcode/keys.txt` | mismo formato, usado si no hay variables |

| Status | Cooldown |
| --- | --- |
| `402` creditos insuficientes | 24h |
| `401` / `403` key invalida o sin permiso | 24h |
| `429` rate limit | `Retry-After` si existe y es <= 6h, si no 5m |
| `5xx` | ninguno (no es culpa de la key) |

Si todas las keys estan en cooldown se usa la que se recupera primero. El estado vive en
`<state>/commandcode-keys-state.json` (modo 0600, solo etiquetas) y cada cooldown se
registra en `<state>/commandcode-rotation.log`.

```
COMMANDCODE_ALLOWED_HOSTS=gateway.internal,proxy.lan
```
Expand Down
1 change: 1 addition & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import plugin from "./src/index.ts";

export default plugin;
export * from "./src/auth/resolver.ts";
export * from "./src/auth/pool.ts";
export * from "./src/client/api.ts";
export * from "./src/hooks/resilience.ts";
export * from "./src/hooks/zdr.ts";
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "opencode-commandcode",
"version": "1.0.7",
"version": "1.1.0",
"description": "Production-grade OpenCode plugin for Command Code: dynamic model discovery, stability hooks and quota telemetry",
"main": "index.ts",
"type": "module",
Expand Down
Loading
Loading