Files
gemini-nexus/thoughts/ledgers/CONTINUITY_ses_24bf.md
T

6.6 KiB

session, updated
session updated
ses_24bf 2026-04-22T08:17:02.015Z

Session Summary

Goal

Fix the Gemini Nexus proxy so that OpenCode IDE (and other AI SDK consumers) correctly receive and display SSE streaming responses from the /v1/chat/completions endpoint.

Constraints & Preferences

  • Preserve existing proxy behavior for non-OpenCode clients (flat SSE format compatibility)
  • Keep trafficLogger operational; do not break file-based log rotation
  • Do not remove debug console.log statements until OpenCode compatibility is fully verified
  • Prefer automated tests over manual curl tests for regression coverage

Progress

Done

  • Identified that OpenCode uses @ai-sdk/openai-compatible which requires delta.role: 'assistant' on first SSE chunk
  • Added hasSentRoleChunk flag and pushRoleChunk() helper in processStreamResponse (proxy.service.ts) to emit initial role chunk
  • Added winston.transports.Console transport to traffic-logger.ts for real-time stdout visibility
  • Added upstream request/response logging via trafficLogger.logUpstream() and trafficLogger.logUpstreamResponse() in proxy.service.ts (streaming, non-streaming, fallback paths)
  • Added console.log in writeSseResponse (proxy.controller.ts) to log full SSE payloads sent to client
  • Fixed proxy-controller.integration.test.ts mock req object to match new 3-argument chatCompletions signature
  • Updated proxy-retry-mock.test.ts to expect "role":"assistant" in SSE output
  • Created comprehensive automated stream debug tests in src/tests/unit/proxy-stream-debug.test.ts
  • Discovered root cause: Gemini v1internal API returns SSE chunks wrapped in {"response": {"candidates": [...]}, "traceId": "...", "metadata": {}}, but processStreamResponse accessed json.candidates directly → undefined → empty content
  • Fixed processStreamResponse to unwrap payload with const responsePayload = json.response ?? json; before extracting candidates
  • Added handles Gemini v1internal wrapped response format (response.candidates) test case to prevent regression
  • All 8 stream-debug tests pass; proxy-retry-mock shows 22 passed, 1 pre-existing failure (maps Anthropic response to OpenAI response with reasoning and tool_calls — unrelated to this change)

In Progress

  • Awaiting user to restart the Gemini Nexus app and verify the fix with curl / OpenCode IDE

Blocked

  • (none)

Key Decisions

  • Emit role: 'assistant' chunk before first content: Required by Vercel AI SDK's @ai-sdk/openai-compatible provider which validates delta.role === 'assistant' on first chunk
  • Support both wrapped and flat response formats: The fix uses json.response ?? json to handle both Gemini v1internal (wrapped) and standard (flat) SSE payloads
  • Keep traffic logger + console transport dual logging: File rotation preserved for production, console added for debugging; will remove debug console.log statements once verified

Next Steps

  1. User restarts the Gemini Nexus app (pnpm start or rs in terminal)
  2. Run curl test: curl -N -X POST http://localhost:8045/v1/chat/completions -H "Content-Type: application/json" -H "Authorization: Bearer <key>" -d '{"model":"gemini-3.1-pro-low","messages":[{"role":"user","content":"diga oi"}],"stream":true}'
  3. Verify console output shows [DEBUG] Extracted parts: [{"text":"..."}] (non-empty) and [SSE-OUT ...] shows content chunks with actual text
  4. Test with OpenCode IDE to confirm responses now appear
  5. Once verified, remove temporary [DEBUG] console.log statements from proxy.service.ts and proxy.controller.ts (keep trafficLogger calls)
  6. Clean up todo items and close debugging session

Critical Context

  • Root cause insight: The Gemini internal streaming endpoint (v1internal) wraps each SSE data line as {"response": {"candidates": [...]}, "traceId": "...", "metadata": {}} instead of the flat {"candidates": [...]} used in public generateContent endpoints. The non-streaming path already handled this via gemini.client.ts (payload.response unwrap), but processStreamResponse (streaming path) did not.
  • Working format: After fix, a wrapped SSE line like {"response":{"candidates":[{"content":{"parts":[{"text":"Oi!"}]}}]}} correctly extracts parts = [{"text":"Oi!"}]
  • Broken format (before fix): Same wrapped line yielded json.candidates = undefined → parts = [] → empty content chunks
  • Test file for regression: src/tests/unit/proxy-stream-debug.test.ts now covers wrapped format, flat format, empty parts, reasoning content, function calls, multi-line buffers, partial lines, and no-content-field chunks
  • Pre-existing test failure: proxy-retry-mock.test.ts line 611 expect(...).toContain(...) fails because reasoning_content is undefined on the OpenAI response object — this failure existed before all changes

File Operations

Read

  • src/lib/geminiNexus/ClaudeRequestMapper.ts
  • src/lib/geminiNexus/ClaudeResponseMapper.ts
  • src/server/modules/proxy/proxy.controller.ts
  • src/server/modules/proxy/proxy.service.ts
  • src/tests/unit/proxy-controller.integration.test.ts
  • src/tests/unit/proxy-parity-fixtures.test.ts
  • src/tests/unit/proxy-retry-mock.test.ts
  • src/utils/traffic-logger.ts
  • src/server/modules/proxy/clients/gemini.client.ts

Modified

  • src/server/modules/proxy/proxy.controller.ts — added console.log in writeSseResponse for SSE payload visibility; trafficLogger call added alongside existing logic
  • src/server/modules/proxy/proxy.service.ts — critical fix: processStreamResponse now unwraps json.response ?? json to extract candidates from Gemini v1internal wrapped format; added pushRoleChunk() for initial role:assistant delta; added trafficLogger.logUpstream() and trafficLogger.logUpstreamResponse() calls in streaming, non-streaming, fallback, and project-context-error paths; changed _requestId to requestId parameter
  • src/tests/unit/proxy-controller.integration.test.ts — fixed tests to pass mock req object matching new 3-arg chatCompletions signature
  • src/tests/unit/proxy-retry-mock.test.ts — updated stream tests to expect "role":"assistant" in output
  • src/tests/unit/proxy-stream-debug.test.ts — created comprehensive stream parsing tests; replaced flaky empty-stream timeout test with wrapped-format regression test
  • src/utils/traffic-logger.ts — added winston.transports.Console transport with colorized format for real-time stdout logging