@ai-sdk/google
Version:
The **[Google Generative AI provider](https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai)** for the [AI SDK](https://ai-sdk.dev/docs) contains language model support for the [Google Generative AI](https://ai.google/discover/generativeai/)
130 lines (116 loc) • 4.11 kB
text/typescript
import {
createJsonResponseHandler,
delay,
getFromApi,
isAbortError,
type FetchFunction,
} from '@ai-sdk/provider-utils';
import { googleFailedResponseHandler } from '../google-error';
import { cancelGoogleInteraction } from './cancel-google-interaction';
import {
googleInteractionsResponseSchema,
type GoogleInteractionsResponse,
} from './google-interactions-api';
import type { GoogleInteractionsStatus } from './google-interactions-prompt';
const TERMINAL_STATUSES: ReadonlySet<GoogleInteractionsStatus | string> =
new Set(['completed', 'failed', 'cancelled', 'incomplete']);
export function isTerminalStatus(
status: GoogleInteractionsStatus | string | null | undefined,
): boolean {
return status != null && TERMINAL_STATUSES.has(status);
}
/*
* Default polling cadence for background interactions. Starts at 1 s, doubles
* each tick up to a 10 s ceiling, and gives up after 30 minutes -- agent runs
* such as deep research can take tens of minutes server-side, so we err on
* the long side rather than truncate a real run. Override per-call via
* `providerOptions.google.pollingTimeoutMs`.
*/
const DEFAULT_INITIAL_DELAY_MS = 1000;
const DEFAULT_MAX_DELAY_MS = 10000;
const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;
export type PollGoogleInteractionResult = {
response: GoogleInteractionsResponse;
rawResponse: unknown;
responseHeaders: Record<string, string> | undefined;
};
/**
* Polls `GET {baseURL}/interactions/{id}` until the response status is
* terminal (`completed` / `failed` / `cancelled` / `incomplete`). Throws if
* the polling loop exceeds `timeoutMs`, the response has no `id` to poll on,
* or the abort signal fires.
*/
export async function pollGoogleInteractionUntilTerminal({
baseURL,
interactionId,
headers,
fetch,
abortSignal,
initialDelayMs = DEFAULT_INITIAL_DELAY_MS,
maxDelayMs = DEFAULT_MAX_DELAY_MS,
timeoutMs = DEFAULT_TIMEOUT_MS,
}: {
baseURL: string;
interactionId: string | null | undefined;
headers: Record<string, string | undefined>;
fetch?: FetchFunction;
abortSignal?: AbortSignal;
initialDelayMs?: number;
maxDelayMs?: number;
timeoutMs?: number;
}): Promise<PollGoogleInteractionResult> {
if (interactionId == null || interactionId.length === 0) {
throw new Error(
'google.interactions: cannot poll a background interaction without an id. ' +
'The POST response did not include an interaction id.',
);
}
const startedAt = Date.now();
let nextDelayMs = initialDelayMs;
const url = `${baseURL}/interactions/${encodeURIComponent(interactionId)}`;
/*
* When the caller aborts, fire a best-effort `POST /interactions/{id}/cancel`
* so the run stops billing on Google's side. Wrap every exit path that's
* triggered by an abort -- the explicit `abortSignal.aborted` check, the
* AbortError thrown by `delay()`, and any AbortError thrown by `getFromApi`.
*/
const cancelOnServer = () =>
cancelGoogleInteraction({ baseURL, interactionId, headers, fetch });
try {
while (true) {
if (abortSignal?.aborted) {
await cancelOnServer();
throw new DOMException('Polling was aborted', 'AbortError');
}
if (Date.now() - startedAt > timeoutMs) {
throw new Error(
`google.interactions: timed out polling interaction ${interactionId} after ${timeoutMs}ms.`,
);
}
await delay(nextDelayMs, { abortSignal });
const {
value: response,
rawValue: rawResponse,
responseHeaders,
} = await getFromApi({
url,
headers,
failedResponseHandler: googleFailedResponseHandler,
successfulResponseHandler: createJsonResponseHandler(
googleInteractionsResponseSchema,
),
abortSignal,
fetch,
});
if (isTerminalStatus(response.status)) {
return { response, rawResponse, responseHeaders };
}
nextDelayMs = Math.min(nextDelayMs * 2, maxDelayMs);
}
} catch (error) {
if (isAbortError(error)) {
await cancelOnServer();
}
throw error;
}
}