|
|
@@ -46,6 +46,8 @@ import { GoogleAuth } from "google-auth-library"
|
|
|
import { ProviderTransform } from "./transform"
|
|
|
import { Installation } from "../installation"
|
|
|
|
|
|
+const DEFAULT_CHUNK_TIMEOUT = 120_000
|
|
|
+
|
|
|
export namespace Provider {
|
|
|
const log = Log.create({ service: "provider" })
|
|
|
|
|
|
@@ -85,6 +87,54 @@ export namespace Provider {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+ function wrapSSE(res: Response, ms: number, ctl: AbortController) {
|
|
|
+ if (typeof ms !== "number" || ms <= 0) return res
|
|
|
+ if (!res.body) return res
|
|
|
+ if (!res.headers.get("content-type")?.includes("text/event-stream")) return res
|
|
|
+
|
|
|
+ const reader = res.body.getReader()
|
|
|
+ const body = new ReadableStream<Uint8Array>({
|
|
|
+ async pull(ctrl) {
|
|
|
+ const part = await new Promise<Awaited<ReturnType<typeof reader.read>>>((resolve, reject) => {
|
|
|
+ const id = setTimeout(() => {
|
|
|
+ const err = new Error("SSE read timed out")
|
|
|
+ ctl.abort(err)
|
|
|
+ void reader.cancel(err)
|
|
|
+ reject(err)
|
|
|
+ }, ms)
|
|
|
+
|
|
|
+ reader.read().then(
|
|
|
+ (part) => {
|
|
|
+ clearTimeout(id)
|
|
|
+ resolve(part)
|
|
|
+ },
|
|
|
+ (err) => {
|
|
|
+ clearTimeout(id)
|
|
|
+ reject(err)
|
|
|
+ },
|
|
|
+ )
|
|
|
+ })
|
|
|
+
|
|
|
+ if (part.done) {
|
|
|
+ ctrl.close()
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ ctrl.enqueue(part.value)
|
|
|
+ },
|
|
|
+ async cancel(reason) {
|
|
|
+ ctl.abort(reason)
|
|
|
+ await reader.cancel(reason)
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ return new Response(body, {
|
|
|
+ headers: new Headers(res.headers),
|
|
|
+ status: res.status,
|
|
|
+ statusText: res.statusText,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
const BUNDLED_PROVIDERS: Record<string, (options: any) => SDK> = {
|
|
|
"@ai-sdk/amazon-bedrock": createAmazonBedrock,
|
|
|
"@ai-sdk/anthropic": createAnthropic,
|
|
|
@@ -1092,21 +1142,23 @@ export namespace Provider {
|
|
|
if (existing) return existing
|
|
|
|
|
|
const customFetch = options["fetch"]
|
|
|
+ const chunkTimeout = options["chunkTimeout"] || DEFAULT_CHUNK_TIMEOUT
|
|
|
+ delete options["chunkTimeout"]
|
|
|
|
|
|
options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
|
|
|
// Preserve custom fetch if it exists, wrap it with timeout logic
|
|
|
const fetchFn = customFetch ?? fetch
|
|
|
const opts = init ?? {}
|
|
|
+ const chunkAbortCtl = typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined
|
|
|
+ const signals: AbortSignal[] = []
|
|
|
|
|
|
- if (options["timeout"] !== undefined && options["timeout"] !== null) {
|
|
|
- const signals: AbortSignal[] = []
|
|
|
- if (opts.signal) signals.push(opts.signal)
|
|
|
- if (options["timeout"] !== false) signals.push(AbortSignal.timeout(options["timeout"]))
|
|
|
-
|
|
|
- const combined = signals.length > 1 ? AbortSignal.any(signals) : signals[0]
|
|
|
+ if (opts.signal) signals.push(opts.signal)
|
|
|
+ if (chunkAbortCtl) signals.push(chunkAbortCtl.signal)
|
|
|
+ if (options["timeout"] !== undefined && options["timeout"] !== null && options["timeout"] !== false)
|
|
|
+ signals.push(AbortSignal.timeout(options["timeout"]))
|
|
|
|
|
|
- opts.signal = combined
|
|
|
- }
|
|
|
+ const combined = signals.length === 0 ? null : signals.length === 1 ? signals[0] : AbortSignal.any(signals)
|
|
|
+ if (combined) opts.signal = combined
|
|
|
|
|
|
// Strip openai itemId metadata following what codex does
|
|
|
// Codex uses #[serde(skip_serializing)] on id fields for all item types:
|
|
|
@@ -1126,11 +1178,14 @@ export namespace Provider {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- return fetchFn(input, {
|
|
|
+ const res = await fetchFn(input, {
|
|
|
...opts,
|
|
|
// @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
|
|
|
timeout: false,
|
|
|
})
|
|
|
+
|
|
|
+ if (!chunkAbortCtl) return res
|
|
|
+ return wrapSSE(res, chunkTimeout, chunkAbortCtl)
|
|
|
}
|
|
|
|
|
|
const bundledFn = BUNDLED_PROVIDERS[model.api.npm]
|