- Add cacheHitRate to ProxyMetricsService, handler, and router schema - Fix StatCard to support string values and purple accent - Fix duplicate tabs keys in i18n translations (EN/ZH/RU) - Add missing cacheStatus translations across all languages
6.5 KiB
6.5 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
trafficLoggeroperational; do not break file-based log rotation - Do not remove debug
console.logstatements 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-compatiblewhich requiresdelta.role: 'assistant'on first SSE chunk - Added
hasSentRoleChunkflag andpushRoleChunk()helper inprocessStreamResponse(proxy.service.ts) to emit initial role chunk - Added
winston.transports.Consoletransport totraffic-logger.tsfor real-time stdout visibility - Added upstream request/response logging via
trafficLogger.logUpstream()andtrafficLogger.logUpstreamResponse()inproxy.service.ts(streaming, non-streaming, fallback paths) - Added
console.loginwriteSseResponse(proxy.controller.ts) to log full SSE payloads sent to client - Fixed
proxy-controller.integration.test.tsmockreqobject to match new 3-argumentchatCompletionssignature - Updated
proxy-retry-mock.test.tsto 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
v1internalAPI returns SSE chunks wrapped in{"response": {"candidates": [...]}, "traceId": "...", "metadata": {}}, butprocessStreamResponseaccessedjson.candidatesdirectly →undefined→ empty content - Fixed
processStreamResponseto unwrap payload withconst responsePayload = json.response ?? json;before extractingcandidates - 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-compatibleprovider which validatesdelta.role === 'assistant'on first chunk - Support both wrapped and flat response formats: The fix uses
json.response ?? jsonto handle both Geminiv1internal(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.logstatements once verified
Next Steps
- User restarts the Gemini Nexus app (
pnpm startorrsin terminal) - 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}' - Verify console output shows
[DEBUG] Extracted parts: [{"text":"..."}](non-empty) and[SSE-OUT ...]shows content chunks with actual text - Test with OpenCode IDE to confirm responses now appear
- Once verified, remove temporary
[DEBUG]console.logstatements fromproxy.service.tsandproxy.controller.ts(keep trafficLogger calls) - 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 publicgenerateContentendpoints. The non-streaming path already handled this viagemini.client.ts(payload.responseunwrap), butprocessStreamResponse(streaming path) did not. - Working format: After fix, a wrapped SSE line like
{"response":{"candidates":[{"content":{"parts":[{"text":"Oi!"}]}}]}}correctly extractsparts = [{"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.tsnow 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.tsline 611expect(...).toContain(...)fails becausereasoning_contentisundefinedon the OpenAI response object — this failure existed before all changes
File Operations
Read
src/lib/geminiNexus/ClaudeRequestMapper.tssrc/lib/geminiNexus/ClaudeResponseMapper.tssrc/server/modules/proxy/proxy.controller.tssrc/server/modules/proxy/proxy.service.tssrc/tests/unit/proxy-controller.integration.test.tssrc/tests/unit/proxy-parity-fixtures.test.tssrc/tests/unit/proxy-retry-mock.test.tssrc/utils/traffic-logger.tssrc/server/modules/proxy/clients/gemini.client.ts
Modified
src/server/modules/proxy/proxy.controller.ts— addedconsole.loginwriteSseResponsefor SSE payload visibility;trafficLoggercall added alongside existing logicsrc/server/modules/proxy/proxy.service.ts— critical fix:processStreamResponsenow unwrapsjson.response ?? jsonto extract candidates from Gemini v1internal wrapped format; addedpushRoleChunk()for initialrole:assistantdelta; addedtrafficLogger.logUpstream()andtrafficLogger.logUpstreamResponse()calls in streaming, non-streaming, fallback, and project-context-error paths; changed_requestIdtorequestIdparametersrc/tests/unit/proxy-controller.integration.test.ts— fixed tests to pass mockreqobject matching new 3-argchatCompletionssignaturesrc/tests/unit/proxy-retry-mock.test.ts— updated stream tests to expect"role":"assistant"in outputsrc/tests/unit/proxy-stream-debug.test.ts— created comprehensive stream parsing tests; replaced flaky empty-stream timeout test with wrapped-format regression testsrc/utils/traffic-logger.ts— addedwinston.transports.Consoletransport with colorized format for real-time stdout logging