Baseline for the Kong replacement on api.riotpiao.com. Brings the working tree under version control for the first time: gateway source, the task board that drives the agent runs, test fixtures, and K8s manifests. Anchor the gateway ignore rule to the repo root. Unanchored, "gateway" also matched the cmd/gateway/ source directory, so the program entrypoint was excluded from every commit. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
3.8 KiB
3.8 KiB
2.12 — Anthropic SSE state machine (RED)
Phase: 2 — LLM surfaces Stage: RED Depends on: 2.11, 1.2, 1.3
POST /llm/v1/messages with "stream":true must emit Anthropic SSE. Anthropic uses
NAMED events carrying content-block indices; the upstream emits flat OpenAI data-only
chunks. Write the failing tests against the event sequence before writing a translator.
- Every frame has both an
event:line and adata:line; a baredata:frame is a failure on this surface Content-Typeistext/event-stream- Event order for a full response is exactly:
message_start, then for each blockcontent_block_start, one or morecontent_block_delta,content_block_stop, thenmessage_delta, thenmessage_stop message_startcarries the message envelope with the client-sent model,roleassistant, emptycontent, andusage.input_tokens- The block carrying
reasoning_contentis index 0 with block typethinking, and its deltas arethinking_delta - The block carrying
contentis index 1 with block typetext, and its deltas aretext_delta - The end of reasoning is only knowable when
contentfirst arrives, so the arrival of the firstcontenttoken MUST emitcontent_block_stopfor index 0 beforecontent_block_startfor index 1 — the two blocks never overlap - If a response has no
reasoning_contentat all, the text block is index 0 and no thinking block is started; indices are assigned in emission order with no gaps - If a response has
reasoning_contentand never anycontent, the thinking block is still closed beforemessage_delta message_deltacarriesstop_reasonandusage.output_tokens; upstreamfinish_reasonstopbecomesend_turnandlengthbecomesmax_tokensmessage_stopis the final frame and is emitted exactly once per response- Translation is streaming and unbuffered: each upstream chunk is converted and flushed as it arrives, and the response is never accumulated to be inspected
- A client disconnect mid-stream cancels the upstream request immediately and releases the slot, rather than orphaning the generation
- An upstream failure after
message_startterminates the stream with an error frame rather than a truncated but apparently successful sequence - The OpenAI
data: [DONE]sentinel is consumed by the translator and never forwarded to a/llmclient
An orphaned generation holds one of only eight vLLM sequence slots in the cluster, which is why disconnect cancellation is an acceptance criterion here and not only in the proxy layer.
Verify
curl -N -s localhost:8080/llm/v1/messages -H 'content-type: application/json' \
-d '{"model":"reasoning","max_tokens":64,"stream":true,"messages":[{"role":"user","content":"hi"}]}' \
| grep '^event:'
# expected: message_start, content_block_start, content_block_delta..., content_block_stop,
# content_block_start, content_block_delta..., content_block_stop, message_delta, message_stop
curl -N -s localhost:8080/llm/v1/messages -H 'content-type: application/json' \
-d '{"model":"reasoning","max_tokens":64,"stream":true,"messages":[{"role":"user","content":"hi"}]}' \
| grep -n -E 'content_block_stop|"index":1' | head -3
# expected: the index 0 content_block_stop line precedes the first line mentioning index 1
timeout 1 curl -N -s localhost:8080/llm/v1/messages -H 'content-type: application/json' \
-d '{"model":"reasoning","max_tokens":512,"stream":true,"messages":[{"role":"user","content":"long"}]}' >/dev/null
grep -c 'cancelled' /tmp/stub-reasoning.log
# expected: 1 within a second of the client going away