Skip to content

feat(proxy): expand model catalog, per-model reasoning effort, opencode variants + production hardening#2

Merged
thaolaptrinh merged 6 commits into
mainfrom
fix/proxy-hardening
Jul 20, 2026
Merged

feat(proxy): expand model catalog, per-model reasoning effort, opencode variants + production hardening#2
thaolaptrinh merged 6 commits into
mainfrom
fix/proxy-hardening

Conversation

@thaolaptrinh

Copy link
Copy Markdown
Owner

Verified end-to-end against the live CC /alpha/generate stream (captured events, probed the validation schema) and the command-code CLI bundle (authoritative model catalog). The biggest issue — tool_choice rejected by CC — was invisible from code review alone.

Functional fixes (bugs found via live capture)

  • tool_choice object form — the proxy sent a bare string, which CC rejects with 400 "expected object, received string at params.tool_choice". Any request with tool_choice (other than OpenAI "auto") broke. Now maps both OpenAI and Anthropic to CC's union {type:"auto"|"any"|"tool", name?}. Verified live (all shapes → 200). CC has no "required" (use "any") and no "none".
  • Streaming Anthropic input_tokens — CC's start event carries no usage, so message_start reported input_tokens: 0. Now emits input_tokens (and cache_read_input_tokens) in message_delta; the Anthropic SDK merges it over message_start. Verified against @anthropic-ai/sdk source (MessageStream).

Production hardening

  • Retry retryable upstream failures (5xx/429/timeout/network) up to 2× with backoff, before the stream starts only; client aborts never retried. (isRetryable is now actually used.)
  • Body size limit 10 MiB → 413 (anti-OOM).
  • Guards on all response writes against destroyed/ended sockets.
  • unhandledRejection/uncaughtException backstop + bounded graceful shutdown (10s force-exit).
  • Docker: mount the correct ~/.config/commandcode-api-proxy path (was ~/.commandcode, so auth.json never loaded); bind host port to 127.0.0.1 only.
  • CORS_ORIGIN env (default *, empty disables). Dropped key-prefix logging.

Model catalog + per-model reasoning effort

  • Added 12 models reachable on the standard plan (verified 200, not 403): GLM-5.2(+Fast), MiniMax-M3, Kimi-K2.7-Code(+Highspeed), Kimi-K3, mimo-v2.5-pro, Step-3.7-Flash, grok-4.5, nemotron-3-ultra, inkling, Hy3. Excluded plan-gated ones (gemini×2, fugu-ultra, muse-spark).
  • reasoningEfforts map (4 models, verified against the CC CLI catalog): deepseek-v4-pro/flash & GLM-5.2 → {high,max}; grok-4.5 → {low,medium,high}.
  • resolveEffortForModel() clips an unsupported effort to the nearest valid level per model, and reasoning_effort now accepts xhigh/max (so deepseek's max is reachable). Verified live.
  • Bare model names (e.g. GLM-5.2, Kimi-K3) — as OpenCode sends them — now resolve to canonical IDs (case-insensitive alias + last-segment fallback).

OpenCode integration (/variants)

OpenCode's /variants picker lists a model's variants, auto-generated from models.dev (which doesn't cover our custom IDs). The setup now declares variants explicitly for effort-capable models and disabled:true the tiers each model doesn't support (OpenCode filters them out), so /variants shows exactly what each model supports.

Verification

133 tests pass; tsc/lint/check/build green. Every functional change cross-checked three ways: read code → CC CLI bundle → live probe.

Known minor (non-blocking)

maxOutputTokens in models.json only covers the original 14 models; new ones fall back to 128000. The CC catalog exposes no max-output field, so this is cosmetic (OpenCode's output-limit meter only) — left for the maintainer to fill when data is available.

Suggested merge: squash

Several commits are fixups to earlier ones in this PR; squashing yields one clean commit on main (full breakdown preserved above).

🤖 Generated with Claude Code

thaolaptrinh and others added 6 commits July 19, 2026 18:41
…on hardening

Found + verified against the live CC /alpha/generate stream (captured real
events + probed the validation schema). The biggest issue — tool_choice — was
invisible from code review alone.

Functional fixes
- tool_choice: was sent as a bare string, which CC rejects with 400
  ("expected object, received string at params.tool_choice"). Map both OpenAI
  and Anthropic tool_choice to CC's object union {type:"auto"|"any"|"tool",
  name?}. CC has no "required" (use "any") and no "none" (omit). Verified all
  three shapes return 200 against live CC.
- streaming Anthropic input_tokens: CC's `start` event carries no usage, so
  message_start reported input_tokens=0. Emit input_tokens in message_delta
  from finish.totalUsage (the Anthropic SDK merges it over message_start).

Robustness / ops
- Retry retryable upstream failures (5xx/429/timeout/network) up to 2x with
  backoff, before the stream starts only; client aborts are never retried.
  (isRetryable was previously computed but unused.)
- Request body size limit (10 MiB → 413) to prevent OOM.
- Guard all response writes against destroyed/ended sockets (client disconnect
  mid-request no longer throws).
- Top-level unhandledRejection/uncaughtException handlers + bounded graceful
  shutdown (force-exit after 10s).

Config / packaging
- Docker: mount ~/.config/commandcode-api-proxy (was ~/.commandcode, so
  auth.json never loaded); bind host port to 127.0.0.1 only.
- CORS_ORIGIN env (default *; empty disables); document the localhost-only
  security posture.
- Drop key-prefix logging (length only).
- Remove dead NDJSONParser class (runtime parses via parseCCLine).

Tests: 127 pass; added tool_choice (object form) + streaming input_tokens
coverage.

Co-Authored-By: Claude <noreply@anthropic.com>
Researched the model catalog in the command-code CLI (the authoritative
source) and the /alpha/generate plan gating by probing each candidate live.

Catalog (src/models.json)
- Add 12 models reachable on the standard plan (verified 200, not 403):
  GLM-5.2, GLM-5.2-Fast, MiniMax-M3, Kimi-K2.7-Code (+Highspeed), Kimi-K3,
  mimo-v2.5-pro, Step-3.7-Flash, grok-4.5, nemotron-3-ultra, inkling, Hy3.
- Exclude plan-gated models (gemini-3.5-flash, gemini-3.1-flash-lite,
  fugu-ultra, muse-spark) — they 403 MODEL_NOT_IN_PLAN on standard.
- Add reasoningEfforts map; fix stale contextWindows (Step-3.5-Flash 1M,
  Kimi K2.5/K2.6 256k); add aliases for all new models.

Reasoning effort (the capability gap)
- Each CC model accepts a different effort subset (e.g. deepseek-v4-pro is
  {high,max}; grok-4.5 is {low,medium,high}). CC silently coerces unsupported
  values, so the proxy previously could send (or drop) the wrong level and
  could never reach "max"/"xhigh".
- models.ts: resolveEffortForModel() clips a request to the nearest valid
  level for the model; uncatalogued models pass through unchanged.
- OpenAI: widen reasoning_effort to low/medium/high/xhigh/max.
- Anthropic: extend thinking.budget_tokens mapping through xhigh/max, then
  clip per model. Verified live: deepseek-v4-pro+low→high, +max→max;
  grok-4.5+xhigh→high.

Tests: 130 pass; added effort-clipping coverage (per-model clip, xhigh/max,
grok cap) for both protocols. README aliases + reasoning-effort notes updated.

Co-Authored-By: Claude <noreply@anthropic.com>
OpenCode's provider config (written by --setup-opencode) keys each model by
the bare last path segment with original casing (e.g. "GLM-5.2",
"MiniMax-M3", "Kimi-K3") and sends that key as the model id. The proxy's
resolveModel only matched lowercase aliases or org-prefixed ids, so mixed-case
bare names passed through unchanged and CC rejected them (403 — needs org/model).
Only deepseek/grok worked, by lowercase-alias coincidence.

resolveModel now: (a) matches aliases case-insensitively, and (b) falls back to
the builtin catalog's last path segment. Verified live: GLM-5.2, MiniMax-M3,
Kimi-K3, Kimi-K2.7-Code-Highspeed, nemotron-3-ultra-550b-a55b all resolve to
canonical ids and return 200.

Tests: 131 pass; added bare-name resolution coverage.

Co-Authored-By: Claude <noreply@anthropic.com>
OpenCode's `/variants` picker lists a model's `variants` map. OpenCode only
auto-generates reasoning variants from models.dev metadata, which does not
cover our custom commandcode model IDs — so the picker was empty.

The opencode setup now declares `variants` explicitly for effort-capable
models, driven by models.json's reasoningEfforts map:
  deepseek-v4-pro/flash, GLM-5.2 → {high, max}
  grok-4.5                     → {low, medium, high}
Each variant maps to `{ reasoningEffort: <level> }`, which OpenCode forwards
to the proxy (and the proxy clips per model — see resolveEffortForModel).

Source-of-truth for the mechanism: sst/opencode —
provider/provider.ts (config variants are mergeDeep'd in) and
provider/transform.ts ({reasoningEffort} variant shape for openai-compatible).

Tests: 133 pass; added setup-opencode coverage (variants emitted for effort
models, omitted for others).

Co-Authored-By: Claude <noreply@anthropic.com>
…the model

OpenCode auto-adds WIDELY_SUPPORTED_EFFORTS (low/medium/high) to every
reasoning-capable openai-compatible model and mergeDeep's them with our config,
so deepseek-v4-pro (which CC only supports at {high, max}) showed 4 variants
including bogus low/medium (which the proxy would silently clip to high).

Declare the full effort universe per model and mark the unsupported tiers
`disabled: true`. OpenCode filters disabled variants out of the picker
(pickBy !disabled), so each model shows exactly what it supports:
  deepseek-v4-pro/flash, GLM-5.2 → high, max
  grok-4.5                         → low, medium, high

Tests updated to assert the disabled tiers. 133 pass.

Co-Authored-By: Claude <noreply@anthropic.com>
An empty reasoningEfforts array ([] is truthy) bypassed the early return and
reached reduce() on an empty array, which throws. No current model triggers it,
but adding `reasoningEfforts: []` to models.json would have crashed the request.
Added a length guard.

Co-Authored-By: Claude <noreply@anthropic.com>
@thaolaptrinh thaolaptrinh changed the title Proxy hardening: tool_choice, streaming usage, retry + model catalog & reasoning effort feat(proxy): expand model catalog, per-model reasoning effort, opencode variants + production hardening Jul 20, 2026
@thaolaptrinh
thaolaptrinh merged commit 665a859 into main Jul 20, 2026
3 checks passed
@thaolaptrinh
thaolaptrinh deleted the fix/proxy-hardening branch July 20, 2026 04:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant