Skip to content

test: make the Cohere streaming drift leg compare real event frames - #528

Merged
jpr5 merged 2 commits into
mainfrom
fix/drift-cohere-stream-shape
Oct 11, 2026
Merged

jpr5 merged 2 commits into
mainfrom
fix/drift-cohere-stream-shape

Conversation

@jpr5

@jpr5 jpr5 commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The streaming /v2/chat shape leg in cohere.drift.ts never compared anything. It parsed both bodies with parseDataOnlySSE, which keeps only frames that start with data: . Every Cohere v2 frame is event: <type> + data: <json>, so both lists came out empty, the if (realChunks.length > 0 && mockChunks.length > 0) guard was false, and the test always passed. (A non-200 mock reply was also let through without a comparison.)

Fix (test-only; no src/ behavior change)

  • parseCohereSSE: strict parser. Each frame must be exactly event: + data: with data.type equal to the event name. A trailing data: [DONE] is dropped. Any other frame throws instead of being dropped.
  • The leg now requires mock status 200 and non-empty event lists on both sides, opening with message-start and closing with message-end. It compares every event type with compareSSESequences against SDK shapes for message-start / content-start / content-delta / content-end / message-end (the same pattern the Anthropic stream leg uses).
  • Live no-generation: the 422 envelope skip is unchanged. A 200 stream whose message-end is the no-generation terminal (finish_reason: "ERROR", empty usage) is now a skip that gives the upstream code and id (NO_VALID_RESPONSE_GENERATED id=...), the same as test(drift): accept Cohere upstream no-generation outcomes on the live leg #525. isCohereNoGenerationStreamEnd now takes the allowed error types, like cohereNoGenerationError. K5 passes COHERE_K5_NO_GENERATION, so K5 behavior is unchanged.
  • Offline tests: the parser on the recorded live K5 stream fixture, on aimock's real stream, and rejection of malformed frames. Classifier test for the allowed param.

Local RED (current main, live Cohere)

Temporary probe in the leg:

RED-PROBE realChunks=0 mockChunks=0 realBytes=1607 mockBytes=709
 ✓ Cohere drift > streaming /v2/chat shape matches

Mutant in src/cohere.ts (text stream message-end: finish_reason renamed to finishReason). The test still passes:

RED-PROBE realChunks=0 mockChunks=0 realBytes=1607 mockBytes=708
      Tests  2 passed | 24 skipped (26)

Local GREEN (this branch, live Cohere)

GREEN-PROBE realEvents=13 [message-start,content-start,content-delta x9,content-end,message-end] mockEvents=5 [message-start,content-start,content-delta,content-end,message-end]

Two consecutive live runs, probe removed:

run 1: ✓ Cohere drift > streaming /v2/chat shape matches  304ms
run 2: ✓ Cohere drift > streaming /v2/chat shape matches  317ms

The same finishReason mutant now fails:

× Cohere drift > streaming /v2/chat shape matches
  [critical] LLMOCK DRIFT — field in SDK + real API but missing from mock
     Path:    SSE:message-end.delta.finish_reason

A second mutant (drop the content-end event) also fails:

  [critical] LLMOCK DRIFT — real API emits event type "content-end" but mock does not

Both mutants reverted.

Non-critical differences now visible (not fixed here)

These come from the comparison, but the existing triangulation rules do not rate them critical: live usage.cached_tokens, live content-start carries text: "", and aimock adds content.type on deltas plus billed_units.search_units/classifications. Outside the shape comparison: live ends with data: [DONE] and aimock's Cohere stream does not.

Gates

pnpm typecheck, pnpm lint, pnpm format:check, pnpm build, pnpm test (377 files, 14080 passed) all pass.

Follow-up: parse by the SSE spec (review finding)

parseCohereSSE threw on legal SSE: a single trailing \n, event:X with no space, : keepalive comments, and id:/retry: lines. It now follows the WHATWG event-stream rules: any line ending (\n, \r\n, \r), the space after the colon is optional, comments and id/retry/unknown fields are ignored, multi-line data is joined, and a final event is dispatched without a trailing blank line. These still throw: data that is not JSON, data that is not an object, an event name that does not match data.type (an event without event: is named message by the spec), and any event after [DONE].

RED: the new offline tests run against the previous parser:

× parseCohereSSE > accepts legal SSE variants a server or proxy may send
  Error: Unsupported Cohere SSE frame: "event: message-end\ndata: {...}\n"
× parseCohereSSE > rejects malformed data, mismatched names and events after [DONE]

GREEN: all 4 parser tests pass. Live streaming leg passed 2 consecutive times. The finishReason mutant still fails on SSE:message-end.delta.finish_reason. Gates pass again (377 files, 14080 tests).

The streaming /v2/chat shape leg parsed both bodies with parseDataOnlySSE,
which drops every frame that has an `event:` line. Cohere v2 streams are
all `event:` + `data:` frames, so both chunk lists were empty and the
comparison never ran.

Parse the frames with a strict Cohere parser (event name must equal
data.type, trailing `data: [DONE]` dropped, any other frame throws),
require non-empty streams that open with message-start and close with
message-end, and compare every event type with compareSSESequences.
A live stream that ends in the no-generation terminal (finish_reason
ERROR, empty usage) is a skip that cites the upstream code and id, the
same way the 422 envelope already is. isCohereNoGenerationStreamEnd now
takes the allowed error types, like cohereNoGenerationError.
@pkg-pr-new

pkg-pr-new Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@copilotkit/aimock@528

commit: 32bd1cb

parseCohereSSE threw on legal SSE a server or proxy can send: a single
trailing newline, no space after the field colon, `:` keepalive
comments, and id/retry fields. That would raise a false drift alert.

Parse line by line per the WHATWG event-stream rules: any line ending,
blank line dispatches, comments and id/retry/unknown fields ignored,
optional space after the colon, multi-line data joined, final event
dispatched without a trailing blank line. Malformed data JSON, a
non-object payload, an event/type mismatch and events after [DONE]
still throw.
@jpr5
jpr5 merged commit 75e04da into main Oct 11, 2026
33 checks passed
@jpr5
jpr5 deleted the fix/drift-cohere-stream-shape branch October 11, 2026 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant