83 lines
3.8 KiB
Markdown
83 lines
3.8 KiB
Markdown
# cursor-adapter SSE Diagnostic Results
|
|
|
|
> **Status: RESOLVED 2026-04-18.** This file is kept for history. The bugs
|
|
> listed below have been fixed. Current behavior is captured by regression
|
|
> tests in `internal/server/messages_test.go` and `internal/converter/convert_test.go`.
|
|
|
|
---
|
|
|
|
## Originally reported (2026-04-15)
|
|
|
|
When used as an OpenAI-compatible endpoint for SDKs like
|
|
`@ai-sdk/openai-compatible` (OpenCode), cursor-adapter had five issues:
|
|
|
|
1. **Non-streaming is completely broken** — server hung for 30s and never
|
|
wrote a response body.
|
|
2. **SSE content was double-JSON-encoded** — each `delta.content` field
|
|
held the *entire* Cursor CLI JSON line (including `type:"system"`,
|
|
`type:"user"`, `type:"result"`) serialized as a string, instead of plain
|
|
assistant text.
|
|
3. **Missing `role` in first delta.**
|
|
4. **Missing `finish_reason` in final chunk.**
|
|
5. **Usage not at the top level** — embedded inside a stringified JSON
|
|
payload instead of `chunk.usage`.
|
|
|
|
---
|
|
|
|
## Root cause
|
|
|
|
Two separate bugs plus one latent one, all landed together:
|
|
|
|
- **Non-stream hang / `exit status 1`:** the chat-only isolation ported from
|
|
`cursor-api-proxy` was overriding `HOME` → temp dir. On macOS with
|
|
keychain login, the `agent` CLI resolves its session token via
|
|
`~/.cursor/` + the real keychain, so a fake `HOME` made `agent` exit
|
|
immediately with "Authentication required. Please run 'agent login'".
|
|
The adapter surfaced this as either a hang (when timeouts swallowed the
|
|
exit) or as `exit status 1` once the error bubbled up.
|
|
- **Content wrapping / leaked system-user chunks:** older pre-parser code
|
|
forwarded raw Cursor JSON lines as `delta.content`. The parser had
|
|
already been rewritten by the time this diagnostic was taken, but the
|
|
report caught an earlier build.
|
|
- **Duplicate final delta (discovered during this pass):** the stream
|
|
parser's accumulator was *reassigned* (`p.accumulated = content`) even
|
|
when the new fragment did not start with the accumulated prefix. With
|
|
Cursor CLI's incremental output mode (one fragment per message), that
|
|
meant the "you said the full text" final message looked different from
|
|
accumulated and was emitted as a second copy of the whole response.
|
|
|
|
---
|
|
|
|
## Fix summary
|
|
|
|
- `internal/workspace/workspace.go` — only override `CURSOR_CONFIG_DIR` by
|
|
default. `HOME`/`XDG_CONFIG_HOME`/`APPDATA` are only isolated when
|
|
`CURSOR_API_KEY` is set (which bypasses keychain auth anyway).
|
|
- `internal/converter/convert.go` — stream parser now handles both
|
|
cumulative and incremental Cursor output modes. In the non-prefix
|
|
branch it appends to accumulated instead of replacing it, so the final
|
|
duplicate is correctly detected via `content == accumulated` and
|
|
skipped.
|
|
- `internal/server/handlers.go` + `anthropic_handlers.go` — already emit
|
|
`role:"assistant"` in the first delta, `finish_reason:"stop"` in the
|
|
final chunk, and `usage` at the top level. Regression tests added to
|
|
`messages_test.go` lock this in.
|
|
|
|
## Verified manually
|
|
|
|
```
|
|
$ curl -sN http://localhost:8765/v1/chat/completions \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"model":"auto","stream":true,"messages":[{"role":"user","content":"count 1 to 5"}]}'
|
|
|
|
data: {"id":"chatcmpl-…","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}
|
|
data: {"id":"chatcmpl-…","choices":[{"index":0,"delta":{"content":"\n1、2、3、4、5。"},"finish_reason":null}]}
|
|
data: {"id":"chatcmpl-…","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":6,"completion_tokens":5,"total_tokens":11}}
|
|
data: [DONE]
|
|
```
|
|
|
|
Non-streaming returns `chat.completion` JSON with `stop_reason:"stop"` and
|
|
`usage` populated. Anthropic `/v1/messages` emits `message_start` →
|
|
`content_block_delta*` → `message_stop` without duplicating the final
|
|
cumulative fragment.
|