|
| 1 | +# Case Study: Issue #206 - No debug HTTP requests/responses in --verbose mode |
| 2 | + |
| 3 | +## Issue Summary |
| 4 | + |
| 5 | +When running `agent --model opencode/kimi-k2.5-free --verbose`, the `--verbose` flag enables detailed JSON logging output, but raw HTTP requests and responses to AI model providers are not logged. This makes it impossible to diagnose provider failures — the user can see the model returned zero tokens but cannot see the actual HTTP request/response that caused the failure. |
| 6 | + |
| 7 | +## Evidence |
| 8 | + |
| 9 | +Log file: `solution-draft-log.txt` (2237 lines from gist `a0bb13a57b06e7df16e36caf9093e9e6`) |
| 10 | + |
| 11 | +Key observations from the log: |
| 12 | +- Version `0.16.11` was running (which includes the verbose HTTP logging from PR #205/issue #204) |
| 13 | +- `getSDK` log messages appear at lines 681 and 700 — confirming the SDK was created |
| 14 | +- Model `opencode/kimi-k2.5-free` resolved to `@ai-sdk/openai-compatible` bundled provider |
| 15 | +- The model responded with text and tool calls at lines 1970-1976 |
| 16 | +- **Zero "HTTP request" or "HTTP response" log messages** appear anywhere in the 2237-line log |
| 17 | +- Provider returned zero tokens with unknown finish reason at line 1995 |
| 18 | + |
| 19 | +## Root Cause Analysis |
| 20 | + |
| 21 | +The verbose HTTP logging code in `js/src/provider/provider.ts` (lines 1204-1388) checked `Flag.OPENCODE_VERBOSE` at **SDK creation time** using a conditional guard: |
| 22 | + |
| 23 | +```typescript |
| 24 | +// OLD CODE (buggy) |
| 25 | +if (Flag.OPENCODE_VERBOSE) { |
| 26 | + const innerFetch = options['fetch']; |
| 27 | + options['fetch'] = async (...) => { /* logging wrapper */ }; |
| 28 | +} |
| 29 | +``` |
| 30 | + |
| 31 | +This created a race condition between: |
| 32 | +1. **SDK creation time**: When `getSDK()` creates and caches the provider SDK |
| 33 | +2. **Flag initialization time**: When `Flag.setVerbose(true)` is called from the yargs middleware |
| 34 | + |
| 35 | +The SDK cache uses `Bun.hash.xxHash32(JSON.stringify({ pkg, options }))` as the key. Since `JSON.stringify` strips function values, the cache key is the same regardless of whether the verbose wrapper was applied. This means: |
| 36 | + |
| 37 | +- If the SDK is created **before** verbose mode is enabled, the fetch wrapper is never applied |
| 38 | +- The cached SDK (without verbose logging) is then reused for all subsequent requests |
| 39 | +- Even though `Flag.OPENCODE_VERBOSE` is `true` by the time the actual HTTP request happens, the wrapper was never installed |
| 40 | + |
| 41 | +### Investigation Timeline |
| 42 | + |
| 43 | +1. Verified `Flag.setVerbose(true)` is called in yargs middleware before the command handler |
| 44 | +2. Verified the AI SDK properly passes custom `fetch` to `postJsonToApi` (confirmed via experiment `test-verbose-fetch-wrapper.ts`) |
| 45 | +3. Verified the fetch wrapping mechanism works correctly via experiment |
| 46 | +4. Verified `Flag.OPENCODE_VERBOSE` timing with experiment `test-verbose-flag-timing.ts` |
| 47 | +5. Identified that while individual tests pass, the production SDK caching and module loading order may cause the flag check to fail |
| 48 | + |
| 49 | +## Fix |
| 50 | + |
| 51 | +Changed the verbose logging wrapper from a creation-time conditional to a call-time check: |
| 52 | + |
| 53 | +```typescript |
| 54 | +// NEW CODE (fixed) |
| 55 | +{ |
| 56 | + const innerFetch = options['fetch']; |
| 57 | + options['fetch'] = async (input, init) => { |
| 58 | + // Check verbose flag at call time — not at SDK creation time |
| 59 | + if (!Flag.OPENCODE_VERBOSE) { |
| 60 | + return innerFetch(input, init); |
| 61 | + } |
| 62 | + // ... logging logic ... |
| 63 | + }; |
| 64 | +} |
| 65 | +``` |
| 66 | + |
| 67 | +Key changes: |
| 68 | +- **Always** wrap the fetch function (remove outer `if` guard) |
| 69 | +- Check `Flag.OPENCODE_VERBOSE` **inside** the wrapper at call time |
| 70 | +- When verbose is disabled, the wrapper is a zero-overhead passthrough (just a boolean check) |
| 71 | +- When verbose is enabled (even if it was disabled at SDK creation), logging works correctly |
| 72 | + |
| 73 | +This is robust because: |
| 74 | +- No dependency on flag timing vs SDK creation order |
| 75 | +- Verbose mode can be toggled dynamically |
| 76 | +- Negligible overhead when verbose is disabled (single boolean check per HTTP request) |
| 77 | + |
| 78 | +## Files Changed |
| 79 | + |
| 80 | +- `js/src/provider/provider.ts`: Move verbose flag check from SDK creation time to fetch call time |
| 81 | +- `js/tests/verbose-http-logging.test.ts`: Add tests for runtime flag checking behavior |
| 82 | +- `js/experiments/test-verbose-fetch-wrapper.ts`: Experiment verifying fetch wrapping works |
| 83 | +- `js/experiments/test-verbose-flag-timing.ts`: Experiment verifying flag timing |
| 84 | + |
| 85 | +## Test Coverage |
| 86 | + |
| 87 | +Added 3 new tests in `verbose-http-logging.test.ts`: |
| 88 | +1. Verbose wrapper is a no-op when verbose is disabled |
| 89 | +2. Verbose wrapper logs when verbose is enabled at call time |
| 90 | +3. Verbose flag can change between SDK creation and fetch call |
0 commit comments