3.8 KiB
3.8 KiB
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.goandinternal/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:
- Non-streaming is completely broken — server hung for 30s and never wrote a response body.
- SSE content was double-JSON-encoded — each
delta.contentfield held the entire Cursor CLI JSON line (includingtype:"system",type:"user",type:"result") serialized as a string, instead of plain assistant text. - Missing
rolein first delta. - Missing
finish_reasonin final chunk. - 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 fromcursor-api-proxywas overridingHOME→ temp dir. On macOS with keychain login, theagentCLI resolves its session token via~/.cursor/+ the real keychain, so a fakeHOMEmadeagentexit immediately with "Authentication required. Please run 'agent login'". The adapter surfaced this as either a hang (when timeouts swallowed the exit) or asexit status 1once 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 overrideCURSOR_CONFIG_DIRby default.HOME/XDG_CONFIG_HOME/APPDATAare only isolated whenCURSOR_API_KEYis 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 viacontent == accumulatedand skipped.internal/server/handlers.go+anthropic_handlers.go— already emitrole:"assistant"in the first delta,finish_reason:"stop"in the final chunk, andusageat the top level. Regression tests added tomessages_test.golock 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.