mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
fix(ai): ignore Vertex keepalives sent as SSE data (#50954)
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user