Skip to content

Sync upstream; error on empty streams, 413 oversized payloads - #1

Open
andomeder wants to merge 16 commits into
mainfrom
fix-null-content-tool-calls
Open

andomeder wants to merge 16 commits into
mainfrom
fix-null-content-tool-calls

Conversation

@andomeder

Copy link
Copy Markdown
Owner

No description provided.

bermudi and others added 16 commits June 8, 2026 14:36
Replace all log.Printf with slog (stdlib). Request ID flows through context: middleware generates, handler forwards to upstream as x-request-id, response header set on every endpoint.

Key changes:

- internal/proxy/logging.go: new. RequestLoggingMiddleware, statusWriter with Flush+Hijack, context-aware logging helpers, PROXY_LOG_JSON=1 toggle, -debug flag.

- handler.go: request capture writes .request.json before upstream call (exists even on 401/transport failures), response tee unchanged.

- assembler.go: context-first constructors, usage logged on finish.

- adapter.go: no more debugf field, all logging via context.

- router.go: old logger middleware removed, RequestLoggingMiddleware wraps mux.

- main.go: InitLogger/EnableDebugLogging, -debug flag.

- statusWriter: transparent Flush and Hijack delegation so streaming Flusher type assertions do not break (was a real bug).

Docs: README updated (observability section, correct config.environment, full config field list, -capture-dir and -debug flags). ROADMAP updated (1.2 and 2.2 marked done).
Add scripts/diff-captures.sh: compares proxy .request.json captures against

cmd-recorder POST_alpha_generate.json captures. Normalizes both with jq -S,

runs unified diff, and prints a summary of known expected differences.

Docs: README updated with capture-diff workflow example.
Proxy side:

- Add XCommandCodeMemory, XCommandCodeSkills, XCommandCodeTaste to OpenAIChatRequest

- BuildCCRequestWithWorkingDir forwards these fields to CCRequestBody

- Generate threadId per request (uuid.New) for session continuity

- Update responsesToChatRequest to forward new fields

Pi extension (cc-cwd.ts):

- readAgentsMd: reads AGENTS.md from project dir → memory

- readSkills: reads SKILL.md from .agents/skills/.pi/skills/.commandcode/skills/

  and formats as XML matching the real binary format

- parseFrontmatter: parses YAML frontmatter name/description from skill files

Docs: README updated with threadId, memory, skills, taste fields.
Theory (2026-06-08): The missing threadId may have been a contributing factor

to the "automated environment update" acks. The proxy used to omit

threadId entirely, making every request a new session. If the gateway

constructs the system prompt only on the first message of a session,

the model would see the environment context as new each time and respond

to it. The threadId fix makes the proxy match the real binary session

model.
Reverse-engineered the minified command-code v0.33.1 bundle. Key findings:

**Two distinct /alpha/generate paths**
- callApi() (line 4729, -p mode): sends temperature: 0.3, threadId
- callServerAPI() (line 5255, interactive mode): no temperature, no threadId
  in body, may include reasoning_effort
- The proxy mimics callServerAPI(); the callApi() captures were misleading

**Taste learning is user-config-driven, not a fixed value**
- isTasteLearningEnabled() reads userConfig.tasteLearning (default true)
- The toggle gates ONLY x-taste-learning header
- The taste BODY field is sent unconditionally (gated by file existence)
- Proxy currently hardcodes 'true' — lies when user has taste off
- Fix direction: pi extension forwards userConfig.tasteLearning
  as x_command_code_taste_learning, with -taste-learning CLI flag override

**Header corrections**
- x-co-flag (misnamed INTERNAL_TEAM_FLAG_HEADER) carries
  isOAuthEnforced().toString(), NOT a team flag
- x-session-id is sent by callServerAPI; proxy doesn't send it
- For API-key auth, x-co-flag: 'false' is correct

**reasoning_effort is irrelevant for MiniMax-M3**
- Model registry: MiniMax-M3 has reasoning:!0 but NO reasoningEfforts[]
- Binary has nothing to set, so field is absent
- Proxy is accidentally correct for the model it actually serves
- Still relevant for future reasoning-effort-configurable models

**Doc structure**
- Deduped 'Two API call paths' (was in both gap-tracker and reverse-eng sections)
- New 'Taste learning' section with full analysis
- AGENTS.md gains conventions for x-taste-learning and x-co-flag
- README.md file layout updated to post-refactor state
…ride)

The real binary's callServerAPI() sends x-taste-learning: <isTasteLearningEnabled()>,
which is the user's userConfig.tasteLearning (default true). The proxy used to
hardcode 'true', lying when the user has taste learning off (e.g. the user's
local config.json has tasteLearning: false). For their install the gateway
sees 'true' for what should be 'false'.

Three places accept the value now:
1. x_command_code_taste_learning on the OpenAI request (per-request, set by
   the pi extension which reads ~/.commandcode/config.json)
2. -taste-learning CLI flag on the proxy (proxy-wide default, true)
3. true (binary default for unset config)

Precedence: per-request > CLI flag > binary default.

- Upstream interface gains a tasteLearning bool param on Generate
- ccAdapter.createUpstreamRequest uses the resolved value
- Proxy.TasteLearning *bool field
- ResolveTasteLearning(perRequest, proxyDefault) bool helper
- OpenAIChatRequest and OpenAIResponsesRequest gain XCommandCodeTasteLearning *bool

The pi cc-cwd extension (in /home/daniel/build/agent-extensions/pi/cc-cwd/)
reads ~/.commandcode/config.json tasteLearning and forwards as
x_command_code_taste_learning. Honors COMMAND_CODE_CONFIG_FILE env var for
matching the binary's config.local.json / config.staging.json variants.

Tests:
- TestCreateUpstreamRequest_TasteFalse (adapter sets 'false' when asked)
- TestResolveTasteLearning (7 precedence cases)
- TestHandleChatCompletions_TasteLearningFromRequest (per-request wins)
- TestHandleChatCompletions_TasteLearningFromProxy (CLI default used)
- TestHandleChatCompletions_TasteLearningDefault (binary default true)
Audit pass after the previous taste commit. Found:

1. Bug: responsesToChatRequest didn't copy XCommandCodeTasteLearning
   from the Responses request to the chat request. Users on /v1/responses
   silently lost the per-request taste override. Now wired.
   Added TestResponsesToChatRequest_PassesTasteLearning (true, false, nil).

2. Stale doc: AGENTS.md convention said the extension should read
   ~/.commandcode/config.json and forward tasteLearning. The extension
   actually hardcodes false (per user request, simpler, no cross-system
   dep). Reworded the convention to match the actual implementation.

3. Missing doc: README.md CLI options table didn't list -taste-learning.
   Added.

4. Missing doc: ROADMAP.md didn't track the closed fidelity gap. Added
   'Closed fidelity gaps' section linking the gap to commit e77a37b.
The pi extension generates a session ID (sess_ + 16 hex chars) per pi
process and sends it as x_command_code_session_id on every request. The
proxy now forwards this as the upstream x-session-id header. When no
extension is present (e.g. direct curl), the proxy generates a
per-request fallback in the same format.

Closes the last remaining header gap vs the real command-code binary.
- 'response side' → 'client-facing output' (what the proxy sends to clients)
- 'request side' → 'upstream request' (what the proxy sends to CommandCode)
- 'response-side parity test' → 'client-facing parity test'
- 'request-side fidelity table' → 'upstream-request fidelity table'
- translator: emit EventUnknown (with raw type + line) instead of silently
  dropping unknown NDJSON event types. The assembler (which has request
  context) logs the warning with request ID correlation.
- assembler: handle EventUnknown via reqWarn so concurrent requests don't
  produce orphaned log lines.
- scripts/cc-fingerprint: extract protocol-surface fingerprint from the
  command-code binary bundle (function bodies, named function list, API
  paths, headers, symbol counts). Designed to be diffed between versions.
- baseline fingerprint saved at ref/fingerprints/v0.33.4.txt.

Why: the proxy depends on matching an opaque binary that auto-updates.
Unknown response-shape changes were silently swallowed; request-shape
drift was invisible until it broke something. These two changes turn
protocol drift from a silent failure into a greppable signal.
The 2026-06-08 fix that added per-request threadId was wrong. Captured
traffic from callServerAPI() (225 main-loop captures from interactive
command-code) shows the real binary does not send threadId in the body
at all. The per-request UUID was making the gateway treat each turn as
a brand-new session, rebuilding the system prompt every time.

Code changes:
- remove ThreadID field from CCRequestBody
- remove newThreadID() helper and its call in BuildCCRequestWithWorkingDir
- update test assertion (was checking for non-nil threadId)

Docs:
- AGENTS.md: rewrite MiniMax-M3 distraction factor dev2k6#2 as reverted,
  add factor dev2k6#3 (skills gap), clarify x-session-id vs threadId
- MAINTAINING.md: add callServerAPI() capture source, rewrite threadId
  section as resolved, add skills gap section, update fidelity tables
- README.md: remove threadId from request fields, note skills gap
- ROADMAP.md: add threadId revert entry, update review date
Tested against live api.commandcode.ai: the gateway rejects system and
developer roles in params.messages[] with a hard 400 (not a soft
warning). Without DropSystemMessages, every pi request would fail.

Also observed: params.stream must be true, and message content must be
an array (not a bare string) for non-tool roles.
…ction, global skills forwarding, and removal of threadId field
@andomeder
andomeder force-pushed the fix-null-content-tool-calls branch from 1d64106 to a4d6373 Compare August 14, 2026 10:49
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.

2 participants