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 c84bd6cedb
commit cef2036575
2 changed files with 96 additions and 77 deletions
+48 -39
View File
@@ -63,16 +63,24 @@ function debugLog(message: string): void {
}
}
/**
* Check if an error is a retryable network/server error
*/
function isRetryableError(status: number): boolean {
// 4xx client errors (except 429 rate limit) are not retryable
// 5xx server errors are retryable
// Network errors (status 0) are retryable
if (status === 0) return true // Network error
if (status === 429) return true // Rate limit
if (status >= 500 && status < 600) return true // Server errors
if (status === 0) return true
if (status === 429) return true
if (status >= 500 && status < 600) return true
return false
}
async function isRetryableResponse(response: Response): Promise<boolean> {
if (isRetryableError(response.status)) return true
if (response.status === 403) {
try {
const text = await response.clone().text()
if (text.includes("SUBSCRIPTION_REQUIRED") || text.includes("Gemini Code Assist license")) {
debugLog(`[RETRY] 403 SUBSCRIPTION_REQUIRED detected, will retry with next endpoint`)
return true
}
} catch {}
}
return false
}
@@ -145,6 +153,8 @@ async function attemptFetch(
thoughtSignature,
})
debugLog(`[REQ] streaming=${transformed.streaming}, url=${transformed.url}`)
const response = await fetch(transformed.url, {
method: init.method || "POST",
headers: transformed.headers,
@@ -152,7 +162,11 @@ async function attemptFetch(
signal: init.signal,
})
if (!response.ok && isRetryableError(response.status)) {
debugLog(
`[RESP] status=${response.status} content-type=${response.headers.get("content-type") ?? ""} url=${response.url}`
)
if (!response.ok && (await isRetryableResponse(response))) {
debugLog(`Endpoint failed: ${endpoint} (status: ${response.status}), trying next`)
return null
}
@@ -223,41 +237,36 @@ async function transformResponseWithThinking(
result = await transformResponse(response)
}
if (streaming) {
return result.response
}
try {
const text = await result.response.clone().text()
debugLog(`[TSIG][RESP] Response text length: ${text.length}`)
if (streaming) {
const signature = extractSignatureFromSsePayload(text)
debugLog(`[TSIG][RESP] SSE signature extracted: ${signature ? "yes" : "no"}`)
if (signature) {
setThoughtSignature(fetchInstanceId, signature)
debugLog(`[TSIG][STORE] Stored signature for ${fetchInstanceId}: ${signature.substring(0, 30)}...`)
}
const parsed = JSON.parse(text) as GeminiResponseBody
debugLog(`[TSIG][RESP] Parsed keys: ${Object.keys(parsed).join(", ")}`)
debugLog(`[TSIG][RESP] Has candidates: ${!!parsed.candidates}, count: ${parsed.candidates?.length ?? 0}`)
const signature = extractSignatureFromResponse(parsed)
debugLog(`[TSIG][RESP] Signature extracted: ${signature ? signature.substring(0, 30) + "..." : "NONE"}`)
if (signature) {
setThoughtSignature(fetchInstanceId, signature)
debugLog(`[TSIG][STORE] Stored signature for ${fetchInstanceId}`)
} else {
const parsed = JSON.parse(text) as GeminiResponseBody
debugLog(`[TSIG][RESP] Parsed keys: ${Object.keys(parsed).join(", ")}`)
debugLog(`[TSIG][RESP] Has candidates: ${!!parsed.candidates}, count: ${parsed.candidates?.length ?? 0}`)
debugLog(`[TSIG][WARN] No signature found in response!`)
}
const signature = extractSignatureFromResponse(parsed)
debugLog(`[TSIG][RESP] Signature extracted: ${signature ? signature.substring(0, 30) + "..." : "NONE"}`)
if (signature) {
setThoughtSignature(fetchInstanceId, signature)
debugLog(`[TSIG][STORE] Stored signature for ${fetchInstanceId}`)
} else {
debugLog(`[TSIG][WARN] No signature found in response!`)
}
if (shouldIncludeThinking(modelName)) {
const thinkingResult = extractThinkingBlocks(parsed)
if (thinkingResult.hasThinking) {
const transformed = transformResponseThinking(parsed)
return new Response(JSON.stringify(transformed), {
status: result.response.status,
statusText: result.response.statusText,
headers: result.response.headers,
})
}
if (shouldIncludeThinking(modelName)) {
const thinkingResult = extractThinkingBlocks(parsed)
if (thinkingResult.hasThinking) {
const transformed = transformResponseThinking(parsed)
return new Response(JSON.stringify(transformed), {
status: result.response.status,
statusText: result.response.statusText,
headers: result.response.headers,
})
}
}
} catch {}