- 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
89 lines
6.5 KiB
Markdown
89 lines
6.5 KiB
Markdown
---
|
|
session: ses_24bf
|
|
updated: 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
|
|
|
|
- [x] Identified that OpenCode uses `@ai-sdk/openai-compatible` which requires `delta.role: 'assistant'` on first SSE chunk
|
|
- [x] Added `hasSentRoleChunk` flag and `pushRoleChunk()` helper in `processStreamResponse` (`proxy.service.ts`) to emit initial role chunk
|
|
- [x] Added `winston.transports.Console` transport to `traffic-logger.ts` for real-time stdout visibility
|
|
- [x] Added upstream request/response logging via `trafficLogger.logUpstream()` and `trafficLogger.logUpstreamResponse()` in `proxy.service.ts` (streaming, non-streaming, fallback paths)
|
|
- [x] Added `console.log` in `writeSseResponse` (`proxy.controller.ts`) to log full SSE payloads sent to client
|
|
- [x] Fixed `proxy-controller.integration.test.ts` mock `req` object to match new 3-argument `chatCompletions` signature
|
|
- [x] Updated `proxy-retry-mock.test.ts` to expect `"role":"assistant"` in SSE output
|
|
- [x] Created comprehensive automated stream debug tests in `src/tests/unit/proxy-stream-debug.test.ts`
|
|
- [x] **Discovered root cause**: Gemini `v1internal` API returns SSE chunks wrapped in `{"response": {"candidates": [...]}, "traceId": "...", "metadata": {}}`, but `processStreamResponse` accessed `json.candidates` directly → `undefined` → empty content
|
|
- [x] Fixed `processStreamResponse` to unwrap payload with `const responsePayload = json.response ?? json;` before extracting `candidates`
|
|
- [x] Added `handles Gemini v1internal wrapped response format (response.candidates)` test case to prevent regression
|
|
- [x] 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
|