fix(antigravity): improve streaming retry logic and implement true SSE streaming

- Add isRetryableResponse() to detect SUBSCRIPTION_REQUIRED 403 errors for retry handling
- Remove JSDoc comments from isRetryableError() for clarity
- Add debug logging for request/response details (streaming flag, status, content-type)
- Refactor transformStreamingResponse() to use TransformStream for true streaming
  - Replace buffering approach with incremental chunk processing
  - Implement createSseTransformStream() for line-by-line transformation
  - Reduces memory footprint and Time-To-First-Byte (TTFB)
- Update SSE content-type detection to include alt=sse URL parameter
- Simplify response transformation logic for non-streaming path
- Add more granular debug logging for thought signature extraction

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
YeonGyu-Kim
2025-12-13 04:50:11 +09:00
parent abd90bbc9c
commit 5608bd0ef9
2 changed files with 96 additions and 77 deletions

View File

@@ -339,31 +339,39 @@ export function transformStreamingPayload(payload: string): string {
.join("\n")
}
function createSseTransformStream(): TransformStream<Uint8Array, Uint8Array> {
const decoder = new TextDecoder()
const encoder = new TextEncoder()
let buffer = ""
return new TransformStream({
transform(chunk, controller) {
buffer += decoder.decode(chunk, { stream: true })
const lines = buffer.split("\n")
buffer = lines.pop() || ""
for (const line of lines) {
const transformed = transformSseLine(line)
controller.enqueue(encoder.encode(transformed + "\n"))
}
},
flush(controller) {
if (buffer) {
const transformed = transformSseLine(buffer)
controller.enqueue(encoder.encode(transformed))
}
},
})
}
/**
* Transforms a streaming SSE response from Antigravity to OpenAI format.
*
* **⚠️ CURRENT IMPLEMENTATION: BUFFERING**
* This implementation reads the entire stream into memory before transforming.
* While functional, it does not preserve true streaming characteristics:
* - Blocks until entire response is received
* - Consumes memory proportional to response size
* - Increases Time-To-First-Byte (TTFB)
*
* **TODO: Future Enhancement**
* Implement true streaming using ReadableStream transformation:
* - Parse SSE chunks incrementally
* - Transform and yield chunks as they arrive
* - Reduce memory footprint and TTFB
*
* For streaming responses (current buffered approach):
* - Unwraps the `response` field from each SSE event
* - Returns transformed SSE text as new Response
* - Extracts usage metadata from headers
*
* Note: Does NOT handle thinking block extraction (Task 10)
* Uses TransformStream to process SSE chunks incrementally as they arrive.
* Each line is transformed immediately and yielded to the client.
*
* @param response - The SSE response from Antigravity API
* @returns TransformResult with transformed response and metadata
* @returns TransformResult with transformed streaming response
*/
export async function transformStreamingResponse(response: Response): Promise<TransformResult> {
const headers = new Headers(response.headers)
@@ -402,7 +410,8 @@ export async function transformStreamingResponse(response: Response): Promise<Tr
// Check content type
const contentType = response.headers.get("content-type") ?? ""
const isEventStream = contentType.includes("text/event-stream")
const isEventStream =
contentType.includes("text/event-stream") || response.url.includes("alt=sse")
if (!isEventStream) {
// Not SSE, delegate to non-streaming transform
@@ -434,24 +443,25 @@ export async function transformStreamingResponse(response: Response): Promise<Tr
}
}
// Handle SSE stream
// NOTE: Current implementation buffers entire stream - see JSDoc for details
try {
const text = await response.text()
const transformed = transformStreamingPayload(text)
return {
response: new Response(transformed, {
status: response.status,
statusText: response.statusText,
headers,
}),
usage,
}
} catch {
// If reading fails, return original response
if (!response.body) {
return { response, usage }
}
headers.delete("content-length")
headers.delete("content-encoding")
headers.set("content-type", "text/event-stream; charset=utf-8")
const transformStream = createSseTransformStream()
const transformedBody = response.body.pipeThrough(transformStream)
return {
response: new Response(transformedBody, {
status: response.status,
statusText: response.statusText,
headers,
}),
usage,
}
}
/**
@@ -462,7 +472,7 @@ export async function transformStreamingResponse(response: Response): Promise<Tr
*/
export function isStreamingResponse(response: Response): boolean {
const contentType = response.headers.get("content-type") ?? ""
return contentType.includes("text/event-stream")
return contentType.includes("text/event-stream") || response.url.includes("alt=sse")
}
/**