fix(ai): ignore Vertex keepalives sent as SSE data (#50954)

This commit is contained in:
Aiden Cline
2026-09-23 13:25:24 -05:00
committed by GitHub
parent e64e4d0fbb
commit ad15173aa7
3 changed files with 48 additions and 3 deletions
+6 -3
View File
@@ -253,9 +253,9 @@ export const errorText = (error: unknown) => {
/**
* `framing` step for Server-Sent Events. Decodes UTF-8, runs the SSE channel
* decoder, optionally filters named events, and drops empty and bare `null`
* events. `[DONE]` is dropped by default or retained for protocols that use it
* as their stream boundary. Retry control events are ignored without
* decoder, optionally filters named events, and drops empty events and known
* keepalives that proxies send as data. `[DONE]` is dropped by default or
* retained for protocols that use it as their stream boundary. Retry control events are ignored without
* interrupting the stream. Decoder failures become provider output errors so
* the public error channel stays `AIError`.
*/
@@ -291,6 +291,9 @@ export const sseFraming = (
// `data: null`, between events or after `[DONE]`. No protocol has a
// null event, so it carries nothing and must not abort the stream.
event.data !== "null" &&
// Vertex AI partner models (e.g. `xai/grok-4.6`) send their SSE
// keepalive comment as `data: : keepalive` while reasoning.
event.data !== ": keepalive" &&
(event.data !== "[DONE]" || includeDone || (events !== undefined && event.event !== "message")),
),
Stream.map((event) => event.data),
@@ -343,6 +343,34 @@ describe("Google Vertex providers", () => {
}),
)
// Captured from xai/grok-4.6 on Vertex: one keepalive every 15s until the first token.
it.effect("ignores keepalives sent as data while a partner model reasons", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLM.request({
model: GoogleVertexChat.configure({
accessToken: "vertex-token",
location: "global",
project: "vertex-project",
}).model("xai/grok-4.6"),
prompt: "Say hello.",
}),
).pipe(
Effect.provide(
fixedResponse(
`data: : keepalive\n\ndata: : keepalive\n\n${sseEvents(
deltaChunk({ role: "assistant", content: "Hello." }),
finishChunk("stop"),
)}`,
),
),
)
expect(response.text).toBe("Hello.")
expect(response.finishReason).toEqual({ normalized: "stop", raw: "stop" })
}),
)
it.effect("sends Grok requests through Vertex Responses", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
+14
View File
@@ -192,6 +192,20 @@ describe("AI.Usage", () => {
}),
)
it.effect("sseFraming drops keepalive comments sent as data and keeps other payloads", () =>
Effect.gen(function* () {
const frames = yield* ProviderShared.sseFraming(
Stream.make(
new TextEncoder().encode(
'data: {"first":true}\n\ndata: : keepalive\n\n: keepalive\n\ndata: : ping\n\ndata: {"second":true}\n\n',
),
),
).pipe(Stream.runCollect)
expect(Array.from(frames)).toEqual(['{"first":true}', ": ping", '{"second":true}'])
}),
)
test("visibleOutputTokens clamps reasoning > output to zero", () => {
expect(new Usage({ outputTokens: 10, reasoningTokens: 4 }).visibleOutputTokens).toBe(6)
expect(new Usage({ outputTokens: 10 }).visibleOutputTokens).toBe(10)