@ai-sdk/google
Version:
9,018 lines • 327 kB
JavaScript
import { EXPERIMENTAL_EMBEDDING_MODEL_PROVIDER_OPTIONS_TRANSFORMER, WORKFLOW_DESERIALIZE, WORKFLOW_SERIALIZE, combineHeaders, connectToWebSocket, convertAsyncIteratorToReadableStream, convertBase64ToUint8Array, convertInlineFileDataToUint8Array, convertJsonResponseToolStream, convertToBase64, convertUint8ArrayToBase64, createEventSourceResponseHandler, createJsonErrorResponseHandler, createJsonLinesResponseHandler, createJsonResponseHandler, createNullLanguageModelUsage, createProviderExecutedToolFactory, createProviderStreamError, createToolNameMapping, delay, detectMediaType, downloadBlob, encodePathSegment, generateId, getFromApi, getRuntimeEnvironmentUserAgent, getTopLevelMediaType, isAbortError, isCustomReasoning, isFullMediaType, isRecord, isSameOrigin, isUrlSupported, lazySchema, loadApiKey, mapReasoningToProviderBudget, mapReasoningToProviderEffort, normalizeBatchRequestCounts, parseProviderOptions, postJsonToApi, postToApi, removeUndefinedEntries, resolve, resolveFullMediaType, resolveProviderOptions, resolveProviderReference, resolveWith, safeParseJSON, safeValidateTypes, secureJsonParse, serializeModelOptions, stringifyWithBigIntAndCycles, waitForWebSocketBufferDrain, withUserAgentSuffix, withoutTrailingSlash, zodSchema } from "@ai-sdk/provider-utils";
import { Experimental_ExtractionLanguageModel } from "@ai-sdk/provider-utils/experimental-extraction";
import { AISDKError, Experimental_DecisionUnsupportedQuestionTypeError, InvalidArgumentError, InvalidResponseDataError, TooManyEmbeddingValuesForCallError, UnsupportedFunctionalityError } from "@ai-sdk/provider";
import { z } from "zod/v4";
//#region src/google-decision-api.ts
const googleDecisionResponseSchema = lazySchema(() => {
const probability = z.number().min(0).max(1);
return zodSchema(z.object({
answers: z.record(z.string(), z.discriminatedUnion("type", [
z.object({
type: z.literal("noul"),
noul: probability,
confidence: probability.nullish()
}),
z.object({
type: z.literal("choice"),
choice: z.string(),
probabilities: z.record(z.string(), probability),
confidence: probability.nullish()
}),
z.object({
type: z.literal("score"),
score: z.number(),
probabilities: z.record(z.string(), probability),
confidence: probability.nullish(),
legend: z.record(z.string(), z.string()).nullish()
})
])).nullish(),
usage: z.object({
total_input_tokens: z.number().nullish(),
total_output_tokens: z.number().nullish(),
total_tokens: z.number().nullish(),
input_tokens_by_modality: z.array(z.object({
modality: z.string(),
tokens: z.number()
})).nullish()
}).nullish(),
prompt_feedback: z.object({
block_reason: z.string().nullish(),
block_reason_message: z.string().nullish()
}).nullish()
}));
});
//#endregion
//#region src/google-error.ts
const getGoogleErrorSchema = () => z.object({
code: z.number().nullable(),
message: z.string(),
status: z.string(),
details: z.array(z.unknown()).nullish()
});
const googleErrorDataSchema = lazySchema(() => zodSchema(z.object({ error: getGoogleErrorSchema() })));
const googleFailedResponseHandler = createJsonErrorResponseHandler({
errorSchema: googleErrorDataSchema,
errorToMessage: (data) => data.error.message
});
function mapGoogleErrorStatusToHttpStatusCode(status) {
switch (status) {
case "CANCELLED": return 499;
case "INVALID_ARGUMENT":
case "FAILED_PRECONDITION":
case "OUT_OF_RANGE": return 400;
case "DEADLINE_EXCEEDED": return 504;
case "NOT_FOUND": return 404;
case "ALREADY_EXISTS":
case "ABORTED": return 409;
case "PERMISSION_DENIED": return 403;
case "UNAUTHENTICATED": return 401;
case "RESOURCE_EXHAUSTED": return 429;
case "UNIMPLEMENTED": return 501;
case "UNKNOWN":
case "INTERNAL":
case "DATA_LOSS": return 500;
case "UNAVAILABLE": return 503;
default: return;
}
}
//#endregion
//#region src/google-decision-model.ts
var DecisionGoogleModel = class DecisionGoogleModel {
constructor(modelId, config) {
this.modelId = modelId;
this.config = config;
this.specificationVersion = "v4";
this.supportedQuestionTypes = [
"choice",
"score",
"boolean"
];
}
get provider() {
return this.config.provider;
}
static [WORKFLOW_SERIALIZE](model) {
return serializeModelOptions({
modelId: model.modelId,
config: model.config
});
}
static [WORKFLOW_DESERIALIZE](options) {
return new DecisionGoogleModel(options.modelId, options.config);
}
/** @deprecated Use `doDecide` instead. */
doEvaluate(options) {
return this.doDecide(options);
}
async doDecide({ state, questions, maxOutputTokens, reasoning, abortSignal, headers, providerOptions }) {
abortSignal?.throwIfAborted();
const entries = Object.entries(questions);
if (entries.length === 0) throw new InvalidArgumentError({
argument: "questions",
message: "Google Decisions requires at least one question."
});
const requestQuestions = Object.fromEntries(entries.map(([id, question]) => {
if (question.type === "box") throw new Experimental_DecisionUnsupportedQuestionTypeError({
questionId: id,
questionType: question.type,
provider: this.provider,
modelId: this.modelId
});
if (question.instructions == null) throw new InvalidArgumentError({
argument: `questions.${id}.instructions`,
message: "Google Decisions requires instructions for every question."
});
const instructions = toText(question.instructions);
if (question.type === "boolean") return [id, {
type: "noul",
instructions,
criteria: question.criteria == null ? void 0 : {
true_description: question.criteria.true == null ? void 0 : toText(question.criteria.true),
false_description: question.criteria.false == null ? void 0 : toText(question.criteria.false)
}
}];
const count = Object.keys(question.criteria).length;
if (count < 2 || count > 255) throw new InvalidArgumentError({
argument: `questions.${id}.criteria`,
message: "Google Decisions requires 2 to 255 options or score levels."
});
if (question.type === "score" && question.criteria.some((level) => level == null || level === "")) throw new InvalidArgumentError({
argument: `questions.${id}.criteria`,
message: "Google Decisions requires non-empty score level descriptions."
});
return [id, {
type: question.type,
instructions,
criteria: question.type === "choice" ? Object.fromEntries(Object.entries(question.criteria).map(([key, value]) => [key, value == null ? null : toText(value)])) : question.criteria.map((level) => toText(level))
}];
}));
const input = state.map((part) => {
if (part.type === "text") return {
type: "text",
text: part.text
};
if (part.type === "json") return {
type: "text",
text: JSON.stringify(part.value)
};
const type = part.mediaType === "application/pdf" ? "document" : part.mediaType.split("/")[0];
if (![
"image",
"audio",
"video",
"document"
].includes(type)) throw new UnsupportedFunctionalityError({ functionality: `Google decision file media type: ${part.mediaType}` });
if (part.data.type !== "data" && part.data.type !== "url") throw new UnsupportedFunctionalityError({ functionality: `Google decision file input: ${part.data.type}` });
return {
type,
mime_type: part.mediaType,
...part.data.type === "url" ? { uri: part.data.url.toString() } : { data: typeof part.data.data === "string" ? part.data.data : convertUint8ArrayToBase64(part.data.data) }
};
});
const { value: response, rawValue, responseHeaders } = await postJsonToApi({
url: `${this.config.baseURL}/decisions`,
headers: combineHeaders(await resolve(this.config.headers), headers),
body: {
model: this.modelId,
input,
questions: requestQuestions
},
failedResponseHandler: googleFailedResponseHandler,
successfulResponseHandler: createJsonResponseHandler(googleDecisionResponseSchema),
abortSignal,
fetch: this.config.fetch
});
abortSignal?.throwIfAborted();
const blocked = response.prompt_feedback?.block_reason != null;
const responseAnswers = response.answers;
if (!blocked && (responseAnswers == null || Object.keys(responseAnswers).length !== entries.length || entries.some(([id]) => !Object.prototype.hasOwnProperty.call(responseAnswers, id)))) throw new InvalidResponseDataError({
data: rawValue,
message: "Google Decisions must return exactly one answer for every question."
});
return {
answers: Object.fromEntries(entries.map(([id, question]) => {
if (blocked) return [id, { type: "refusal" }];
const answer = responseAnswers[id];
if (question.type === "boolean" && answer.type === "noul") return [id, {
type: "boolean",
probability: answer.noul
}];
if (question.type === "choice" && answer.type === "choice" && Object.prototype.hasOwnProperty.call(question.criteria, answer.choice)) return [id, {
type: "choice",
choice: answer.choice,
probabilities: answer.probabilities
}];
if (question.type === "score" && answer.type === "score" && answer.score >= 0 && answer.score <= question.criteria.length - 1) return [id, {
type: "score",
score: answer.score,
probabilities: answer.probabilities
}];
throw new InvalidResponseDataError({
data: rawValue,
message: `Google Decisions returned an invalid answer for question "${id}".`
});
})),
usage: response.usage == null ? void 0 : {
inputTokens: response.usage.total_input_tokens ?? void 0,
outputTokens: response.usage.total_output_tokens ?? void 0
},
warnings: [
...maxOutputTokens != null ? [{
type: "unsupported",
feature: "maxOutputTokens"
}] : [],
...reasoning != null ? [{
type: "unsupported",
feature: "reasoning"
}] : [],
...Object.keys(providerOptions?.google ?? {}).map((option) => ({
type: "unsupported",
feature: `providerOptions.google.${option}`
}))
],
providerMetadata: { google: {
...response.usage == null ? {} : { usage: response.usage },
...response.prompt_feedback == null ? {} : { promptFeedback: response.prompt_feedback },
confidence: Object.fromEntries(Object.entries(responseAnswers ?? {}).flatMap(([id, answer]) => answer.confidence == null ? [] : [[id, answer.confidence]])),
legend: Object.fromEntries(Object.entries(responseAnswers ?? {}).flatMap(([id, answer]) => answer.type === "score" && answer.legend != null ? [[id, answer.legend]] : []))
} },
response: {
modelId: this.modelId,
headers: responseHeaders,
body: rawValue
}
};
}
};
function toText(input) {
return typeof input === "string" ? input : JSON.stringify(input);
}
//#endregion
//#region src/version.ts
const VERSION = "4.0.121";
//#endregion
//#region src/google-embedding-model-options.ts
const googleEmbeddingContentPartSchema = () => z.union([
z.object({ text: z.string() }),
z.object({ inlineData: z.object({
mimeType: z.string(),
data: z.string()
}) }),
z.object({ fileData: z.object({
fileUri: z.string(),
mimeType: z.string()
}) })
]);
const googleEmbeddingModelOptions = lazySchema(() => zodSchema(z.object({
/**
* Optional. Optional reduced dimension for the output embedding.
* If set, excessive values in the output embedding are truncated from the end.
*/
outputDimensionality: z.number().optional(),
/**
* Optional. Specifies the task type for generating embeddings.
* Supported task types:
* - SEMANTIC_SIMILARITY: Optimized for text similarity.
* - CLASSIFICATION: Optimized for text classification.
* - CLUSTERING: Optimized for clustering texts based on similarity.
* - RETRIEVAL_DOCUMENT: Optimized for document retrieval.
* - RETRIEVAL_QUERY: Optimized for query-based retrieval.
* - QUESTION_ANSWERING: Optimized for answering questions.
* - FACT_VERIFICATION: Optimized for verifying factual information.
* - CODE_RETRIEVAL_QUERY: Optimized for retrieving code blocks based on natural language queries.
*/
taskType: z.enum([
"SEMANTIC_SIMILARITY",
"CLASSIFICATION",
"CLUSTERING",
"RETRIEVAL_DOCUMENT",
"RETRIEVAL_QUERY",
"QUESTION_ANSWERING",
"FACT_VERIFICATION",
"CODE_RETRIEVAL_QUERY"
]).optional(),
/**
* Optional. Per-value multimodal content parts for embedding non-text
* content (images, video, PDF, audio). Each entry corresponds to the
* embedding value at the same index and its parts are merged with the
* text value in the request. Use `null` for entries that are text-only.
*
* The array length must match the number of values being embedded. In
* the case of a single embedding, the array length must be 1.
*/
content: z.array(z.array(googleEmbeddingContentPartSchema()).min(1).nullable()).optional()
})));
//#endregion
//#region src/google-embedding-model.ts
var GoogleEmbeddingModel = class GoogleEmbeddingModel {
static [WORKFLOW_SERIALIZE](model) {
return serializeModelOptions({
modelId: model.modelId,
config: model.config
});
}
static [WORKFLOW_DESERIALIZE](options) {
return new GoogleEmbeddingModel(options.modelId, options.config);
}
get provider() {
return this.config.provider;
}
constructor(modelId, config) {
this.specificationVersion = "v4";
this.maxEmbeddingsPerCall = 100;
this.supportsParallelCalls = true;
this[EXPERIMENTAL_EMBEDDING_MODEL_PROVIDER_OPTIONS_TRANSFORMER] = ({ providerOptions, values, startIndex, endIndex }) => {
const multimodalContent = providerOptions?.google?.content;
if (!Array.isArray(multimodalContent)) return providerOptions;
validateMultimodalContentLength({
multimodalContent,
values
});
return {
...providerOptions,
google: {
...providerOptions?.google,
content: multimodalContent.slice(startIndex, endIndex)
}
};
};
this.modelId = modelId;
this.config = config;
}
async doEmbed({ values, dimensions, headers, abortSignal, providerOptions }) {
const googleOptions = await parseProviderOptions({
provider: "google",
providerOptions,
schema: googleEmbeddingModelOptions
});
if (values.length > this.maxEmbeddingsPerCall) throw new TooManyEmbeddingValuesForCallError({
provider: this.provider,
modelId: this.modelId,
maxEmbeddingsPerCall: this.maxEmbeddingsPerCall,
values
});
const mergedHeaders = combineHeaders(this.config.headers ? await resolve(this.config.headers) : void 0, headers);
const multimodalContent = googleOptions?.content;
validateMultimodalContentLength({
multimodalContent,
values
});
if (values.length === 1) {
const valueParts = multimodalContent?.[0];
const textPart = values[0] ? [{ text: values[0] }] : [];
const parts = valueParts != null ? [...textPart, ...valueParts] : [{ text: values[0] }];
const { responseHeaders, value: response, rawValue } = await postJsonToApi({
url: `${this.config.baseURL}/models/${this.modelId}:embedContent`,
headers: mergedHeaders,
body: {
model: `models/${this.modelId}`,
content: { parts },
outputDimensionality: googleOptions?.outputDimensionality ?? dimensions,
taskType: googleOptions?.taskType
},
failedResponseHandler: googleFailedResponseHandler,
successfulResponseHandler: createJsonResponseHandler(googleGenerativeAISingleEmbeddingResponseSchema),
abortSignal,
fetch: this.config.fetch
});
return {
warnings: [],
embeddings: [response.embedding.values],
usage: void 0,
response: {
headers: responseHeaders,
body: rawValue
}
};
}
const { responseHeaders, value: response, rawValue } = await postJsonToApi({
url: `${this.config.baseURL}/models/${this.modelId}:batchEmbedContents`,
headers: mergedHeaders,
body: { requests: values.map((value, index) => {
const valueParts = multimodalContent?.[index];
const textPart = value ? [{ text: value }] : [];
return {
model: `models/${this.modelId}`,
content: {
role: "user",
parts: valueParts != null ? [...textPart, ...valueParts] : [{ text: value }]
},
outputDimensionality: googleOptions?.outputDimensionality ?? dimensions,
taskType: googleOptions?.taskType
};
}) },
failedResponseHandler: googleFailedResponseHandler,
successfulResponseHandler: createJsonResponseHandler(googleGenerativeAITextEmbeddingResponseSchema),
abortSignal,
fetch: this.config.fetch
});
return {
warnings: [],
embeddings: response.embeddings.map((item) => item.values),
usage: void 0,
response: {
headers: responseHeaders,
body: rawValue
}
};
}
};
const googleGenerativeAITextEmbeddingResponseSchema = lazySchema(() => zodSchema(z.object({ embeddings: z.array(z.object({ values: z.array(z.number()) })) })));
const googleGenerativeAISingleEmbeddingResponseSchema = lazySchema(() => zodSchema(z.object({ embedding: z.object({ values: z.array(z.number()) }) })));
function validateMultimodalContentLength({ multimodalContent, values }) {
if (multimodalContent != null && multimodalContent.length !== values.length) throw new Error(`The number of multimodal content entries (${multimodalContent.length}) must match the number of values (${values.length}).`);
}
//#endregion
//#region src/get-model-path.ts
function getModelPath(modelId) {
return modelId.includes("/") ? modelId : `models/${modelId}`;
}
//#endregion
//#region src/continue-google-stream.ts
/**
* Concatenates the event streams of a long decode response that spans several
* requests (Gemini 4 long decode).
*
* Events of the current stream are passed through unchanged. When an event
* signals that the per-request cap was reached while budget remains
* (`getContinuationToken` returns a token), the next request is sent with that
* token once the current stream ends, and its events are appended after a
* `continuation-start` marker. The marker orders per-response state changes
* with buffered events, including when the new stream is empty. If the
* continuation request fails, the error is emitted as a failed parse result
* and the stream ends.
*/
function continueGoogleStream({ stream, getContinuationToken, continueStream }) {
let reader = stream.getReader();
let pendingContinuationToken;
return new ReadableStream({
async pull(controller) {
while (true) {
const { done, value } = await reader.read();
if (!done) {
if (value.success) {
const token = getContinuationToken(value.value);
if (token != null) pendingContinuationToken = token;
}
controller.enqueue(value);
return;
}
if (pendingContinuationToken == null) {
controller.close();
return;
}
const continuationToken = pendingContinuationToken;
pendingContinuationToken = void 0;
try {
reader = (await continueStream(continuationToken)).getReader();
controller.enqueue({ type: "continuation-start" });
return;
} catch (error) {
controller.enqueue({
success: false,
error,
rawValue: void 0
});
controller.close();
return;
}
}
},
async cancel(reason) {
await reader.cancel(reason);
}
});
}
//#endregion
//#region src/convert-google-usage.ts
function convertGoogleUsage(usage) {
if (usage == null) return createNullLanguageModelUsage();
const promptTokens = usage.promptTokenCount ?? 0;
const candidatesTokens = usage.candidatesTokenCount ?? 0;
const toolUsePromptTokens = usage.toolUsePromptTokenCount ?? 0;
const cachedContentTokens = usage.cachedContentTokenCount ?? 0;
const thoughtsTokens = usage.thoughtsTokenCount ?? 0;
const inputTokens = promptTokens + toolUsePromptTokens;
return {
inputTokens: {
total: inputTokens,
noCache: inputTokens - cachedContentTokens,
cacheRead: cachedContentTokens,
cacheWrite: void 0
},
outputTokens: {
total: candidatesTokens + thoughtsTokens,
text: candidatesTokens,
reasoning: thoughtsTokens
},
raw: usage
};
}
/**
* Sums the usage metadata of two requests, e.g. the hops of a long decode
* response that was continued automatically. Non-numeric fields are taken
* from the later request.
*/
function addGoogleUsageMetadata(a, b) {
if (a == null || b == null) return b ?? a;
return {
...a,
...b,
promptTokenCount: addCounts(a.promptTokenCount, b.promptTokenCount),
candidatesTokenCount: addCounts(a.candidatesTokenCount, b.candidatesTokenCount),
toolUsePromptTokenCount: addCounts(a.toolUsePromptTokenCount, b.toolUsePromptTokenCount),
totalTokenCount: addCounts(a.totalTokenCount, b.totalTokenCount),
cachedContentTokenCount: addCounts(a.cachedContentTokenCount, b.cachedContentTokenCount),
thoughtsTokenCount: addCounts(a.thoughtsTokenCount, b.thoughtsTokenCount),
promptTokensDetails: addTokenDetails(a.promptTokensDetails, b.promptTokensDetails),
cacheTokensDetails: addTokenDetails(a.cacheTokensDetails, b.cacheTokensDetails),
candidatesTokensDetails: addTokenDetails(a.candidatesTokensDetails, b.candidatesTokensDetails),
toolUsePromptTokensDetails: addTokenDetails(a.toolUsePromptTokensDetails, b.toolUsePromptTokensDetails)
};
}
function addCounts(a, b) {
return a == null && b == null ? b ?? a : (a ?? 0) + (b ?? 0);
}
function addTokenDetails(a, b) {
if (a == null || b == null) return b ?? a;
const tokenCounts = /* @__PURE__ */ new Map();
for (const { modality, tokenCount } of [...a, ...b]) tokenCounts.set(modality, (tokenCounts.get(modality) ?? 0) + tokenCount);
return Array.from(tokenCounts, ([modality, tokenCount]) => ({
modality,
tokenCount
}));
}
//#endregion
//#region src/google-prompt.ts
const googleVideoMetadataSchema = z.object({
/** Start offset as a duration in seconds, e.g. `'3.5s'`. */
startOffset: z.string().optional(),
/** End offset as a duration in seconds, e.g. `'10s'`. */
endOffset: z.string().optional(),
/** Frames per second in the range (0, 24]. Defaults to 1. */
fps: z.number().gt(0).max(24).optional()
});
//#endregion
//#region src/tool/code-execution.ts
const codeExecutionInputSchema = z.object({
language: z.string().describe("The programming language of the code."),
code: z.string().describe("The code to be executed.")
});
const codeExecutionOutputSchema = z.object({
outcome: z.string().describe("The outcome of the execution (e.g., \"OUTCOME_OK\")."),
output: z.string().describe("The output from the code execution.")
});
/**
* A tool that enables the model to generate and run Python code.
*
* @note Ensure the selected model supports Code Execution.
* Multi-tool usage with the code execution tool is typically compatible with Gemini >=2 models.
*
* @see https://ai.google.dev/gemini-api/docs/code-execution (Google AI)
* @see https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/code-execution-api (Vertex AI)
*/
const codeExecution = createProviderExecutedToolFactory({
id: "google.code_execution",
inputSchema: codeExecutionInputSchema,
outputSchema: codeExecutionOutputSchema
});
//#endregion
//#region src/convert-to-google-messages.ts
/**
* Sentinel value Google documents for replaying functionCall parts whose
* original thoughtSignature is not available to the client.
*
* See https://ai.google.dev/gemini-api/docs/thought-signatures.
*/
const SKIP_THOUGHT_SIGNATURE_VALIDATOR = "skip_thought_signature_validator";
const dataUrlRegex = /^data:([^;,]+);base64,(.+)$/s;
function parseBase64DataUrl(value) {
const match = dataUrlRegex.exec(value);
if (match == null) return;
return {
mediaType: match[1],
data: match[2]
};
}
function convertUrlToolResultPart(url) {
const parsedDataUrl = parseBase64DataUrl(url);
if (parsedDataUrl == null) return;
return { inlineData: {
mimeType: parsedDataUrl.mediaType,
data: parsedDataUrl.data
} };
}
function containsJSONSchemaReference(value, seen = /* @__PURE__ */ new WeakSet()) {
if (value === null || typeof value !== "object") return false;
if (seen.has(value)) return false;
seen.add(value);
if (Array.isArray(value)) return value.some((item) => containsJSONSchemaReference(item, seen));
return Object.entries(value).some(([key, nestedValue]) => key === "$ref" || containsJSONSchemaReference(nestedValue, seen));
}
function serializeFunctionResponseContent(value) {
if (containsJSONSchemaReference(value)) return stringifyWithBigIntAndCycles(value);
return containsNonJsonValue(value) ? secureJsonParse(stringifyWithBigIntAndCycles(value)) : value;
}
function containsNonJsonValue(value, ancestors = /* @__PURE__ */ new Set()) {
if (typeof value === "bigint") return true;
if (typeof value !== "object" || value === null) return false;
if (typeof value.toJSON === "function") return true;
if (ancestors.has(value)) return true;
ancestors.add(value);
try {
return Object.values(value).some((item) => containsNonJsonValue(item, ancestors));
} finally {
ancestors.delete(value);
}
}
function appendToolResultParts(parts, toolName, outputValue, toolCallId, includeFunctionCallIds = true, supportedUrls = {}) {
const functionResponseParts = [];
const responseTextParts = [];
for (const contentPart of outputValue) switch (contentPart.type) {
case "text":
responseTextParts.push(contentPart.text);
break;
case "file":
if (contentPart.data.type === "data") functionResponseParts.push({ inlineData: {
mimeType: resolveFullMediaType({ part: contentPart }),
data: convertToBase64(contentPart.data.data)
} });
else if (contentPart.data.type === "url") {
const url = contentPart.data.url.toString();
const convertedUrlPart = convertUrlToolResultPart(url);
const supportedUrl = contentPart.data.url.protocol === "gs:" && contentPart.data.originalUrl != null ? contentPart.data.originalUrl : url;
if (convertedUrlPart != null) functionResponseParts.push(convertedUrlPart);
else if (isFullMediaType(contentPart.mediaType) && isUrlSupported({
url: supportedUrl,
mediaType: contentPart.mediaType,
supportedUrls
})) functionResponseParts.push({ fileData: {
mimeType: contentPart.mediaType,
fileUri: supportedUrl
} });
else responseTextParts.push(JSON.stringify(contentPart));
} else responseTextParts.push(JSON.stringify(contentPart));
break;
default: responseTextParts.push(JSON.stringify(contentPart));
}
parts.push({ functionResponse: {
...includeFunctionCallIds && toolCallId != null ? { id: toolCallId } : {},
name: toolName,
response: {
name: toolName,
content: responseTextParts.length > 0 ? responseTextParts.join("\n") : "Tool executed successfully."
},
...functionResponseParts.length > 0 ? { parts: functionResponseParts } : {}
} });
}
function appendLegacyToolResultParts(parts, toolName, outputValue, toolCallId, includeFunctionCallIds = true) {
for (const contentPart of outputValue) switch (contentPart.type) {
case "text":
parts.push({ functionResponse: {
...includeFunctionCallIds && toolCallId != null ? { id: toolCallId } : {},
name: toolName,
response: {
name: toolName,
content: contentPart.text
}
} });
break;
case "file":
if (contentPart.data.type === "data") {
const topLevelMediaType = getTopLevelMediaType(contentPart.mediaType);
parts.push({ inlineData: {
mimeType: resolveFullMediaType({ part: contentPart }),
data: convertToBase64(contentPart.data.data)
} }, { text: `Tool executed successfully and returned this ${topLevelMediaType === "image" ? "image" : "file"} as a response` });
} else parts.push({ text: JSON.stringify(contentPart) });
break;
default: parts.push({ text: JSON.stringify(contentPart) });
}
}
function convertToGoogleMessages(prompt, options) {
const systemInstructionParts = [];
const contents = [];
let systemMessagesAllowed = true;
const isGemmaModel = options?.isGemmaModel ?? false;
const isGemini3Model = options?.isGemini3Model ?? false;
const onWarning = options?.onWarning;
const providerOptionsNames = options?.providerOptionsNames ?? ["google"];
const isVertexLike = !providerOptionsNames.includes("google");
const supportsFunctionResponseParts = options?.supportsFunctionResponseParts ?? true;
const includeFunctionCallIds = options?.includeFunctionCallIds ?? true;
const supportedFunctionResponseUrls = options?.supportedFunctionResponseUrls ?? {};
let sentinelInjected = false;
const missingSignatureToolNames = [];
const injectSkipSignature = (toolName) => {
missingSignatureToolNames.push(toolName);
sentinelInjected = true;
return SKIP_THOUGHT_SIGNATURE_VALIDATOR;
};
const readProviderOpts = (part) => {
for (const name of providerOptionsNames) {
const v = part.providerOptions?.[name];
if (v != null) return v;
}
if (isVertexLike) return part.providerOptions?.google;
return part.providerOptions?.googleVertex ?? part.providerOptions?.vertex;
};
for (const { role, content } of prompt) switch (role) {
case "system":
if (!systemMessagesAllowed) throw new UnsupportedFunctionalityError({ functionality: "system messages are only supported at the beginning of the conversation" });
systemInstructionParts.push({ text: content });
break;
case "user": {
systemMessagesAllowed = false;
const parts = [];
for (const part of content) switch (part.type) {
case "text":
parts.push({ text: part.text });
break;
case "file": {
const rawVideoMetadata = readProviderOpts(part)?.videoMetadata;
const videoMetadata = rawVideoMetadata == null ? void 0 : googleVideoMetadataSchema.parse(rawVideoMetadata);
switch (part.data.type) {
case "url":
parts.push({
fileData: {
mimeType: resolveFullMediaType({ part }),
fileUri: part.data.url.protocol === "gs:" && part.data.originalUrl != null ? part.data.originalUrl : part.data.url.toString()
},
...videoMetadata != null ? { videoMetadata } : {}
});
break;
case "reference":
if (isVertexLike) throw new UnsupportedFunctionalityError({ functionality: "file parts with provider references" });
parts.push({
fileData: {
mimeType: resolveFullMediaType({ part }),
fileUri: resolveProviderReference({
reference: part.data.reference,
provider: "google"
})
},
...videoMetadata != null ? { videoMetadata } : {}
});
break;
case "text":
parts.push({
inlineData: {
mimeType: isFullMediaType(part.mediaType) ? part.mediaType : "text/plain",
data: convertToBase64(new TextEncoder().encode(part.data.text))
},
...videoMetadata != null ? { videoMetadata } : {}
});
break;
case "data": parts.push({
inlineData: {
mimeType: resolveFullMediaType({ part }),
data: convertToBase64(part.data.data)
},
...videoMetadata != null ? { videoMetadata } : {}
});
}
break;
}
}
contents.push({
role: "user",
parts
});
break;
}
case "assistant": {
systemMessagesAllowed = false;
let modelResponseHasSignedFunctionCall = false;
contents.push({
role: "model",
parts: content.map((part) => {
const providerOpts = readProviderOpts(part);
const thoughtSignature = providerOpts?.thoughtSignature != null ? String(providerOpts.thoughtSignature) : void 0;
switch (part.type) {
case "text": return part.text.length === 0 && thoughtSignature == null ? void 0 : {
text: part.text,
thoughtSignature
};
case "reasoning": return part.text.length === 0 && thoughtSignature == null ? void 0 : {
text: part.text,
thought: true,
thoughtSignature
};
case "reasoning-file":
switch (part.data.type) {
case "url": throw new UnsupportedFunctionalityError({ functionality: "File data URLs in assistant messages are not supported" });
case "data": return {
inlineData: {
mimeType: part.mediaType,
data: convertToBase64(part.data.data)
},
thought: true,
thoughtSignature
};
}
break;
case "file":
switch (part.data.type) {
case "url": throw new UnsupportedFunctionalityError({ functionality: "File data URLs in assistant messages are not supported" });
case "reference":
if (isVertexLike) throw new UnsupportedFunctionalityError({ functionality: "file parts with provider references" });
return {
fileData: {
mimeType: part.mediaType,
fileUri: resolveProviderReference({
reference: part.data.reference,
provider: "google"
})
},
...providerOpts?.thought === true ? { thought: true } : {},
thoughtSignature
};
case "text": return {
inlineData: {
mimeType: isFullMediaType(part.mediaType) ? part.mediaType : "text/plain",
data: convertToBase64(new TextEncoder().encode(part.data.text))
},
...providerOpts?.thought === true ? { thought: true } : {},
thoughtSignature
};
case "data": return {
inlineData: {
mimeType: part.mediaType,
data: convertToBase64(part.data.data)
},
...providerOpts?.thought === true ? { thought: true } : {},
thoughtSignature
};
}
break;
case "tool-call": {
if (part.providerExecuted === true && part.toolName === "code_execution") return { executableCode: codeExecutionInputSchema.parse(typeof part.input === "string" ? secureJsonParse(part.input) : part.input) };
const serverToolCallId = providerOpts?.serverToolCallId != null ? String(providerOpts.serverToolCallId) : void 0;
const serverToolType = providerOpts?.serverToolType != null ? String(providerOpts.serverToolType) : void 0;
const isServerToolCall = serverToolCallId != null && serverToolType != null;
const effectiveThoughtSignature = thoughtSignature ?? (isGemini3Model && !(!isServerToolCall && thoughtSignature == null && modelResponseHasSignedFunctionCall) ? injectSkipSignature(part.toolName) : void 0);
if (!isServerToolCall && thoughtSignature != null) modelResponseHasSignedFunctionCall = true;
if (isServerToolCall) return {
toolCall: {
toolType: serverToolType,
args: typeof part.input === "string" ? secureJsonParse(part.input) : part.input,
id: serverToolCallId
},
thoughtSignature: effectiveThoughtSignature
};
return {
functionCall: {
...includeFunctionCallIds && part.toolCallId != null ? { id: part.toolCallId } : {},
name: part.toolName,
args: parseToolCallInput(part.input)
},
thoughtSignature: effectiveThoughtSignature
};
}
case "tool-result": {
if (part.toolName === "code_execution" && part.output.type === "json") return { codeExecutionResult: codeExecutionOutputSchema.parse(part.output.value) };
const serverToolCallId = providerOpts?.serverToolCallId != null ? String(providerOpts.serverToolCallId) : void 0;
const serverToolType = providerOpts?.serverToolType != null ? String(providerOpts.serverToolType) : void 0;
if (serverToolCallId && serverToolType) return {
toolResponse: {
toolType: serverToolType,
response: part.output.type === "json" ? part.output.value : {},
id: serverToolCallId
},
thoughtSignature
};
return;
}
}
}).filter((part) => part !== void 0)
});
break;
}
case "tool": {
systemMessagesAllowed = false;
const parts = [];
for (const part of content) {
if (part.type === "tool-approval-response") continue;
const partProviderOpts = readProviderOpts(part);
const serverToolCallId = partProviderOpts?.serverToolCallId != null ? String(partProviderOpts.serverToolCallId) : void 0;
const serverToolType = partProviderOpts?.serverToolType != null ? String(partProviderOpts.serverToolType) : void 0;
if (serverToolCallId && serverToolType) {
const serverThoughtSignature = partProviderOpts?.thoughtSignature != null ? String(partProviderOpts.thoughtSignature) : void 0;
if (contents.length > 0) {
const lastContent = contents[contents.length - 1];
if (lastContent.role === "model") {
lastContent.parts.push({
toolResponse: {
toolType: serverToolType,
response: part.output.type === "json" ? part.output.value : {},
id: serverToolCallId
},
thoughtSignature: serverThoughtSignature
});
continue;
}
}
}
const output = part.output;
if (output.type === "content") {
if (supportsFunctionResponseParts) appendToolResultParts(parts, part.toolName, output.value, part.toolCallId, includeFunctionCallIds, supportedFunctionResponseUrls);
else appendLegacyToolResultParts(parts, part.toolName, output.value, part.toolCallId, includeFunctionCallIds);
} else {
const isError = output.type === "error-text" || output.type === "error-json" || output.type === "execution-denied";
parts.push({ functionResponse: {
...includeFunctionCallIds && part.toolCallId != null ? { id: part.toolCallId } : {},
name: part.toolName,
response: {
name: part.toolName,
...isError ? { error: output.type === "execution-denied" ? output.reason ?? "Tool call execution denied." : serializeFunctionResponseContent(output.value) } : { content: serializeFunctionResponseContent(output.value) }
}
} });
}
}
contents.push({
role: "user",
parts
});
break;
}
}
if (isGemmaModel && systemInstructionParts.length > 0 && contents.length > 0 && contents[0].role === "user") {
const systemText = systemInstructionParts.map((part) => part.text).join("\n\n");
contents[0].parts.unshift({ text: systemText + "\n\n" });
}
if (sentinelInjected && onWarning != null) {
const uniqueToolNames = Array.from(new Set(missingSignatureToolNames));
onWarning({
type: "other",
message: `Replayed ${missingSignatureToolNames.length} \`functionCall\` part(s) for a Gemini 3 model without a \`thoughtSignature\` (tools: ${uniqueToolNames.map((name) => `\`${name}\``).join(", ")}). Injected the documented \`skip_thought_signature_validator\` sentinel to keep the request from failing with HTTP 400. The likely cause is application code that drops \`providerOptions.google.thoughtSignature\` when persisting or serializing assistant tool-call messages. See https://ai.google.dev/gemini-api/docs/thought-signatures.`
});
}
return {
systemInstruction: systemInstructionParts.length > 0 && !isGemmaModel ? { parts: systemInstructionParts } : void 0,
contents
};
}
function parseToolCallInput(input) {
if (typeof input !== "string") return input;
try {
return secureJsonParse(input);
} catch {
return input;
}
}
//#endregion
//#region src/download-tool-result-files.ts
/**
* Download tool result file URLs that Vertex cannot reference directly before
* converting the prompt to the Google request format.
*/
async function downloadToolResultFiles(prompt, { abortSignal, fetch, maxBytes, supportedUrls = {} }) {
const result = [];
for (const message of prompt) {
if (message.role === "assistant") {
const content = [];
for (const part of message.content) content.push(part.type === "tool-result" ? {
...part,
output: await downloadToolResultOutput(part.output, {
abortSignal,
fetch,
maxBytes,
supportedUrls
})
} : part);
result.push({
...message,
content
});
continue;
}
if (message.role === "tool") {
const content = [];
for (const part of message.content) {
if (part.type !== "tool-result") {
content.push(part);
continue;
}
content.push({
...part,
output: await downloadToolResultOutput(part.output, {
abortSignal,
fetch,
maxBytes,
supportedUrls
})
});
}
result.push({
...message,
content
});
continue;
}
result.push(message);
}
return result;
}
async function downloadToolResultOutput(output, { abortSignal, fetch, maxBytes, supportedUrls }) {
if (output.type !== "content") return output;
const value = [];
for (const part of output.value) {
if (part.type !== "file" || part.data.type !== "url") {
value.push(part);
continue;
}
if (isUrlSupported({
url: part.data.url.toString(),
mediaType: part.mediaType,
supportedUrls
})) {
value.push(part);
continue;
}
const blob = await downloadBlob(part.data.url.toString(), {
abortSignal,
fetch,
maxBytes
});
const data = new Uint8Array(await blob.arrayBuffer());
const detectedMediaType = detectMediaType({
data,
topLevelType: "image"
});
value.push({
...part,
data: {
type: "data",
data
},
mediaType: detectedMediaType ?? (blob.type && !isFullMediaType(part.mediaType) ? blob.type : part.mediaType)
});
}
return {
...output,
value
};
}
//#endregion
//#region src/sanitize-response-json-schema.ts
/**
* Recursively replaces `const` with a single-value `enum` in the JSON Schema
* locations supported by Google because `responseJsonSchema` does not support
* `const`. All other schema properties are preserved.
*/
function sanitizeResponseJsonSchema(schema) {
const { const: constValue, properties, items, additionalProperties, anyOf, oneOf, ...result } = schema;
return {
...result,
...constValue !== void 0 ? { enum: [constValue] } : {},
...properties != null ? { properties: sanitizeDefinitions(properties) } : {},
...items != null ? { items: Array.isArray(items) ? items.map(sanitizeDefinition) : sanitizeDefinition(items) } : {},
...additionalProperties != null ? { additionalProperties: typeof additionalProperties === "boolean" ? additionalProperties : sanitizeDefinition(additionalProperties) } : {},
...anyOf != null ? { anyOf: anyOf.map(sanitizeDefinition) } : {},
...oneOf != null ? { oneOf: oneOf.map(sanitizeDefinition) } : {},
...result.$defs != null ? { $defs: sanitizeDefinitions(result.$defs) } : {}
};
}
function sanitizeDefinitions(definitions) {
return Object.fromEntries(Object.entries(definitions).map(([name, definition]) => [name, sanitizeDefinition(definition)]));
}
function sanitizeDefinition(definition) {
return typeof definition === "boolean" ? definition : sanitizeResponseJsonSchema(definition);
}
//#endregion
//#region src/google-language-model-options.ts
const googleLanguageModelOptions = lazySchema(() => zodSchema(z.object({
responseModalities: z.array(z.enum([
"TEXT",
"IMAGE",
"AUDIO"
])).optional(),
thinkingConfig: z.object({
thinkingBudget: z.number().optional(),
includeThoughts: z.boolean().optional(),
thinkingLevel: z.enum([
"minimal",
"low",
"medium",
"high",
"xhigh"
]).optional()
}).optional(),
/**
* Optional.
* The name of the cached content used as context to serve the prediction.
* Format: cachedContents/{cachedContent}
*/
cachedContent: z.string().optional(),
/**
* Optional. Enable structured output. Default is true.
*
* This is useful when the JSON Schema contains elements that are
* not supported by the OpenAPI schema version that
* Google uses. You can use this to disable
* structured outputs if you need to.
*/
structuredOutputs: z.boolean().optional(),
/**
* Optional. A list of unique safety settings for blocking unsafe content.
*/
safetySettings: z.array(z.object({
category: z.enum([
"HARM_CATEGORY_UNSPECIFIED",
"HARM_CATEGORY_HATE_SPEECH",
"HARM_CATEGORY_DANGEROUS_CONTENT",
"HARM_CATEGORY_HARASSMENT",
"HARM_CATEGORY_SEXUALLY_EXPLICIT",
"HARM_CATEGORY_CIVIC_INTEGRITY"
]),
threshold: z.enum([
"HARM_BLOCK_THRESHOLD_UNSPECIFIED",
"BLOCK_LOW_AND_ABOVE",
"BLOCK_MEDIUM_AND_ABOVE",
"BLOCK_ONLY_HIGH",
"BLOCK_NONE",
"OFF"
])
})).optional(),
threshold: z.enum([
"HARM_BLOCK_THRESHOLD_UNSPECIFIED",
"BLOCK_LOW_AND_ABOVE",
"BLOCK_MEDIUM_AND_ABOVE",
"BLOCK_ONLY_HIGH",
"BLOCK_NONE",
"OFF"
]).optional(),
/**
* Optional. Enables timestamp understanding for audio-only files.
*
* https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/audio-understanding
*/
audioTimestamp: z.boolean().optional(),
/**
* Optional. Defines labels used in billing reports. Available on Vertex AI only.
*
* https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/add-labels-to-api-calls
*/
labels: z.record(z.string(), z.string()).optional(),
/**
* Optional. If specified, the media resolution specified will be used.
*
* https://ai.google.dev/api/generate-content#MediaResolution
*/
mediaResolution: z.enum([
"MEDIA_RESOLUTION_UNSPECIFIED",
"MEDIA_RESOLUTION_LOW",
"MEDIA_RESOLUTION_MEDIUM",
"MEDIA_RESOLUTION_HIGH"
]).optional(),
/**
* Optional. Configures the image generation aspect ratio for Gemini models.
*
* https://ai.google.dev/gemini-api/docs/image-generation#aspect_ratios
*/
imageConfig: z.object({
aspectRatio: z.enum([
"1:1",
"2:3",
"3:2",
"3:4",
"4:3",
"4:5",
"5:4",
"9:16",
"16:9",
"21:9",
"1:8",
"8:1",
"1:4",
"4:1"
]).optional(),
imageSize: z.enum([
"1K",
"2K",
"4K",
"512"
]).optional(),
/**
* Optional. Controls the generation of people in images.
* Vertex AI only.
*/
personGeneration: z.enum([
"PERSON_GENERATION_UNSPECIFIED",
"ALLOW_ALL",
"ALLOW_ADULT",
"ALLOW_NONE"
]).optional(),
/**
* Optional. Controls whether generation of prominent people
* (celebrities) is allowed. When set together with
* `personGeneration`, `personGeneration` takes precedence.
* Vertex AI only.
*
* https://docs.cloud.google.com/vertex-ai/generative-ai/docs/reference/rest/v1/GenerationConfig
*/
prominentPeople: z.enum([
"PROMINENT_PEOPLE_UNSPECIFIED",
"ALLOW_PROMINENT_PEOPLE",
"BLOCK_PROMINENT_PEOPLE"
]).optional(),
/**
* Optional. The image output format for generated images.
* Vertex AI only.
*/
imageOutputOptions: z.object({
mimeType: z.enum(["image/jpeg", "image/png"]).optional(),
compressionQuality: z.number().optional()
}).optional()
}).optional(),
/**
* Optional. Configuration for grounding retrieval.
* Used to provide location context for Google Maps and Google Search grounding.
*
* https://cloud.google.com/vertex-ai/generative-ai/docs/grounding/grounding-with-google-maps
*/
retrievalConfig: z.object({ latLng: z.object({
latitude: z.number(),
longitude: z.number()
}).optional() }).optional(),
/**
* Optional. When set to true, function call arguments will be streamed
* incrementally via partialArgs in streaming responses. Only supported
* on the Vertex AI API (not the Gemini API) and only for Gemini 3+
* models.
*
* @default false
*
* https://docs.cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling#streaming-fc
*/
streamFunctionCallArguments: z.boolean().optional(),
/**
* Optional. The service tier to use for the request. Sent as the
* `serviceTier` body field. Gemini API only.
*/
serviceTier: z.enum([
"standard",
"flex",
"priority"
]).optional(),
/**
* Optional. Resumes a response that stopped with `MAX_TOKENS` because it
* used up its `maxOutputTokens` budget (Gemini 4 long decode). Pass the
* `continuationToken` from the previous response's provider metadata
* together with the original prompt and a higher `maxOutputTokens` (the
* budget is cumulative across requests). Requests that reach the
* per-request cap while budget remains (`CONTINUATION`) are continued
* automatically. Sent as the `continuationToken` body field.
*/
continuationToken: z.string().optional(),
/**
* Optional. Vertex AI only. Sent as the
* `X-Vertex-AI-LLM-Shared-Request-Type` request header to select a
* shared (PayGo) tier. With Provisioned Throughput allocated and
* `requestType` unset, the request falls back to this tier only if
* PT capacity is exhausted.
*
* https://docs.cloud.google.com/vertex-ai/generative-ai/docs/priority-paygo
* https://docs.cloud.google.com/vertex-ai/generative-ai/docs/flex-paygo
*/
sharedRequestType: z.enum([
"priority",
"flex",
"standard"
]).optional(),
/**
* Optional. Vertex AI only. Sent as the `X-Vertex-AI-LLM-Request-Type`
* request header. Set to `'shared'` together with `sharedRequestType`
* to bypass Provisioned Throughput entirely.
*
* https://docs.cloud.google.com/vertex-ai/generative-ai/docs/priority-paygo
*/
requestType: z.enum(["shared"]).optional()
})));
//#endregion
//#region src/google-model-catalog.ts
const gemini1ModelPattern = /(^|[/.])gemini-1(?:[.-]|$)/i;
const gemini2ModelPattern = /(^|[/.])gemini-2(?:[.-]|$)/i;
const gemini25ModelPattern = /(^|[/.])gemini-2\.5(?:[.-]|$)/i;
const geminiModelPattern = /(^|[/.])gemini-/i;
/** Options accepted under providerOptions.google (or googleVertex on Vertex). */
const googleProviderOptions = { language: {
responseModalities: { description: "Response modalities to generate." },
thinkingConfig: { description: "Thinking budget, level, and thought output." },
cachedContent: { description: "Cached content to use as request context." },
structuredOutputs: { description: "Whether to send the JSON response schema." },
safetySettings: { description: "Safety settings for the request." },
threshold: { description: "Default threshold for configurable safety categories." },
audioTimestamp: { description: "Enable timestamp understanding for audio." },
labels: { description: "Billing labels. Vertex AI only." },
mediaResolution: { description: "Resolution used to process media inputs." },
imageConfig: { description: "Image generation configuration." },
retrievalConfig: { description: "Retrieval configuration for tools." },
streamFunctionCallArguments: { description: "Stream function arguments. Vertex AI only." },
serviceTier: { description: "Service tier. Gemini API only." },
continuationToken: { description: "Resume a response using its continuation token." },
sharedRequestType: { description: "Shared PayGo tier. Vertex AI only." },
requestType: { description: "Request routing type. Vertex AI only." }
} };
const standard = {
image: true,
objectGeneration: true,
tools: true,
streaming: true,
googleSearch: true,
urlContext: true
};
const googleModelCatalog = Object.fromEntries(Object.entries({
"gemini-nano-banana-2.1": {
image: true,
objectGeneration: false,
tools: false,
streaming: false,
googleSearch: true,
urlContext: false
},
"gemini-4-argon": standard,
"gemini-4-carbon": standard,
"gemini-4-hydrogen": standard,
"gemini-3.8-flash": standard,
"gemini-3.7-flash": standard,
"gemini-3.6-flash": standard,
"gemini-3.5-flash": standard,
"gemini-3.5-flash-lite": standard,
"gemini-3.1-pro-preview": standard,
"gemini-3.1-flash-image-preview": standard,
"gemini-3.1-flash-lite-preview": standard,
"gemini-3-pro-preview": standard,
"gemini-3-pro-image-preview": standard,
"gemini-3-flash-preview": standard,
"gemini-2.5-pro": standard,
"gemini-2.5-flash": standard,
"gemini-2.5-flash-lite": standard,
"gemini-2.5-flash-lite-preview-06-17": standard,
"gemini-2.0-flash": standard,
aqa: void 0,
"deep-research-max-preview-04-2026": void 0,
"deep-research-preview-04-2026": void 0,
"deep-research-pro-preview-12-2025": void 0,
"gemini-2.0-flash-001": void 0,
"gemini-2.0-flash-lite": void 0,
"gemini-2.0-flash-lite-001": void 0,
"gemini-2.5-computer-use-preview-10-2025": void 0,
"gemini-2.5-flash-image": void 0,
"gemini-2.5-flash-native-audio-latest": void 0,
"gemini-2.5-flash-native-audio-preview-09-2025": void 0,
"gemini-2.5-flash-native-audio-preview-12-2025": void 0,
"gemini-2.5-flash-preview-tts": void 0,
"gemini-2.5-pro-preview-tts": void 0,
"gemini-3.1-flash-tts-preview": void 0,
"gemini-3.1-pro-preview-customtools": void 0,
"gemini-flash-latest": void 0,
"gemini-flash-lite-latest": void 0,
"gemini-pro-latest": void 0,
"gemini-robotics-er-1.5-preview": void 0,
"gemma-3-12b-it": void 0,
"gemma-3-1b-it": void 0,
"gemma-3-27b-it": void 0,
"gemma-3-4b-it": void 0,
"gemma-3n-e2b-it": void 0,
"gemma-3n-e4b-it": void 0,
"nano-banana-pro-preview": void 0
}).filter(([, definition]) => definition != null).map(([modelId, documented]) => [modelId, createGoogleLanguageModelEntry(modelId, documented)]));
function getGoogleLanguageModelEntry(modelId) {
return Object.prototype.hasOwnProperty.call(googleModelCatalog, modelId) ? googleModelCatalog[modelId] : createGoogleLanguageModelEntry(modelId);
}
function createGoogleLanguageModelEntry(modelId, documented) {
const runtime = resolveRuntimeDefinition(modelId);
const declared = documented != null;
const id = modelId.toLowerCase();
const thinkingFormat = runtime.usesGemini3Features && !modelId.includes("gemini-3-pro-image") ? "level" : "budget";
const isHydrogen = /(^|[/.])gemini-4-hydrogen(?:-|$)/i.test(modelId);
const minLevel = isHydrogen ? "low" : getMinimumThinkingLevelForGemini3Model(modelId);
return {
capabilities: { language: {
inputs: {
image: documented?.image ?? "unknown",
audio: "unknown"
},
objectGeneration: documented?.objectGeneration ?? "unknown",
tools: {
usage: documented?.tools ?? "unknown",
streaming: documented?.streaming ?? "unknown",
gemini2: runtime.supportsGemini2Tools,
googleSearch: documented?.googleSearch ?? "unknown",
urlContext: documented?.urlContext ?? "unknown",
fileSearch: runtime.supportsFileSearch,
mixed: runtime.usesGemini3Features,
functionResponseParts: runtime.usesGemini3Features
},
reasoning: {
support: "unknown",
efforts: thinkingFormat === "level" ? [
...minLevel === "minimal" ? ["minimal"] : [],
"low",
"medium",
"high",
...isHydrogen ? ["xhigh"] : []
] : void 0,
minBudget: id.includes("2.5-pro") ? 128 : 0,
maxBudget: id.includes("2.5-pro") || id.includes("gemini-3-pro-image") ? 32768 : 24576
},
sampling: {
temperature: "unknown",
topP: "unknown",
topK: "unknown",
logprobs: false,
penalties: gemini25ModelPattern.test(modelId) ? { condition: "Only supported on the Vertex AI API." } : true
},
structuredOutput: { withFunctionTools: runtime.usesGemini3Features }
} },
providerOptions: { language: declareProviderOptions(declared ? true : "unknown", {
labels: { condition: "Only supported on the Vertex AI API." },
sharedRequestType: { condition: "Only supported on the Vertex AI API." },
requestType: { condition: "Only supported on the Vertex AI API." },
serviceTier: { condition: "Only supported on the Gemini API." },
streamFunctionCallArguments: { condition: "Only supported for streaming requests on the Vertex AI API with Gemini 3 or newer." }
}) },
requestConfig: { language: {
systemInstruction: id.startsWith("gemma-") ? "user" : "system",
thinkingFormat,
reasoningMaxOutputTokens: 65536,
jsonResponseTool: /(^|\/)gemini-/i.test(modelId)
} }
};
}
function declareProviderOptions(support, exceptions) {
return Object.fromEntries(Object.keys(googleProviderOptions.language).map((option) => [option, exceptions[option] ?? support]));
}
function isKnownPreGemini2Model(modelId) {
return gemini1ModelPattern.test(modelId) || /(^|[/.])gemini-pro(?:-vision)?$/i.test(modelId) || /(^|[/.])gemini-robotics-er-1\.5(?:[.-]|$)/i.test(modelId);
}
/**
* Classifies Gemini capabilities by excluding known older generations.
*
* Google model IDs are open-ended, so unrecognized Gemini IDs and aliases
* intentionally inherit the newest supported behavior. Add exceptions here
* only when a model is known to require a legacy request shape.
*/
function resolveRuntimeDefinition(modelId) {
const isGeminiModel = geminiModelPattern.test(modelId);
const isGemini2Model = gemini2ModelPattern.test(modelId);
const isKnownPreGemini2 = isKnownPreGemini2Model(modelId);
const usesGemini3Features = isGeminiModel && !(isKnownPreGemini2 || isGemini2Model);
return {
supportsGemini2Tools: isGeminiModel && !isKnownPreGemini2 || modelId.toLowerCase().includes("nano-banana"),
supportsFileSearch: gemini25ModelPattern.test(modelId) || usesGemini3Features,
usesGemini3Features
};
}
function getMinimumThinkingLevelForGemini3Model(modelId) {
const modelName = /(?:^|[/.])(gemini-[^/]*)$/i.exec(modelId)?.[1].toLowerCase();
if (modelName === "gemini-flash-latest") return "low";
const proVersionMatch = /^gemini-(\d+)(?:\.\d+)?-pro(?:$|-)/.exec(modelName ?? "");
if (Number(proVersionMatch?.[1]) >= 3) return "low";
const versionMatch = /^gemini-(\d+)\.(\d+)-flash(?:$|-(?!lite(?:-|$)))/.exec(modelName ?? "");
if (versionMatch == null) return "minimal";
const majorVersion = Number(versionMatch[1]);
const minorVersion = Number(versionMatch[2]);
return majorVersion > 3 || majorVersion === 3 && minorVersion >= 7 ? "low" : "minimal";
}
//#endregion
//#region src/google-model-capabilities.ts
/** Compatibility projection of the resolved catalog for existing request code. */
function getGoogleModelCapabilities(modelId) {
const { tools } = getGoogleLanguageModelEntry(modelId).capabilities.language;
return {
supportsGemini2Tools: tools.gemini2,
supportsFileSearch: tools.fileSearch,
usesGemini3Features: tools.mixed
};
}
//#endregion
//#region src/google-prepare-tools.ts
function prepareTools({ tools, toolChoice, modelId, isVertexProvider = false }) {
if (toolChoice?.type === "none") tools = tools?.filter((tool) => tool.type === "function");
tools = tools?.length ? tools : void 0;
const toolWarnings = [];
const { supportsGemini2Tools, supportsFileSearch, usesGemini3Features } = getGoogleModelCapabilities(modelId);
if (tools == null) return {
tools: void 0,
toolConfig: void 0,
toolWarnings
};
const hasFunctionTools = tools.some((tool) => tool.type === "function");
const hasProviderTools = tools.some((tool) => tool.type === "provider");
if (hasFunctionTools && hasProviderTools && !usesGemini3Features) toolWarnings.push({
type: "unsupported",
feature: `combination of function and provider-defined tools`
});
if (hasProviderTools) {
const googleTools = [];
tools.filter((tool) => tool.type === "provider").forEach((tool) => {
switch (tool.id) {
case "google.google_search":
if (supportsGemini2Tools) googleTools.push({ googleSearch: { ...tool.args } });
else toolWarnings.push({
type: "unsupported",
feature: `provider-defined tool ${tool.id}`,
details: "Google Search requires Gemini 2.0 or newer."
});
break;
case "google.enterprise_web_search":
if (supportsGemini2Tools) googleTools.push({ enterpriseWebSearch: {} });
else toolWarnings.push({
type: "unsupported",
feature: `provider-defined tool ${tool.id}`,
details: "Enterprise Web Search requires Gemini 2.0 or newer."
});
break;
case "google.url_context":
if (supportsGemini2Tools) googleTools.push({ urlContext: {} });
else toolWarnings.push({
type: "unsupported",
feature: `provider-defined tool ${tool.id}`,
details: "The URL context tool is not supported with other Gemini models than Gemini 2."
});
break;
case "google.code_execution":
if (supportsGemini2Tools) googleTools.push({ codeExecution: {} });
else toolWarnings.push({
type: "unsupported",
feature: `provider-defined tool ${tool.id}`,
details: "The code execution tool is not supported with other Gemini models than Gemini 2."
});
break;
case "google.file_search":
if (supportsFileSearch) googleTools.push({ fileSearch: { ...tool.args } });
else toolWarnings.push({
type: "unsupported",
feature: `provider-defined tool ${tool.id}`,
details: "The file search tool is only supported with Gemini 2.5 models and Gemini 3 models."
});
break;
case "google.vertex_rag_store":
if (supportsGemini2Tools) googleTools.push({ retrieval: { vertex_rag_store: {
rag_resources: { rag_corpus: tool.args.ragCorpus },
similarity_top_k: tool.args.topK
} } });
else toolWarnings.push({
type: "unsupported",
feature: `provider-defined tool ${tool.id}`,
details: "The RAG store tool is not supported with other Gemini models than Gemini 2."
});
break;
case "google.google_maps":
if (supportsGemini2Tools) googleTools.push({ googleMaps: {} });
else toolWarnings.push({
type: "unsupported",
feature: `provider-defined tool ${tool.id}`,
details: "The Google Maps grounding tool is not supported with Gemini models other than Gemini 2 or newer."
});
break;
default: toolWarnings.push({
type: "unsupported",
feature: `provider-defined tool ${tool.id}`
});
}
});
if (hasFunctionTools && usesGemini3Features && googleTools.length > 0) {
const functionDeclarations = [];
for (const tool of tools) if (tool.type === "function") functionDeclarations.push(prepareFunctionDeclaration(tool));
const combinedToolConfig = {
functionCallingConfig: { mode: "VALIDATED" },
...!isVertexProvider && { includeServerSideToolInvocations: true }
};
if (toolChoice != null) switch (toolChoice.type) {
case "auto": break;
case "none":
combinedToolConfig.functionCallingConfig = { mode: "NONE" };
break;
case "required":
combinedToolConfig.functionCallingConfig = { mode: "ANY" };
break;
case "tool": combinedToolConfig.functionCallingConfig = {
mode: "ANY",
allowedFunctionNames: [toolChoice.toolName]
};
}
return {
tools: [...googleTools, { functionDeclarations }],
toolConfig: combinedToolConfig,
toolWarnings
};
}
return {
tools: googleTools.length > 0 ? googleTools : void 0,
toolConfig: void 0,
toolWarnings
};
}
const functionDeclarations = [];
let hasStrictTools = false;
for (const tool of tools) switch (tool.type) {
case "function":
functionDeclarations.push(prepareFunctionDeclaration(tool));
if (tool.strict === true) hasStrictTools = true;
break;
default: toolWarnings.push({
type: "unsupported",
feature: `function tool ${tool.name}`
});
}
if (toolChoice == null) return {
tools: [{ functionDeclarations }],
toolConfig: hasStrictTools ? { functionCallingConfig: { mode: "VALIDATED" } } : void 0,
toolWarnings
};
const type = toolChoice.type;
switch (type) {
case "auto": return {
tools: [{ functionDeclarations }],
toolConfig: { functionCallingConfig: { mode: hasStrictTools ? "VALIDATED" : "AUTO" } },
toolWarnings
};
case "none": return {
tools: [{ functionDeclarations }],
toolConfig: { functionCallingConfig: { mode: "NONE" } },
toolWarnings
};
case "required": return {
tools: [{ functionDeclarations }],
toolConfig: { functionCallingConfig: { mode: "ANY" } },
toolWarnings
};
case "tool": return {
tools: [{ functionDeclarations }],
toolConfig: { functionCallingConfig: {
mode: "ANY",
allowedFunctionNames: [toolChoice.toolName]
} },
toolWarnings
};
default: throw new UnsupportedFunctionalityError({ functionality: `tool choice type: ${type}` });
}
}
function prepareFunctionDeclaration(tool) {
return {
name: tool.name,
description: tool.description ?? "",
parametersJsonSchema: tool.inputSchema
};
}
//#endregion
//#region src/google-json-accumulator.ts
/**
* Incrementally builds a JSON object from Google's streaming `partialArgs`
* chunks emitted during tool-call function calling. Tracks both the structured
* object and a running JSON text representation so callers can emit text deltas
* that, when concatenated, form valid nested JSON matching JSON.stringify output.
*
* Input: [{jsonPath:"$.location",stringValue:"Boston"}]
* Output: '{"location":"Boston"', then finalize() → closingDelta='}'
*/
var GoogleJSONAccumulator = class {
constructor() {
this.accumulatedArgs = {};
this.jsonText = "";
this.pathStack = [];
this.stringOpen = false;
}
/**
* Input: [{jsonPath:"$.brightness",numberValue:50}]
* Output: { currentJSON:{brightness:50}, textDelta:'{"brightness":50' }
*/
processPartialArgs(partialArgs) {
let delta = "";
for (const arg of partialArgs) {
const isArrayTerminator = arg.jsonPath.endsWith("[]");
const rawPath = (isArrayTerminator ? arg.jsonPath.slice(0, -2) : arg.jsonPath).replace(/^\$\./, "");
if (!rawPath || rawPath === "$") continue;
const segments = parsePath(rawPath);
if (!normalizeAndCheckIndexes(this.accumulatedArgs, segments)) continue;
const existingValue = getNestedValue(this.accumulatedArgs, segments);
if (arg.stringValue != null && existingValue !== void 0) {
const escaped = JSON.stringify(arg.stringValue).slice(1, -1);
setNestedValue(this.accumulatedArgs, segments, existingValue + arg.stringValue);
delta += escaped;
if (arg.willContinue !== true) delta += this.closeOpenString();
continue;
}
const resolved = resolvePartialArgValue(arg);
if (resolved == null) {
if (existingValue !== void 0) {
if (typeof existingValue === "string" && arg.willContinue !== true) delta += this.closeOpenString();
continue;
}
if (arg.willContinue === true) continue;
setNestedValue(this.accumulatedArgs, segments, isArrayTerminator ? [] : {});
delta += this.emitContainer(segments, isArrayTerminator);
continue;
}
setNestedValue(this.accumulatedArgs, segments, resolved.value);
delta += this.emitNavigationTo(segments, arg, resolved.json);
}
this.jsonText += delta;
return {
currentJSON: this.accumulatedArgs,
textDelta: delta
};
}
/**
* Input: jsonText='{"brightness":50', accumulatedArgs={brightness:50}
* Output: { finalJSON:'{"brightness":50}', closingDelta:'}' }
*/
finalize() {
const finalArgs = JSON.stringify(this.accumulatedArgs);
return {
finalJSON: finalArgs,
closingDelta: finalArgs.slice(this.jsonText.length)
};
}
/**
* Input: stringOpen=true
* Output: '"' (and stringOpen=false), or '' when no string is open
*/
closeOpenString() {
if (!this.stringOpen) return "";
this.stringOpen = false;
return "\"";
}
/**
* Input: pathStack=[] (first call) or pathStack=[root,...] (subsequent calls)
* Output: '{' (first call) or '' (subsequent calls)
*/
ensureRoot() {
if (this.pathStack.length === 0) {
this.pathStack.push({
segment: "",
isArray: false,
childCount: 0
});
return "{";
}
return "";
}
/**
* Emits the JSON text fragment needed to navigate from the current open
* path to the new leaf at `targetSegments`, then writes the value.
*
* Input: targetSegments=["recipe","name"], arg={jsonPath:"$.recipe.name",stringValue:"Lasagna"}, valueJson='"Lasagna"'
* Output: '{"recipe":{"name":"Lasagna"'
*/
emitNavigationTo(targetSegments, arg, valueJson) {
let fragment = this.closeOpenString();
fragment += this.ensureRoot();
const targetContainerSegments = targetSegments.slice(0, -1);
const leafSegment = targetSegments[targetSegments.length - 1];
const commonDepth = this.findCommonStackDepth(targetContainerSegments);
fragment += this.closeDownTo(commonDepth);
fragment += this.openDownTo(targetContainerSegments, leafSegment);
fragment += this.emitLeaf(leafSegment, arg, valueJson);
return fragment;
}
/**
* Navigates to `targetSegments` and opens a container there. The container
* stays open so later entries for its children continue inside it; it is
* closed by subsequent navigation or by `finalize()`.
*
* Input: targetSegments=["nested","tags"], isArray=true
* Output: '{"nested":{"tags":['
*/
emitContainer(targetSegments, isArray) {
let fragment = this.closeOpenString();
fragment += this.ensureRoot();
const commonDepth = this.findCommonStackDepth(targetSegments);
fragment += this.closeDownTo(commonDepth);
fragment += this.openDownTo(targetSegments, isArray ? 0 : "");
return fragment;
}
/**
* Returns the stack depth to preserve when navigating to a new target
* container path. Always >= 1 (the root is never popped).
*
* Input: stack=[root,"recipe","ingredients",0], target=["recipe","ingredients",1]
* Output: 3 (keep root+"recipe"+"ingredients")
*/
findCommonStackDepth(targetContainer) {
const maxDepth = Math.min(this.pathStack.length - 1, targetContainer.length);
let common = 0;
for (let i = 0; i < maxDepth; i++) if (this.pathStack[i + 1].segment === targetContainer[i]) common++;
else break;
return common + 1;
}
/**
* Closes containers from the current stack depth back down to `targetDepth`.
*
* Input: this.pathStack=[root,"recipe","ingredients",0], targetDepth=3
* Output: '}'
*/
closeDownTo(targetDepth) {
let fragment = "";
while (this.pathStack.length > targetDepth) {
const entry = this.pathStack.pop();
fragment += entry.isArray ? "]" : "}";
}
return fragment;
}
/**
* Opens containers from the current stack depth down to the full target
* container path, emitting opening `{`, `[`, keys, and commas as needed.
* `leafSegment` is used to determine if the innermost container is an array.
*
* Input: this.pathStack=[root], targetContainer=["recipe","ingredients"], leafSegment=0
* Output: '"recipe":{"ingredients":['
*/
openDownTo(targetContainer, leafSegment) {
let fragment = "";
const startIdx = this.pathStack.length - 1;
for (let i = startIdx; i < targetContainer.length; i++) {
const pathSegment = targetContainer[i];
const parentEntry = this.pathStack[this.pathStack.length - 1];
if (parentEntry.childCount > 0) fragment += ",";
parentEntry.childCount++;
if (typeof pathSegment === "string") fragment += `${JSON.stringify(pathSegment)}:`;
const isArray = typeof (i + 1 < targetContainer.length ? targetContainer[i + 1] : leafSegment) === "number";
fragment += isArray ? "[" : "{";
this.pathStack.push({
segment: pathSegment,
isArray,
childCount: 0
});
}
return fragment;
}
/**
* Emits the comma, key, and value for a leaf entry in the current container.
*
* Input: leafSegment="name", arg={stringValue:"Lasagna"}, valueJson='"Lasagna"'
* Output: '"name":"Lasagna"' (or ',"name":"Lasagna"' if container.childCount > 0)
*/
emitLeaf(leafSegment, arg, valueJson) {
let fragment = "";
const container = this.pathStack[this.pathStack.length - 1];
if (container.childCount > 0) fragment += ",";
container.childCount++;
if (typeof leafSegment === "string") fragment += `${JSON.stringify(leafSegment)}:`;
if (arg.stringValue != null && arg.willContinue) {
fragment += valueJson.slice(0, -1);
this.stringOpen = true;
} else fragment += valueJson;
return fragment;
}
};
/**
* Splits a dotted/bracketed JSON path like `recipe.ingredients[0].name` into segments.
*
* Input: "recipe.ingredients[0].name"
* Output: ["recipe", "ingredients", 0, "name"]
*/
function parsePath(rawPath) {
const segments = [];
for (const part of rawPath.split(".")) {
const bracketIdx = part.indexOf("[");
if (bracketIdx === -1) segments.push(part);
else {
if (bracketIdx > 0) segments.push(part.slice(0, bracketIdx));
for (const m of part.matchAll(/\[(\d+)\]/g)) segments.push(parseInt(m[1], 10));
}
}
return segments;
}
const hasOwn = Object.prototype.hasOwnProperty;
/**
* Checks only direct properties so path traversal never follows the prototype chain.
*/
function hasOwnProperty(obj, key) {
return hasOwn.call(obj, key);
}
/**
* Defines path values as own data properties so special keys like `__proto__`
* cannot invoke prototype setters while accumulating streamed arguments.
*/
function defineOwnProperty(obj, key, value) {
Object.defineProperty(obj, key, {
value,
enumerable: true,
configurable: true,
writable: true
});
}
/**
* Traverses a nested object along the given path segments and returns the leaf value.
*
* Input: ({recipe:{name:"Lasagna"}}, ["recipe","name"])
* Output: "Lasagna"
*/
function getNestedValue(obj, segments) {
let current = obj;
for (const pathSegment of segments) {
if (current == null || typeof current !== "object") return void 0;
const currentRecord = current;
if (!hasOwnProperty(currentRecord, pathSegment)) return void 0;
current = currentRecord[pathSegment];
}
return current;
}
/**
* Normalizes dotted array indexes in place and checks that every array index
* is an existing element or the next one. Arrays are streamed in order, and a
* sparse index would make `finalize()` serialize one `null` per hole, so a tiny
* response could allocate without bound.
* A missing or `null` parent is replaced with a new array, so it starts at 0.
*
* Input: ({items:["a"]}, ["items",1]) or ({items:["a"]}, ["items",5])
* Output: true or false
*/
function normalizeAndCheckIndexes(obj, segments) {
let current = obj;
for (let i = 0; i < segments.length; i++) {
let pathSegment = segments[i];
if (Array.isArray(current) && typeof pathSegment === "string") {
const index = Number(pathSegment);
if (Number.isInteger(index) && index >= 0 && index < 2 ** 32 - 1 && String(index) === pathSegment) {
pathSegment = index;
segments[i] = index;
}
}
if (typeof pathSegment === "number") {
const length = Array.isArray(current) ? current.length : current == null ? 0 : void 0;
if (length !== void 0 && pathSegment > length) return false;
}
current = current != null && typeof current === "object" && hasOwnProperty(current, pathSegment) ? current[pathSegment] : void 0;
}
return true;
}
/**
* Sets a value at a nested path, creating intermediate objects or arrays as needed.
*
* Input: obj={}, segments=["recipe","ingredients",0,"name"], value="Noodles"
* Output: {recipe:{ingredients:[{name:"Noodles"}]}}
*/
function setNestedValue(obj, segments, value) {
let current = obj;
for (let i = 0; i < segments.length - 1; i++) {
const pathSegment = segments[i];
const nextSeg = segments[i + 1];
if (!hasOwnProperty(current, pathSegment) || current[pathSegment] == null) defineOwnProperty(current, pathSegment, typeof nextSeg === "number" ? [] : {});
current = current[pathSegment];
}
defineOwnProperty(current, segments[segments.length - 1], value);
}
/**
* Extracts the first non-null typed value from a partial arg and returns it with its JSON representation.
*
* Input: arg={stringValue:"Boston"} or arg={numberValue:50}
* Output: {value:"Boston", json:'"Boston"'} or {value:50, json:'50'}
*/
function resolvePartialArgValue(arg) {
const value = arg.stringValue ?? arg.numberValue ?? arg.boolValue;
if (value != null) return {
value,
json: JSON.stringify(value)
};
if ("nullValue" in arg) return {
value: null,
json: "null"
};
}
//#endregion
//#region src/map-google-finish-reason.ts
function mapGoogleFinishReason({ finishReason, hasToolCalls }) {
switch (finishReason) {
case "STOP": return hasToolCalls ? "tool-calls" : "stop";
case "MAX_TOKENS":
case "CONTINUATION": return "length";
case "IMAGE_SAFETY":
case "RECITATION":
case "SAFETY":
case "BLOCKLIST":
case "PROHIBITED_CONTENT":
case "SPII": return "content-filter";
case "MALFORMED_FUNCTION_CALL": return "error";
default: return "other";
}
}
//#endregion
//#region src/google-language-model.ts
const configurableSafetySettingCategories = [
"HARM_CATEGORY_HATE_SPEECH",
"HARM_CATEGORY_DANGEROUS_CONTENT",
"HARM_CATEGORY_HARASSMENT",
"HARM_CATEGORY_SEXUALLY_EXPLICIT"
];
const googleCloudStorageFunctionResponseUrls = {
"image/png": [/^gs:\/\/.*$/],
"image/jpeg": [/^gs:\/\/.*$/],
"image/webp": [/^gs:\/\/.*$/],
"application/pdf": [/^gs:\/\/.*$/],
"text/plain": [/^gs:\/\/.*$/]
};
var GoogleLanguageModel = class GoogleLanguageModel {
static [WORKFLOW_SERIALIZE](model) {
return serializeModelOptions({
modelId: model.modelId,
config: model.config
});
}
static [WORKFLOW_DESERIALIZE](options) {
return new GoogleLanguageModel(options.modelId, options.config);
}
constructor(modelId, config) {
this.specificationVersion = "v4";
this.modelId = modelId;
this.config = config;
this.generateId = config.generateId ?? generateId;
}
get provider() {
return this.config.provider;
}
get supportedUrls() {
return this.config.supportedUrls?.() ?? {};
}
static async prepareRequest({ modelId, config, options: { prompt, maxOutputTokens, temperature, topP, topK, frequencyPenalty, presencePenalty, stopSequences, responseFormat, seed, tools, toolChoice, reasoning, providerOptions, abortSignal }, isStreaming = false }) {
const warnings = [];
const providerOptionsNames = config.provider.includes("vertex") ? ["googleVertex", "vertex"] : ["google"];
const { options: googleOptions } = await resolveProviderOptions({
providerOptions,
namespaces: [...providerOptionsNames, "google"],
resolution: "first",
schema: googleLanguageModelOptions
});
const isVertexProvider = config.provider.startsWith("google.vertex.");
if (tools?.some((tool) => tool.type === "provider" && tool.id === "google.vertex_rag_store") && !isVertexProvider) warnings.push({
type: "other",
message: `The 'vertex_rag_store' tool is only supported with the Google Vertex provider and might not be supported or could behave unexpectedly with the current Google provider (${config.provider}).`
});
if (googleOptions?.streamFunctionCallArguments && !isVertexProvider) warnings.push({
type: "other",
message: `'streamFunctionCallArguments' is only supported on the Vertex AI API and will be ignored with the current Google provider (${config.provider}). See https://docs.cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling#streaming-fc`
});
if (googleOptions?.serviceTier && isVertexProvider) warnings.push({
type: "other",
message: "'serviceTier' is a Gemini API option and is not supported on Vertex AI. Use 'sharedRequestType' (and optionally 'requestType') instead. See https://docs.cloud.google.com/vertex-ai/generative-ai/docs/priority-paygo"
});
if ((googleOptions?.sharedRequestType || googleOptions?.requestType) && !isVertexProvider) warnings.push({
type: "other",
message: `'sharedRequestType' and 'requestType' are Vertex AI options and are ignored with the current Google provider (${config.provider}).`
});
const vertexPaygoHeaders = isVertexProvider && (googleOptions?.sharedRequestType || googleOptions?.requestType) ? {
...googleOptions.sharedRequestType && { "X-Vertex-AI-LLM-Shared-Request-Type": googleOptions.sharedRequestType },
...googleOptions.requestType && { "X-Vertex-AI-LLM-Request-Type": googleOptions.requestType }
} : void 0;
const bodyServiceTier = isVertexProvider ? void 0 : googleOptions?.serviceTier;
let imageConfig = googleOptions?.imageConfig;
if (imageConfig != null && !isVertexProvider) {
const { personGeneration, prominentPeople, imageOutputOptions, ...geminiApiImageConfig } = imageConfig;
const droppedImageConfigFields = Object.entries({
personGeneration,
prominentPeople,
imageOutputOptions
}).filter(([, value]) => value != null).map(([key]) => `'imageConfig.${key}'`);
if (droppedImageConfigFields.length > 0) {
warnings.push({
type: "other",
message: `${droppedImageConfigFields.join(", ")} ${droppedImageConfigFields.length === 1 ? "is a Vertex AI option and is" : "are Vertex AI options and are"} ignored with the current Google provider (${config.provider}).`
});
imageConfig = geminiApiImageConfig;
}
}
const entry = getGoogleLanguageModelEntry(modelId);
const isGemmaModel = entry.requestConfig.language.systemInstruction === "user";
const isGemini25DeveloperApiModel = !isVertexProvider && entry.capabilities.language.sampling.penalties !== true;
if (isGemini25DeveloperApiModel && frequencyPenalty != null) warnings.push({
type: "unsupported",
feature: "frequencyPenalty"
});
if (isGemini25DeveloperApiModel && presencePenalty != null) warnings.push({
type: "unsupported",
feature: "presencePenalty"
});
const usesGemini3Features = entry.capabilities.language.tools.functionResponseParts;
const supportedFunctionResponseUrls = usesGemini3Features && config.downloadToolResultFiles?.supportsGoogleCloudStorageUrls ? googleCloudStorageFunctionResponseUrls : void 0;
const { contents, systemInstruction } = convertToGoogleMessages(config.downloadToolResultFiles ? await downloadToolResultFiles(prompt, {
abortSignal,
maxBytes: config.downloadToolResultFiles.maxBytes,
fetch: config.downloadToolResultFiles.fetch,
supportedUrls: supportedFunctionResponseUrls
}) : prompt, {
isGemmaModel,
isGemini3Model: usesGemini3Features,
onWarning: (warning) => warnings.push(warning),
providerOptionsNames,
supportsFunctionResponseParts: usesGemini3Features,
includeFunctionCallIds: !isVertexProvider,
supportedFunctionResponseUrls
});
const { tools: googleTools, toolConfig: googleToolConfig, toolWarnings } = prepareTools({
tools,
toolChoice,
modelId,
isVertexProvider
});
const toolNameMapping = createToolNameMapping({
tools,
providerToolNames: { "google.code_execution": "code_execution" }
});
const resolvedThinking = googleOptions?.thinkingConfig?.thinkingBudget != null || googleOptions?.thinkingConfig?.thinkingLevel != null ? void 0 : resolveThinkingConfig({
reasoning,
modelId,
warnings
});
const thinkingConfig = googleOptions?.thinkingConfig || resolvedThinking ? {
...resolvedThinking,
...googleOptions?.thinkingConfig
} : void 0;
const streamFunctionCallArguments = isStreaming && isVertexProvider ? googleOptions?.streamFunctionCallArguments ?? false : void 0;
const safetyThreshold = googleOptions?.threshold;
const safetySettings = googleOptions?.safetySettings ?? (safetyThreshold != null ? configurableSafetySettingCategories.map((category) => ({
category,
threshold: safetyThreshold
})) : void 0);
const toolConfig = googleToolConfig || streamFunctionCallArguments || googleOptions?.retrievalConfig ? {
...googleToolConfig,
...streamFunctionCallArguments && { functionCallingConfig: {
...googleToolConfig?.functionCallingConfig,
streamFunctionCallArguments: true
} },
...googleOptions?.retrievalConfig && { retrievalConfig: googleOptions.retrievalConfig }
} : void 0;
return {
args: {
generationConfig: {
maxOutputTokens,
temperature,
topK,
topP,
frequencyPenalty: isGemini25DeveloperApiModel ? void 0 : frequencyPenalty,
presencePenalty: isGemini25DeveloperApiModel ? void 0 : presencePenalty,
stopSequences,
seed,
responseMimeType: responseFormat?.type === "json" ? "application/json" : void 0,
responseJsonSchema: responseFormat?.type === "json" && responseFormat.schema != null && (googleOptions?.structuredOutputs ?? true) ? sanitizeResponseJsonSchema(responseFormat.schema) : void 0,
...googleOptions?.audioTimestamp && { audioTimestamp: googleOptions.audioTimestamp },
responseModalities: googleOptions?.responseModalities,
thinkingConfig,
...googleOptions?.mediaResolution && { mediaResolution: googleOptions.mediaResolution },
...imageConfig && { imageConfig }
},
contents,
systemInstruction: isGemmaModel ? void 0 : systemInstruction,
safetySettings,
tools: googleTools,
toolConfig,
cachedContent: googleOptions?.cachedContent,
labels: googleOptions?.labels,
serviceTier: bodyServiceTier,
...googleOptions?.continuationToken != null && { continuationToken: googleOptions.continuationToken }
},
warnings: [...warnings, ...toolWarnings],
providerOptionsNames,
extraHeaders: vertexPaygoHeaders,
toolNameMapping
};
}
async getArgs(options, { isStreaming = false } = {}) {
const { responseFormat, toolChoice } = options;
const tools = toolChoice?.type === "none" ? options.tools?.filter((tool) => tool.type === "function") : options.tools;
const entry = getGoogleLanguageModelEntry(this.modelId);
const { structuredOutput } = entry.capabilities.language;
let jsonResponseTool;
if (responseFormat?.type === "json" && responseFormat.schema != null && tools?.some((tool) => tool.type === "function") && entry.requestConfig.language.jsonResponseTool && (!structuredOutput.withFunctionTools || toolChoice?.type === "required" || toolChoice?.type === "tool")) {
let name = "json";
for (let suffix = 1; tools.some((tool) => tool.name === name); suffix++) name = `json_${suffix}`;
jsonResponseTool = {
type: "function",
name,
description: responseFormat.description ?? "Respond with a JSON object.",
inputSchema: responseFormat.schema
};
}
return {
...await GoogleLanguageModel.prepareRequest({
modelId: this.modelId,
config: this.config,
options: jsonResponseTool == null ? options : {
...options,
responseFormat: void 0,
tools: [...tools ?? [], jsonResponseTool],
toolChoice: toolChoice?.type === "tool" ? toolChoice : toolChoice?.type === "none" ? {
type: "tool",
toolName: jsonResponseTool.name
} : { type: "required" }
},
isStreaming
}),
jsonResponseToolName: jsonResponseTool?.name
};
}
static convertGenerateContentResponse({ config, response, responseHeaders, warnings, providerOptionsNames, toolNameMapping, jsonResponseToolName, initialPromptTokenCount, groundingMetadataPerRequest }) {
const wrapProviderMetadata = (payload) => Object.fromEntries(providerOptionsNames.map((name) => [name, payload]));
const candidate = response.candidates?.[0];
const promptBlockReason = response.promptFeedback?.blockReason;
const confirmedPromptBlockReason = isConfirmedPromptBlockReason(promptBlockReason) ? promptBlockReason : void 0;
const isPromptBlocked = candidate?.finishReason == null && confirmedPromptBlockReason != null;
const rawFinishReason = candidate?.finishReason ?? confirmedPromptBlockReason;
const content = [];
const parts = candidate?.content?.parts ?? [];
const usageMetadata = response.usageMetadata;
let lastCodeExecutionToolCallId;
let lastServerToolCallId;
for (const part of parts) if ("executableCode" in part && part.executableCode?.code) {
const toolCallId = config.generateId();
lastCodeExecutionToolCallId = toolCallId;
content.push({
type: "tool-call",
toolCallId,
toolName: toolNameMapping?.toCustomToolName("code_execution") ?? "code_execution",
input: JSON.stringify(part.executableCode),
providerExecuted: true
});
} else if ("codeExecutionResult" in part && part.codeExecutionResult) content.push({
type: "tool-result",
toolCallId: lastCodeExecutionToolCallId,
toolName: toolNameMapping?.toCustomToolName("code_execution") ?? "code_execution",
result: {
outcome: part.codeExecutionResult.outcome,
output: part.codeExecutionResult.output ?? ""
}
});
else if ("text" in part && part.text != null) {
if (jsonResponseToolName != null && part.thought !== true) continue;
const thoughtSignatureMetadata = part.thoughtSignature ? wrapProviderMetadata({ thoughtSignature: part.thoughtSignature }) : void 0;
if (part.text.length === 0) {
if (thoughtSignatureMetadata != null && content.length > 0) {
const lastContent = content[content.length - 1];
lastContent.providerMetadata = thoughtSignatureMetadata;
}
} else content.push({
type: part.thought === true ? "reasoning" : "text",
text: part.text,
providerMetadata: thoughtSignatureMetadata
});
} else if ("functionCall" in part && part.functionCall.name != null) {
if (part.functionCall.name === jsonResponseToolName) {
content.push({
type: "text",
text: typeof part.functionCall.args === "string" ? part.functionCall.args : JSON.stringify(part.functionCall.args ?? {}),
...part.thoughtSignature ? { providerMetadata: wrapProviderMetadata({ thoughtSignature: part.thoughtSignature }) } : {}
});
continue;
}
content.push({
type: "tool-call",
toolCallId: part.functionCall.id || config.generateId(),
toolName: part.functionCall.name,
input: JSON.stringify(part.functionCall.args ?? {}),
providerMetadata: part.thoughtSignature ? wrapProviderMetadata({ thoughtSignature: part.thoughtSignature }) : void 0
});
} else if ("inlineData" in part) {
const hasThought = part.thought === true;
const hasThoughtSignature = !!part.thoughtSignature;
content.push({
type: hasThought ? "reasoning-file" : "file",
data: {
type: "data",
data: part.inlineData.data
},
mediaType: part.inlineData.mimeType,
providerMetadata: hasThoughtSignature ? wrapProviderMetadata({ thoughtSignature: part.thoughtSignature }) : void 0
});
} else if ("toolCall" in part && part.toolCall) {
const toolCallId = part.toolCall.id || config.generateId();
lastServerToolCallId = toolCallId;
content.push({
type: "tool-call",
toolCallId,
toolName: `server:${part.toolCall.toolType}`,
input: JSON.stringify(part.toolCall.args ?? {}),
providerExecuted: true,
dynamic: true,
providerMetadata: part.thoughtSignature ? wrapProviderMetadata({
thoughtSignature: part.thoughtSignature,
serverToolCallId: toolCallId,
serverToolType: part.toolCall.toolType
}) : wrapProviderMetadata({
serverToolCallId: toolCallId,
serverToolType: part.toolCall.toolType
})
});
} else if ("toolResponse" in part && part.toolResponse) {
const responseToolCallId = lastServerToolCallId || part.toolResponse.id || config.generateId();
content.push({
type: "tool-result",
toolCallId: responseToolCallId,
toolName: `server:${part.toolResponse.toolType}`,
result: part.toolResponse.response ?? {},
providerMetadata: part.thoughtSignature ? wrapProviderMetadata({
thoughtSignature: part.thoughtSignature,
serverToolCallId: responseToolCallId,
serverToolType: part.toolResponse.toolType
}) : wrapProviderMetadata({
serverToolCallId: responseToolCallId,
serverToolType: part.toolResponse.toolType
})
});
lastServerToolCallId = void 0;
}
const sources = extractSources({
groundingMetadata: candidate?.groundingMetadata,
generateId: config.generateId
}) ?? [];
for (const source of sources) content.push(source);
return {
content,
finishReason: {
unified: isPromptBlocked ? "content-filter" : mapGoogleFinishReason({
finishReason: rawFinishReason,
hasToolCalls: content.some((part) => part.type === "tool-call" && !part.providerExecuted)
}),
raw: rawFinishReason
},
usage: convertGoogleUsage(usageMetadata),
warnings,
providerMetadata: wrapProviderMetadata({
promptFeedback: response.promptFeedback ?? null,
groundingMetadata: candidate?.groundingMetadata ?? null,
...groundingMetadataPerRequest != null ? { groundingMetadataPerRequest } : {},
urlContextMetadata: candidate?.urlContextMetadata ?? null,
safetyRatings: candidate?.safetyRatings ?? null,
usageMetadata: usageMetadata ?? null,
finishMessage: candidate?.finishMessage ?? null,
serviceTier: usageMetadata?.serviceTier ?? responseHeaders?.["x-gemini-service-tier"] ?? null,
...candidate?.continuationToken != null ? { continuationToken: candidate.continuationToken } : {},
...initialPromptTokenCount != null ? { initialPromptTokenCount } : {}
}),
response: { id: response.responseId ?? void 0 }
};
}
async doGenerate(options) {
const { args, warnings, providerOptionsNames, extraHeaders, toolNameMapping, jsonResponseToolName } = await this.getArgs(options);
const generateContent = async (body) => postJsonToApi({
url: `${this.config.baseURL}/${getModelPath(this.modelId)}:generateContent`,
headers: combineHeaders(await resolve(this.config.headers), options.headers, extraHeaders),
body,
failedResponseHandler: googleFailedResponseHandler,
successfulResponseHandler: createJsonResponseHandler(responseSchema),
abortSignal: options.abortSignal,
fetch: this.config.fetch
});
let { responseHeaders, value: response, rawValue: rawResponse } = await generateContent(args);
const content = [];
const groundingMetadataPerRequest = [];
let usageMetadata;
const firstPromptTokenCount = response.usageMetadata?.promptTokenCount;
let isContinued = false;
while (true) {
const candidate = response.candidates?.[0];
groundingMetadataPerRequest.push(candidate?.groundingMetadata ?? null);
const continuationToken = candidate?.finishReason === "CONTINUATION" ? candidate.continuationToken : void 0;
usageMetadata = addGoogleUsageMetadata(usageMetadata, response.usageMetadata ?? void 0);
if (continuationToken == null) break;
content.push(...GoogleLanguageModel.convertGenerateContentResponse({
config: this.config,
response,
warnings,
providerOptionsNames,
toolNameMapping,
jsonResponseToolName
}).content);
isContinued = true;
({responseHeaders, value: response, rawValue: rawResponse} = await generateContent({
...args,
continuationToken
}));
}
const lastResult = GoogleLanguageModel.convertGenerateContentResponse({
config: this.config,
response: {
...response,
usageMetadata
},
responseHeaders,
warnings,
providerOptionsNames,
toolNameMapping,
jsonResponseToolName,
initialPromptTokenCount: isContinued ? firstPromptTokenCount : void 0,
groundingMetadataPerRequest: isContinued ? groundingMetadataPerRequest : void 0
});
const result = {
...lastResult,
content: [...content, ...lastResult.content]
};
return {
...result,
request: { body: args },
response: {
...result.response,
headers: responseHeaders,
body: rawResponse
}
};
}
async doStream(options) {
const { args, warnings, providerOptionsNames, extraHeaders, toolNameMapping, jsonResponseToolName } = await this.getArgs(options, { isStreaming: true });
const wrapProviderMetadata = (payload) => Object.fromEntries(providerOptionsNames.map((name) => [name, payload]));
const streamGenerateContent = async (body) => postJsonToApi({
url: `${this.config.baseURL}/${getModelPath(this.modelId)}:streamGenerateContent?alt=sse`,
headers: combineHeaders(await resolve(this.config.headers), options.headers, extraHeaders),
body,
failedResponseHandler: googleFailedResponseHandler,
successfulResponseHandler: createEventSourceResponseHandler(chunkSchema),
abortSignal: options.abortSignal,
fetch: this.config.fetch
});
const { responseHeaders, value: firstResponse } = await streamGenerateContent(args);
const groundingMetadataPerRequest = [];
let requestGroundingMetadata = null;
const response = continueGoogleStream({
stream: firstResponse,
getContinuationToken: (chunk) => {
const candidate = chunk.candidates?.[0];
if (candidate?.groundingMetadata != null) requestGroundingMetadata = candidate.groundingMetadata;
return candidate?.finishReason === "CONTINUATION" ? candidate.continuationToken ?? void 0 : void 0;
},
continueStream: async (continuationToken) => {
const { value } = await streamGenerateContent({
...args,
continuationToken
});
groundingMetadataPerRequest.push(requestGroundingMetadata);
requestGroundingMetadata = null;
return value;
}
});
let finishReason = {
unified: "interrupted",
raw: void 0
};
let usage = void 0;
let continuedUsage = void 0;
let initialPromptTokenCount = void 0;
let promptFeedback = null;
let lastGroundingMetadata = null;
let lastUrlContextMetadata = null;
let lastSafetyRatings = null;
let lastFinishMessage = null;
let lastContinuationToken = null;
let confirmedPromptBlockReason;
const generateId = this.config.generateId;
let hasToolCalls = false;
let hasEmittedResponseMetadata = false;
let currentTextBlockId = null;
let currentReasoningBlockId = null;
let blockCounter = 0;
const emittedSourceKeys = /* @__PURE__ */ new Set();
let lastCodeExecutionToolCallId;
let lastServerToolCallId;
const activeStreamingToolCalls = [];
const finishActiveStreamingToolCall = (controller) => {
const active = activeStreamingToolCalls.pop();
if (active == null) return;
if (currentTextBlockId !== null) {
controller.enqueue({
type: "text-end",
id: currentTextBlockId
});
currentTextBlockId = null;
}
if (currentReasoningBlockId !== null) {
controller.enqueue({
type: "reasoning-end",
id: currentReasoningBlockId
});
currentReasoningBlockId = null;
}
const { finalJSON, closingDelta } = active.accumulator.finalize();
if (closingDelta.length > 0) controller.enqueue({
type: "tool-input-delta",
id: active.toolCallId,
delta: closingDelta,
providerMetadata: active.providerMetadata
});
controller.enqueue({
type: "tool-input-end",
id: active.toolCallId,
providerMetadata: active.providerMetadata
});
controller.enqueue({
type: "tool-call",
toolCallId: active.toolCallId,
toolName: active.toolName,
input: finalJSON,
providerMetadata: active.providerMetadata
});
hasToolCalls = true;
};
const result = {
stream: response.pipeThrough(new TransformStream({
start(controller) {
controller.enqueue({
type: "stream-start",
warnings
});
},
transform(chunk, controller) {
if ("type" in chunk) {
finishReason = {
unified: "interrupted",
raw: void 0
};
return;
}
if (options.includeRawChunks) controller.enqueue({
type: "raw",
rawValue: chunk.rawValue
});
if (!chunk.success) {
if (finishReason.unified === "interrupted") finishReason = {
unified: "error",
raw: void 0
};
controller.enqueue({
type: "error",
error: chunk.error
});
return;
}
const value = chunk.value;
if (value.error != null) {
finishReason = {
unified: "error",
raw: void 0
};
const error = value.error;
const statusCode = error.code ?? mapGoogleErrorStatusToHttpStatusCode(error.status);
controller.enqueue({
type: "error",
error: createProviderStreamError({
message: error.message,
type: error.status,
code: error.code ?? void 0,
statusCode,
isRetryable: statusCode != null && (statusCode === 408 || statusCode === 409 || statusCode === 429 || statusCode >= 500),
data: error
})
});
return;
}
if (!hasEmittedResponseMetadata && value.responseId != null) {
hasEmittedResponseMetadata = true;
controller.enqueue({
type: "response-metadata",
id: value.responseId
});
}
const usageMetadata = value.usageMetadata;
if (usageMetadata != null) usage = usageMetadata;
if (value.promptFeedback != null && confirmedPromptBlockReason == null) {
promptFeedback = value.promptFeedback;
if (isConfirmedPromptBlockReason(value.promptFeedback.blockReason)) {
confirmedPromptBlockReason = value.promptFeedback.blockReason;
finishReason = {
unified: "content-filter",
raw: confirmedPromptBlockReason
};
}
}
const candidate = value.candidates?.[0];
if (candidate != null) {
if (candidate.groundingMetadata != null) lastGroundingMetadata = candidate.groundingMetadata;
if (candidate.urlContextMetadata != null) lastUrlContextMetadata = candidate.urlContextMetadata;
if (candidate.safetyRatings != null) lastSafetyRatings = candidate.safetyRatings;
if (candidate.finishMessage != null) lastFinishMessage = candidate.finishMessage;
if (candidate.continuationToken != null) lastContinuationToken = candidate.continuationToken;
}
if (confirmedPromptBlockReason != null || candidate == null) return;
const content = candidate.content;
const sources = extractSources({
groundingMetadata: candidate.groundingMetadata,
generateId,
emittedSourceKeys
});
if (sources != null) for (const source of sources) controller.enqueue(source);
if (content != null) {
const parts = content.parts ?? [];
for (const part of parts) if ("executableCode" in part && part.executableCode?.code) {
const toolCallId = generateId();
lastCodeExecutionToolCallId = toolCallId;
controller.enqueue({
type: "tool-call",
toolCallId,
toolName: toolNameMapping.toCustomToolName("code_execution"),
input: JSON.stringify(part.executableCode),
providerExecuted: true
});
} else if ("codeExecutionResult" in part && part.codeExecutionResult) {
const toolCallId = lastCodeExecutionToolCallId;
if (toolCallId) controller.enqueue({
type: "tool-result",
toolCallId,
toolName: toolNameMapping.toCustomToolName("code_execution"),
result: {
outcome: part.codeExecutionResult.outcome,
output: part.codeExecutionResult.output ?? ""
}
});
} else if ("text" in part && part.text != null) {
const thoughtSignatureMetadata = part.thoughtSignature ? wrapProviderMetadata({ thoughtSignature: part.thoughtSignature }) : void 0;
if (part.text.length === 0) {
if (thoughtSignatureMetadata != null) {
if (currentTextBlockId !== null) controller.enqueue({
type: "text-delta",
id: currentTextBlockId,
delta: "",
providerMetadata: thoughtSignatureMetadata
});
else {
if (currentReasoningBlockId !== null) {
controller.enqueue({
type: "reasoning-end",
id: currentReasoningBlockId
});
currentReasoningBlockId = null;
}
const id = String(blockCounter++);
const type = part.thought === true ? "reasoning" : "text";
controller.enqueue({
type: `${type}-start`,
id,
providerMetadata: thoughtSignatureMetadata
});
controller.enqueue({
type: `${type}-delta`,
id,
delta: "",
providerMetadata: thoughtSignatureMetadata
});
controller.enqueue({
type: `${type}-end`,
id
});
}
}
} else if (part.thought === true) {
if (currentTextBlockId !== null) {
controller.enqueue({
type: "text-end",
id: currentTextBlockId
});
currentTextBlockId = null;
}
if (currentReasoningBlockId === null) {
currentReasoningBlockId = String(blockCounter++);
controller.enqueue({
type: "reasoning-start",
id: currentReasoningBlockId,
providerMetadata: thoughtSignatureMetadata
});
}
controller.enqueue({
type: "reasoning-delta",
id: currentReasoningBlockId,
delta: part.text,
providerMetadata: thoughtSignatureMetadata
});
} else {
if (currentReasoningBlockId !== null) {
controller.enqueue({
type: "reasoning-end",
id: currentReasoningBlockId
});
currentReasoningBlockId = null;
}
if (currentTextBlockId === null) {
currentTextBlockId = String(blockCounter++);
controller.enqueue({
type: "text-start",
id: currentTextBlockId,
providerMetadata: thoughtSignatureMetadata
});
}
controller.enqueue({
type: "text-delta",
id: currentTextBlockId,
delta: part.text,
providerMetadata: thoughtSignatureMetadata
});
}
} else if ("inlineData" in part) {
if (currentTextBlockId !== null) {
controller.enqueue({
type: "text-end",
id: currentTextBlockId
});
currentTextBlockId = null;
}
if (currentReasoningBlockId !== null) {
controller.enqueue({
type: "reasoning-end",
id: currentReasoningBlockId
});
currentReasoningBlockId = null;
}
const hasThought = part.thought === true;
const fileMeta = !!part.thoughtSignature ? wrapProviderMetadata({ thoughtSignature: part.thoughtSignature }) : void 0;
controller.enqueue({
type: hasThought ? "reasoning-file" : "file",
mediaType: part.inlineData.mimeType,
data: {
type: "data",
data: part.inlineData.data
},
providerMetadata: fileMeta
});
} else if ("toolCall" in part && part.toolCall) {
const toolCallId = part.toolCall.id || generateId();
lastServerToolCallId = toolCallId;
const serverMeta = wrapProviderMetadata({
...part.thoughtSignature ? { thoughtSignature: part.thoughtSignature } : {},
serverToolCallId: toolCallId,
serverToolType: part.toolCall.toolType
});
controller.enqueue({
type: "tool-call",
toolCallId,
toolName: `server:${part.toolCall.toolType}`,
input: JSON.stringify(part.toolCall.args ?? {}),
providerExecuted: true,
dynamic: true,
providerMetadata: serverMeta
});
} else if ("toolResponse" in part && part.toolResponse) {
const responseToolCallId = lastServerToolCallId || part.toolResponse.id || generateId();
const serverMeta = wrapProviderMetadata({
...part.thoughtSignature ? { thoughtSignature: part.thoughtSignature } : {},
serverToolCallId: responseToolCallId,
serverToolType: part.toolResponse.toolType
});
controller.enqueue({
type: "tool-result",
toolCallId: responseToolCallId,
toolName: `server:${part.toolResponse.toolType}`,
result: part.toolResponse.response ?? {},
providerMetadata: serverMeta
});
lastServerToolCallId = void 0;
} else if ("functionCall" in part) {
const providerMeta = part.thoughtSignature ? wrapProviderMetadata({ thoughtSignature: part.thoughtSignature }) : void 0;
const isStreamingChunk = part.functionCall.partialArgs != null || part.functionCall.name != null && part.functionCall.willContinue === true;
const isTerminalChunk = part.functionCall.name == null && part.functionCall.args == null && part.functionCall.partialArgs == null && part.functionCall.willContinue == null;
const isCompleteCall = part.functionCall.name != null && part.functionCall.args != null && part.functionCall.partialArgs == null;
const isNoArgsCompleteCall = part.functionCall.name != null && part.functionCall.args == null && part.functionCall.partialArgs == null && part.functionCall.willContinue !== true;
if (!isStreamingChunk && (isCompleteCall || isNoArgsCompleteCall)) {
if (currentTextBlockId !== null) {
controller.enqueue({
type: "text-end",
id: currentTextBlockId
});
currentTextBlockId = null;
}
if (currentReasoningBlockId !== null) {
controller.enqueue({
type: "reasoning-end",
id: currentReasoningBlockId
});
currentReasoningBlockId = null;
}
}
if (isStreamingChunk) {
if (part.functionCall.name != null) {
const toolCallId = part.functionCall.id || generateId();
const accumulator = new GoogleJSONAccumulator();
activeStreamingToolCalls.push({
toolCallId,
toolName: part.functionCall.name,
accumulator,
providerMetadata: providerMeta
});
controller.enqueue({
type: "tool-input-start",
id: toolCallId,
toolName: part.functionCall.name,
providerMetadata: providerMeta
});
if (part.functionCall.partialArgs != null) {
const partialArgs = part.functionCall.partialArgs;
const { textDelta } = accumulator.processPartialArgs(partialArgs);
if (textDelta.length > 0) controller.enqueue({
type: "tool-input-delta",
id: toolCallId,
delta: textDelta,
providerMetadata: providerMeta
});
if (part.functionCall.willContinue !== true && partialArgs.every((arg) => arg.willContinue !== true)) finishActiveStreamingToolCall(controller);
}
} else if (part.functionCall.partialArgs != null && activeStreamingToolCalls.length > 0) {
const active = activeStreamingToolCalls[activeStreamingToolCalls.length - 1];
const partialArgs = part.functionCall.partialArgs;
const { textDelta } = active.accumulator.processPartialArgs(partialArgs);
if (textDelta.length > 0) controller.enqueue({
type: "tool-input-delta",
id: active.toolCallId,
delta: textDelta,
providerMetadata: providerMeta
});
if (part.functionCall.willContinue !== true && partialArgs.every((arg) => arg.willContinue !== true)) finishActiveStreamingToolCall(controller);
}
} else if (isTerminalChunk && activeStreamingToolCalls.length > 0) finishActiveStreamingToolCall(controller);
else if (isCompleteCall) {
const toolCallId = part.functionCall.id || generateId();
const toolName = part.functionCall.name;
const args = typeof part.functionCall.args === "string" ? part.functionCall.args : JSON.stringify(part.functionCall.args ?? {});
controller.enqueue({
type: "tool-input-start",
id: toolCallId,
toolName,
providerMetadata: providerMeta
});
controller.enqueue({
type: "tool-input-delta",
id: toolCallId,
delta: args,
providerMetadata: providerMeta
});
controller.enqueue({
type: "tool-input-end",
id: toolCallId,
providerMetadata: providerMeta
});
controller.enqueue({
type: "tool-call",
toolCallId,
toolName,
input: args,
providerMetadata: providerMeta
});
hasToolCalls = true;
} else if (isNoArgsCompleteCall) {
const toolCallId = part.functionCall.id || generateId();
const toolName = part.functionCall.name;
controller.enqueue({
type: "tool-input-start",
id: toolCallId,
toolName,
providerMetadata: providerMeta
});
controller.enqueue({
type: "tool-input-end",
id: toolCallId,
providerMetadata: providerMeta
});
controller.enqueue({
type: "tool-call",
toolCallId,
toolName,
input: "{}",
providerMetadata: providerMeta
});
hasToolCalls = true;
}
}
}
if (candidate.finishReason === "CONTINUATION") {
if (continuedUsage == null) initialPromptTokenCount = usage?.promptTokenCount;
continuedUsage = addGoogleUsageMetadata(continuedUsage, usage);
usage = void 0;
}
if (candidate.finishReason != null) finishReason = {
unified: mapGoogleFinishReason({
finishReason: candidate.finishReason,
hasToolCalls
}),
raw: candidate.finishReason
};
},
flush(controller) {
usage = addGoogleUsageMetadata(continuedUsage, usage);
if (currentTextBlockId !== null) controller.enqueue({
type: "text-end",
id: currentTextBlockId
});
if (currentReasoningBlockId !== null) controller.enqueue({
type: "reasoning-end",
id: currentReasoningBlockId
});
controller.enqueue({
type: "finish",
finishReason,
usage: convertGoogleUsage(usage),
providerMetadata: wrapProviderMetadata({
promptFeedback,
groundingMetadata: lastGroundingMetadata,
...groundingMetadataPerRequest.length > 0 ? { groundingMetadataPerRequest: [...groundingMetadataPerRequest, requestGroundingMetadata] } : {},
urlContextMetadata: lastUrlContextMetadata,
safetyRatings: lastSafetyRatings,
usageMetadata: usage ?? null,
finishMessage: lastFinishMessage,
serviceTier: usage?.serviceTier ?? responseHeaders?.["x-gemini-service-tier"] ?? null,
...lastContinuationToken != null && (finishReason.raw === "MAX_TOKENS" || finishReason.raw === "CONTINUATION") ? { continuationToken: lastContinuationToken } : {},
...initialPromptTokenCount != null ? { initialPromptTokenCount } : {}
})
});
}
})),
response: { headers: responseHeaders },
request: { body: args }
};
return {
...result,
stream: jsonResponseToolName == null ? result.stream : result.stream.pipeThrough(convertJsonResponseToolStream(jsonResponseToolName))
};
}
};
function resolveThinkingConfig({ reasoning, modelId, warnings }) {
if (!isCustomReasoning(reasoning)) return;
if (getGoogleLanguageModelEntry(modelId).requestConfig.language.thinkingFormat === "level") return resolveGemini3ThinkingConfig({
reasoning,
modelId,
warnings
});
return resolveGemini25ThinkingConfig({
reasoning,
modelId,
warnings
});
}
function resolveGemini3ThinkingConfig({ reasoning, modelId, warnings }) {
const thinkingLevels = getGoogleLanguageModelEntry(modelId).capabilities.language.reasoning.efforts;
const thinkingLevel = mapReasoningToProviderEffort({
reasoning,
effortMap: Object.fromEntries((thinkingLevels ?? []).map((level) => [level, level])),
warnings
});
if (thinkingLevel == null) return;
return { thinkingLevel };
}
function resolveGemini25ThinkingConfig({ reasoning, modelId, warnings }) {
const entry = getGoogleLanguageModelEntry(modelId);
const minThinkingBudget = entry.capabilities.language.reasoning.minBudget;
if (reasoning === "none" && minThinkingBudget === 0) return { thinkingBudget: 0 };
return { thinkingBudget: mapReasoningToProviderBudget({
reasoning,
maxOutputTokens: entry.requestConfig.language.reasoningMaxOutputTokens,
maxReasoningBudget: entry.capabilities.language.reasoning.maxBudget,
minReasoningBudget: minThinkingBudget,
warnings
}) };
}
function extractSources({ groundingMetadata, generateId, emittedSourceKeys }) {
if (!groundingMetadata?.groundingChunks) return;
const sources = [];
const addSource = (source, key) => {
if (emittedSourceKeys?.has(key)) return;
emittedSourceKeys?.add(key);
sources.push(source);
};
for (const chunk of groundingMetadata.groundingChunks) if (chunk.web != null) addSource({
type: "source",
sourceType: "url",
id: generateId(),
url: chunk.web.uri,
title: chunk.web.title ?? void 0
}, JSON.stringify(["url", chunk.web.uri]));
else if (chunk.image != null) addSource({
type: "source",
sourceType: "url",
id: generateId(),
url: chunk.image.sourceUri,
title: chunk.image.title ?? void 0
}, JSON.stringify(["url", chunk.image.sourceUri]));
else if (chunk.retrievedContext != null) {
const uri = chunk.retrievedContext.uri;
const fileSearchStore = chunk.retrievedContext.fileSearchStore;
if (uri && (uri.startsWith("http://") || uri.startsWith("https://"))) addSource({
type: "source",
sourceType: "url",
id: generateId(),
url: uri,
title: chunk.retrievedContext.title ?? void 0
}, JSON.stringify(["url", uri]));
else if (uri) {
const title = chunk.retrievedContext.title ?? "Unknown Document";
let mediaType = "application/octet-stream";
let filename = void 0;
if (uri.endsWith(".pdf")) {
mediaType = "application/pdf";
filename = uri.split("/").pop();
} else if (uri.endsWith(".txt")) {
mediaType = "text/plain";
filename = uri.split("/").pop();
} else if (uri.endsWith(".docx")) {
mediaType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
filename = uri.split("/").pop();
} else if (uri.endsWith(".doc")) {
mediaType = "application/msword";
filename = uri.split("/").pop();
} else if (uri.match(/\.(md|markdown)$/)) {
mediaType = "text/markdown";
filename = uri.split("/").pop();
} else filename = uri.split("/").pop();
addSource({
type: "source",
sourceType: "document",
id: generateId(),
mediaType,
title,
filename
}, JSON.stringify(["document", uri]));
} else if (fileSearchStore) {
const title = chunk.retrievedContext.title ?? "Unknown Document";
addSource({
type: "source",
sourceType: "document",
id: generateId(),
mediaType: "application/octet-stream",
title,
filename: fileSearchStore.split("/").pop()
}, JSON.stringify([
"file-search",
fileSearchStore,
title
]));
}
} else if (chunk.maps != null) {
if (chunk.maps.uri) addSource({
type: "source",
sourceType: "url",
id: generateId(),
url: chunk.maps.uri,
title: chunk.maps.title ?? void 0
}, JSON.stringify(["url", chunk.maps.uri]));
}
return sources.length > 0 ? sources : void 0;
}
const getGroundingMetadataSchema = () => z.object({
webSearchQueries: z.array(z.string()).nullish(),
imageSearchQueries: z.array(z.string()).nullish(),
retrievalQueries: z.array(z.string()).nullish(),
searchEntryPoint: z.object({ renderedContent: z.string() }).nullish(),
groundingChunks: z.array(z.object({
web: z.object({
uri: z.string(),
title: z.string().nullish()
}).nullish(),
image: z.object({
sourceUri: z.string(),
imageUri: z.string(),
title: z.string().nullish(),
domain: z.string().nullish()
}).nullish(),
retrievedContext: z.object({
uri: z.string().nullish(),
title: z.string().nullish(),
text: z.string().nullish(),
fileSearchStore: z.string().nullish(),
customMetadata: z.array(z.object({
key: z.string(),
stringValue: z.string().nullish(),
numericValue: z.number().nullish(),
stringListValue: z.object({ values: z.array(z.string()).nullish() }).nullish()
})).nullish()
}).nullish(),
maps: z.object({
uri: z.string().nullish(),
title: z.string().nullish(),
text: z.string().nullish(),
placeId: z.string().nullish()
}).nullish()
})).nullish(),
groundingSupports: z.array(z.object({
segment: z.object({
startIndex: z.number().nullish(),
endIndex: z.number().nullish(),
text: z.string().nullish()
}).nullish(),
segment_text: z.string().nullish(),
groundingChunkIndices: z.array(z.number()).nullish(),
supportChunkIndices: z.array(z.number()).nullish(),
confidenceScores: z.array(z.number()).nullish(),
confidenceScore: z.array(z.number()).nullish()
})).nullish(),
retrievalMetadata: z.union([z.object({ webDynamicRetrievalScore: z.number() }), z.object({})]).nullish()
});
const partialArgSchema = () => z.object({
jsonPath: z.string(),
stringValue: z.string().nullish(),
numberValue: z.number().nullish(),
boolValue: z.boolean().nullish(),
nullValue: z.unknown().nullish(),
willContinue: z.boolean().nullish()
});
const getContentSchema = () => z.object({ parts: z.array(z.union([
z.object({
functionCall: z.object({
id: z.string().nullish(),
name: z.string().nullish(),
args: z.unknown().nullish(),
partialArgs: z.array(partialArgSchema()).nullish(),
willContinue: z.boolean().nullish()
}),
thoughtSignature: z.string().nullish()
}),
z.object({
inlineData: z.object({
mimeType: z.string(),
data: z.string()
}),
thought: z.boolean().nullish(),
thoughtSignature: z.string().nullish()
}),
z.object({
toolCall: z.object({
toolType: z.string(),
args: z.unknown().nullish(),
id: z.string()
}),
thoughtSignature: z.string().nullish()
}),
z.object({
toolResponse: z.object({
toolType: z.string(),
response: z.unknown().nullish(),
id: z.string()
}),
thoughtSignature: z.string().nullish()
}),
z.object({
executableCode: z.object({
language: z.string(),
code: z.string()
}).nullish(),
codeExecutionResult: z.object({
outcome: z.string(),
output: z.string().nullish()
}).nullish(),
text: z.string().nullish(),
thought: z.boolean().nullish(),
thoughtSignature: z.string().nullish()
})
])).nullish() });
const getSafetyRatingSchema = () => z.object({
category: z.string().nullish(),
probability: z.string().nullish(),
probabilityScore: z.number().nullish(),
severity: z.string().nullish(),
severityScore: z.number().nullish(),
blocked: z.boolean().nullish()
});
const tokenDetailsSchema = () => z.array(z.object({
modality: z.string(),
tokenCount: z.number()
}).loose()).nullish();
const usageSchema$1 = () => z.object({
cachedContentTokenCount: z.number().nullish(),
thoughtsTokenCount: z.number().nullish(),
promptTokenCount: z.number().nullish(),
candidatesTokenCount: z.number().nullish(),
toolUsePromptTokenCount: z.number().nullish(),
totalTokenCount: z.number().nullish(),
trafficType: z.string().nullish(),
serviceTier: z.string().nullish(),
promptTokensDetails: tokenDetailsSchema(),
cacheTokensDetails: tokenDetailsSchema(),
candidatesTokensDetails: tokenDetailsSchema(),
toolUsePromptTokensDetails: tokenDetailsSchema()
}).loose();
const getUrlContextMetadataSchema = () => z.object({ urlMetadata: z.array(z.object({
retrievedUrl: z.string(),
urlRetrievalStatus: z.string()
})).nullish() });
const responseSchema = lazySchema(() => zodSchema(z.object({
responseId: z.string().nullish(),
candidates: z.array(z.object({
content: getContentSchema().nullish().or(z.object({}).strict()),
finishReason: z.string().nullish(),
finishMessage: z.string().nullish(),
continuationToken: z.string().nullish(),
safetyRatings: z.array(getSafetyRatingSchema()).nullish(),
groundingMetadata: getGroundingMetadataSchema().nullish(),
urlContextMetadata: getUrlContextMetadataSchema().nullish()
})).nullish(),
usageMetadata: usageSchema$1().nullish(),
promptFeedback: z.object({
blockReason: z.string().nullish(),
safetyRatings: z.array(getSafetyRatingSchema()).nullish()
}).nullish()
})));
const chunkSchema = lazySchema(() => zodSchema(z.object({
error: getGoogleErrorSchema().nullish(),
responseId: z.string().nullish(),
candidates: z.array(z.object({
content: getContentSchema().nullish(),
finishReason: z.string().nullish(),
finishMessage: z.string().nullish(),
continuationToken: z.string().nullish(),
safetyRatings: z.array(getSafetyRatingSchema()).nullish(),
groundingMetadata: getGroundingMetadataSchema().nullish(),
urlContextMetadata: getUrlContextMetadataSchema().nullish()
})).nullish(),
usageMetadata: usageSchema$1().nullish(),
promptFeedback: z.object({
blockReason: z.string().nullish(),
safetyRatings: z.array(getSafetyRatingSchema()).nullish()
}).nullish()
})));
function isConfirmedPromptBlockReason(blockReason) {
return blockReason != null && blockReason !== "" && blockReason !== "BLOCK_REASON_UNSPECIFIED" && blockReason !== "BLOCKED_REASON_UNSPECIFIED";
}
//#endregion
//#region src/tool/google-search.ts
const googleSearchToolArgsBaseSchema = () => z.looseObject({
searchTypes: z.object({
webSearch: z.object({}).optional(),
imageSearch: z.object({}).optional()
}).optional(),
timeRangeFilter: z.object({
startTime: z.string(),
endTime: z.string()
}).optional()
});
const googleSearch = createProviderExecutedToolFactory({
id: "google.google_search",
inputSchema: lazySchema(() => zodSchema(z.object({}))),
outputSchema: lazySchema(() => zodSchema(z.object({})))
});
//#endregion
//#region src/google-image-model-options.ts
const googleImageModelOptionsSchema = lazySchema(() => zodSchema(z.object({
/**
* Enable Google Search grounding for Gemini image models. The value is
* forwarded as the args of the `google.tools.googleSearch` provider
* tool on the underlying language-model call. Pass `{}` for defaults.
*
* `generateImage` does not accept a `tools` parameter, so this is the
* dedicated escape hatch for grounding image generation the same way
* `generateText` does.
*/
googleSearch: googleSearchToolArgsBaseSchema().optional() })));
//#endregion
//#region src/google-batch.ts
const googleBatchInputFileMaxBytes = 2147483648;
const googleBatchInlineCreationMaxBytes = 2e7;
const supportedGoogleBatchContentTypes = /* @__PURE__ */ new Set([
"text",
"reasoning",
"source",
"tool-call",
"tool-result"
]);
function assertSupportedBatchRequests(requests) {
for (const request of requests) {
const requestType = request.type;
if (requestType !== "text" && requestType !== "image") throw new UnsupportedFunctionalityError({
functionality: `batch request type: ${requestType}`,
message: `The Google Batch API does not support batch requests with type "${requestType}".`
});
}
}
const googleRpcStatusSchema = () => z.object({
code: z.union([z.number(), z.string()]).nullish(),
message: z.string().nullish(),
status: z.string().nullish()
});
const googleBatchStatsSchema = () => z.object({
requestCount: z.union([z.string(), z.number()]).nullish(),
successfulRequestCount: z.union([z.string(), z.number()]).nullish(),
failedRequestCount: z.union([z.string(), z.number()]).nullish(),
pendingRequestCount: z.union([z.string(), z.number()]).nullish()
});
const googleBatchOutputSchema = () => z.object({
responsesFile: z.string().nullish(),
inlinedResponses: z.object({ inlinedResponses: z.array(z.object({
metadata: z.object({ key: z.string() }),
response: z.unknown().nullish(),
error: googleRpcStatusSchema().nullish()
})) }).nullish()
});
const googleBatchOperationZodSchema = () => z.object({
name: z.string(),
metadata: z.object({
state: z.string().nullish(),
createTime: z.string().nullish(),
batchStats: googleBatchStatsSchema().nullish(),
output: googleBatchOutputSchema().nullish()
}).nullish(),
done: z.boolean().nullish(),
error: googleRpcStatusSchema().nullish(),
response: googleBatchOutputSchema().nullish()
});
const googleBatchOperationSchema = lazySchema(() => zodSchema(googleBatchOperationZodSchema()));
const googleBatchListResponseSchema = lazySchema(() => zodSchema(z.object({
operations: z.array(googleBatchOperationZodSchema()).nullish(),
nextPageToken: z.string().nullish()
})));
const googleBatchCancelResponseSchema = lazySchema(() => zodSchema(z.object({})));
const googleFileUploadResponseSchema = lazySchema(() => zodSchema(z.object({ file: z.object({
name: z.string(),
expirationTime: z.string().nullish()
}) })));
const googleBatchResultLineSchema = lazySchema(() => zodSchema(z.object({
key: z.string(),
response: z.unknown().nullish(),
error: googleRpcStatusSchema().nullish()
})));
const googleBatchResponsePreviewSchema = lazySchema(() => zodSchema(z.object({
candidates: z.array(z.unknown()).nullish(),
promptFeedback: z.object({ blockReason: z.string().nullish() }).nullish()
})));
var GoogleBatch = class {
constructor(options) {
this.specificationVersion = "v4";
this.provider = options.provider;
this.batchConfig = options.config;
this.supportedUrls = options.supportedUrls;
this.batchGenerateId = options.config.generateId ?? generateId;
this.maxLineBytes = options.maxLineBytes;
}
async doStartBatch(options) {
assertSupportedBatchRequests(options.requests);
const modelId = getGoogleBatchModelId(options.requests);
const warnings = [];
const displayName = `ai-sdk-batch-${this.batchGenerateId()}`;
const inlinedRequests = [];
const inlineBatchBody = { batch: {
displayName,
...options.webhookUrl != null && { webhookConfig: { uris: [options.webhookUrl] } },
inputConfig: { requests: { requests: inlinedRequests } }
} };
const textEncoder = new TextEncoder();
let inlineInputBytes = textEncoder.encode(JSON.stringify(inlineBatchBody)).byteLength;
let fileParts;
for (const request of options.requests) {
const preparedRequest = request.type === "text" ? await GoogleLanguageModel.prepareRequest({
modelId: request.modelId,
config: this.batchConfig,
options: request.options
}) : await this.prepareImageRequest(request);
const inlinedRequest = {
request: preparedRequest.args,
metadata: { key: request.id }
};
if (fileParts == null) {
const requestBytes = textEncoder.encode(JSON.stringify(inlinedRequest)).byteLength;
const nextInlineInputBytes = inlineInputBytes + requestBytes + (inlinedRequests.length > 0 ? 1 : 0);
if (nextInlineInputBytes < googleBatchInlineCreationMaxBytes) {
inlinedRequests.push(inlinedRequest);
inlineInputBytes = nextInlineInputBytes;
} else {
fileParts = [];
for (const previousRequest of inlinedRequests) fileParts.push(JSON.stringify({
key: previousRequest.metadata.key,
request: previousRequest.request
}), "\n");
inlinedRequests.length = 0;
fileParts.push(JSON.stringify({
key: request.id,
request: preparedRequest.args
}), "\n");
}
} else fileParts.push(JSON.stringify({
key: request.id,
request: preparedRequest.args
}), "\n");
for (const warning of preparedRequest.warnings) warnings.push({
requestId: request.id,
warning
});
}
const createUrl = `${this.batchConfig.baseURL}/${getModelPath(modelId)}:batchGenerateContent`;
if (fileParts == null) {
const { value: operation } = await postJsonToApi({
url: createUrl,
headers: await this.getHeaders(options.headers),
body: inlineBatchBody,
failedResponseHandler: googleFailedResponseHandler,
successfulResponseHandler: createJsonResponseHandler(googleBatchOperationSchema),
abortSignal: options.abortSignal,
fetch: this.batchConfig.fetch
});
return {
batchId: operation.name,
...convertGoogleBatchStatus(operation),
warnings
};
}
const inputFile = new Blob(fileParts, { type: "application/jsonl" });
fileParts.length = 0;
if (inputFile.size > googleBatchInputFileMaxBytes) throw new InvalidArgumentError({
argument: "requests",
message: "Google batch input files must not exceed 2 GB."
});
const { value: uploadUrl } = await postJsonToApi({
url: `${this.getBaseOrigin()}/upload/v1beta/files`,
headers: combineHeaders(await this.getHeaders(options.headers), {
"X-Goog-Upload-Protocol": "resumable",
"X-Goog-Upload-Command": "start",
"X-Goog-Upload-Header-Content-Length": String(inputFile.size),
"X-Goog-Upload-Header-Content-Type": "application/jsonl"
}),
body: { file: { display_name: `${displayName}-input` } },
failedResponseHandler: googleFailedResponseHandler,
successfulResponseHandler: googleUploadUrlResponseHandler,
abortSignal: options.abortSignal,
fetch: this.batchConfig.fetch
});
const { value: uploadedFile } = await postToApi({
url: uploadUrl,
headers: {
"X-Goog-Upload-Offset": "0",
"X-Goog-Upload-Command": "upload, finalize",
"Content-Type": "application/jsonl"
},
body: {
content: inputFile,
values: {
byteLength: inputFile.size,
mediaType: "application/jsonl"
}
},
failedResponseHandler: googleFailedResponseHandler,
successfulResponseHandler: createJsonResponseHandler(googleFileUploadResponseSchema),
abortSignal: options.abortSignal,
fetch: this.batchConfig.fetch
});
const { value: operation } = await postJsonToApi({
url: createUrl,
headers: await this.getHeaders(options.headers),
body: { batch: {
displayName,
...options.webhookUrl != null && { webhookConfig: { uris: [options.webhookUrl] } },
inputConfig: { fileName: uploadedFile.file.name }
} },
failedResponseHandler: googleFailedResponseHandler,
successfulResponseHandler: createJsonResponseHandler(googleBatchOperationSchema),
abortSignal: options.abortSignal,
fetch: this.batchConfig.fetch
});
return {
batchId: operation.name,
...convertGoogleBatchStatus(operation),
providerMetadata: { google: {
inputFileId: uploadedFile.file.name,
...uploadedFile.file.expirationTime != null ? { inputFileExpiresAt: uploadedFile.file.expirationTime } : {}
} },
warnings
};
}
async doGetBatchStatus(options) {
return convertGoogleBatchStatus(await this.retrieveBatch(options));
}
async doCancelBatch(options) {
await postJsonToApi({
url: `${this.batchConfig.baseURL}/${options.batchId}:cancel`,
headers: await this.getHeaders(options.headers),
body: {},
failedResponseHandler: googleFailedResponseHandler,
successfulResponseHandler: createJsonResponseHandler(googleBatchCancelResponseSchema),
abortSignal: options.abortSignal,
fetch: this.batchConfig.fetch
});
return {};
}
async doListBatches(options) {
const url = new URL(`${this.batchConfig.baseURL}/batches`);
if (options.limit != null) url.searchParams.set("pageSize", String(options.limit));
if (options.cursor != null) url.searchParams.set("pageToken", options.cursor);
const { value: page } = await getFromApi({
url: url.toString(),
headers: await this.getHeaders(options.headers),
failedResponseHandler: googleFailedResponseHandler,
successfulResponseHandler: createJsonResponseHandler(googleBatchListResponseSchema),
abortSignal: options.abortSignal,
fetch: this.batchConfig.fetch,
validateUrl: false
});
return {
batches: (page.operations ?? []).map((operation) => ({
batchId: operation.name,
...convertGoogleBatchStatus(operation)
})),
...page.nextPageToken != null ? { nextCursor: page.nextPageToken } : {}
};
}
async doGetBatchResults(options) {
const operation = await this.retrieveBatch(options);
const batchStatus = convertGoogleBatchStatus(operation);
if (batchStatus.status === "pending") throw new InvalidArgumentError({
argument: "batchId",
message: `Google batch "${options.batchId}" is not complete.`
});
const inlinedResponses = operation.metadata?.output?.inlinedResponses?.inlinedResponses ?? operation.response?.inlinedResponses?.inlinedResponses;
if (inlinedResponses != null) return convertAsyncIteratorToReadableStream(this.iterateBatchResults(inlinedResponses.map((result) => ({
key: result.metadata.key,
response: result.response,
error: result.error
}))));
const responsesFile = operation.metadata?.output?.responsesFile ?? operation.response?.responsesFile;
if (responsesFile == null) {
if (batchStatus.status === "completed") throw new InvalidResponseDataError({
data: operation,
message: `Google batch "${options.batchId}" completed without batch output.`
});
return new ReadableStream({ start(controller) {
controller.close();
} });
}
const encodedResponsesFile = responsesFile.split("/").map((segment) => encodeURIComponent(segment)).join("/");
const { value: lines } = await getFromApi({
url: `${this.getBaseOrigin()}/download/v1beta/${encodedResponsesFile}:download?alt=media`,
headers: await this.getHeaders(options.headers),
failedResponseHandler: googleFailedResponseHandler,
successfulResponseHandler: createJsonLinesResponseHandler(googleBatchResultLineSchema, { maxLineBytes: this.maxLineBytes }),
abortSignal: options.abortSignal,
fetch: this.batchConfig.fetch,
validateUrl: false
});
return convertAsyncIteratorToReadableStream(this.iterateBatchResults(lines));
}
async retrieveBatch(options) {
const { value: operation } = await getFromApi({
url: `${this.batchConfig.baseURL}/${options.batchId}`,
headers: await this.getHeaders(options.headers),
failedResponseHandler: googleFailedResponseHandler,
successfulResponseHandler: createJsonResponseHandler(googleBatchOperationSchema),
abortSignal: options.abortSignal,
fetch: this.batchConfig.fetch,
validateUrl: false
});
return operation;
}
async *iterateBatchResults(results) {
for await (const line of results) {
if (line.error != null) {
const error = convertGoogleRpcError(line.error, "Google batch request failed.");
const status = line.error.status === "CANCELLED" || String(line.error.code) === "1" ? "cancelled" : "failed";
yield {
type: "text",
id: line.key,
status,
error
};
continue;
}
if (line.response == null) {
yield {
type: "text",
id: line.key,
status: "failed",
error: {
message: "Google returned a batch result without a response or error.",
code: "invalid_batch_result"
}
};
continue;
}
const preview = await safeValidateTypes({
value: line.response,
schema: googleBatchResponsePreviewSchema
});
if (preview.success && (preview.value.candidates == null || preview.value.candidates.length === 0)) {
const promptFeedback = preview.value.promptFeedback ?? void 0;
const blockReason = promptFeedback?.blockReason ?? void 0;
yield {
type: "text",
id: line.key,
status: "failed",
error: {
message: blockReason == null ? "Google returned a batch response without any candidates." : `Google blocked the batch request (${blockReason}).`,
code: blockReason == null ? "invalid_response" : "prompt_blocked",
...blockReason != null ? { type: blockReason } : {}
},
...promptFeedback != null ? { providerMetadata: { google: { promptFeedback: { blockReason: promptFeedback.blockReason ?? null } } } } : {}
};
continue;
}
const response = await safeValidateTypes({
value: line.response,
schema: responseSchema
});
if (!response.success) {
yield {
type: "text",
id: line.key,
status: "failed",
error: {
message: "Google returned an invalid GenerateContent batch result.",
code: "invalid_response"
}
};
continue;
}
const result = GoogleLanguageModel.convertGenerateContentResponse({
config: this.batchConfig,
response: response.value,
warnings: [],
providerOptionsNames: ["google"]
});
const imageResult = convertGoogleImageBatchResult(result);
if (imageResult != null) {
yield {
type: "image",
id: line.key,
status: "succeeded",
result: imageResult
};
continue;
}
const unsupportedPart = result.content.find((part) => !supportedGoogleBatchContentTypes.has(part.type));
if (unsupportedPart != null) {
yield {
type: "text",
id: line.key,
status: "failed",
error: {
message: `Google returned a "${unsupportedPart.type}" content block, but that content is not supported in AI SDK text batches.`,
code: "unsupported_content"
}
};
continue;
}
yield {
type: "text",
id: line.key,
status: "succeeded",
result
};
}
}
async prepareImageRequest(request) {
const { prompt, n, size, aspectRatio, seed, files, mask, providerOptions } = request.options;
const warnings = [];
if (mask != null) throw new UnsupportedFunctionalityError({ functionality: "mask-based image editing in Google batches" });
if (n > 1) throw new UnsupportedFunctionalityError({ functionality: "multiple images per Google batch request" });
if (size != null) warnings.push({
type: "unsupported",
feature: "size",
details: "This model does not support the `size` option. Use `aspectRatio` instead."
});
const userContent = [];
if (prompt != null) userContent.push({
type: "text",
text: prompt
});
for (const file of files ?? []) userContent.push(file.type === "url" ? {
type: "file",
data: {
type: "url",
url: new URL(file.url)
},
mediaType: "image/*"
} : {
type: "file",
data: {
type: "data",
data: file.data
},
mediaType: file.mediaType
});
const googleImageOptions = await parseProviderOptions({
provider: "google",
providerOptions,
schema: googleImageModelOptionsSchema
});
const { responseModalities: _responseModalities, imageConfig: userImageConfig, ...passthroughGoogleOptions } = await parseProviderOptions({
provider: "google",
providerOptions,
schema: googleLanguageModelOptions
}) ?? {};
const preparedGoogleOptions = await parseProviderOptions({
provider: "google",
providerOptions: { google: {
...passthroughGoogleOptions,
responseModalities: ["IMAGE"],
imageConfig: aspectRatio != null || userImageConfig != null ? {
...userImageConfig,
...aspectRatio != null ? { aspectRatio } : {}
} : void 0
} },
schema: googleLanguageModelOptions
});
const prepared = await GoogleLanguageModel.prepareRequest({
modelId: request.modelId,
config: this.batchConfig,
options: {
prompt: [{
role: "user",
content: userContent
}],
seed,
providerOptions: { google: preparedGoogleOptions ?? { responseModalities: ["IMAGE"] } },
tools: googleImageOptions?.googleSearch != null ? [{
type: "provider",
id: "google.google_search",
name: "google_search",
args: googleImageOptions.googleSearch
}] : void 0
}
});
return {
...prepared,
warnings: [...warnings, ...prepared.warnings]
};
}
async getHeaders(headers) {
return combineHeaders(this.batchConfig.headers ? await resolve(this.batchConfig.headers) : void 0, headers);
}
getBaseOrigin() {
return this.batchConfig.baseURL.replace(/\/v1beta$/, "");
}
};
function convertGoogleBatchStatus(operation) {
const rawStatus = operation.metadata?.state ?? void 0;
const requestCounts = convertGoogleRequestCounts(operation.metadata?.batchStats);
const createdAt = operation.metadata?.createTime ?? void 0;
const error = operation.error != null ? convertGoogleRpcError(operation.error, "Google batch failed.") : void 0;
return {
status: mapGoogleBatchStatus({
rawStatus,
done: operation.done ?? void 0,
hasError: error != null
}),
...rawStatus != null ? { rawStatus } : {},
...requestCounts != null ? { requestCounts } : {},
...error != null ? { error } : {},
...createdAt != null ? { createdAt } : {}
};
}
function mapGoogleBatchStatus({ rawStatus, done, hasError }) {
if (hasError) return "failed";
if (rawStatus == null) return done ? "completed" : "pending";
switch (rawStatus.replace(/^(?:BATCH|JOB)_STATE_/, "")) {
case "SUCCEEDED": return "completed";
case "FAILED":
case "CANCELLED":
case "EXPIRED": return "failed";
default: return "pending";
}
}
function convertGoogleRequestCounts(counts) {
const total = parseCount(counts?.requestCount);
const completed = parseCount(counts?.successfulRequestCount ?? 0);
const failed = parseCount(counts?.failedRequestCount ?? 0);
const pending = parseCount(counts?.pendingRequestCount ?? 0);
return normalizeBatchRequestCounts({
total,
pending,
completed,
failed
});
}
function parseCount(value) {
const count = typeof value === "string" && /^\d+$/.test(value) ? Number(value) : value;
return typeof count === "number" && Number.isSafeInteger(count) && count >= 0 ? count : void 0;
}
function convertGoogleRpcError(error, fallbackMessage) {
return {
message: error.message ?? fallbackMessage,
...error.status != null ? { type: error.status } : {},
...error.code != null ? { code: String(error.code) } : {}
};
}
const googleUploadUrlResponseHandler = async ({ response }) => {
const uploadUrl = response.headers.get("x-goog-upload-url");
if (uploadUrl == null) throw new InvalidResponseDataError({
data: response.headers,
message: "Google did not return a resumable upload URL."
});
return { value: uploadUrl };
};
function getGoogleBatchModelId(requests) {
const modelId = requests[0]?.modelId;
if (modelId == null) throw new InvalidArgumentError({
argument: "requests",
message: "Google batches require at least one request."
});
for (const request of requests) if (request.modelId !== modelId) throw new InvalidArgumentError({
argument: "requests",
message: "Google batches require every request to use the same model because the model is part of the batch endpoint."
});
return modelId;
}
function convertGoogleImageBatchResult(result) {
const images = result.content.flatMap((part) => part.type === "file" && part.mediaType.startsWith("image/") && part.data.type === "data" ? [convertToBase64(part.data.data)] : []);
if (images.length === 0) return void 0;
const googleMetadata = result.providerMetadata?.google ?? {};
return {
images,
warnings: result.warnings,
providerMetadata: { google: {
...googleMetadata,
images: images.map(() => ({}))
} },
response: {
timestamp: /* @__PURE__ */ new Date(),
modelId: result.response?.modelId ?? "",
headers: result.response?.headers
},
usage: {
inputTokens: result.usage.inputTokens.total,
outputTokens: result.usage.outputTokens.total,
totalTokens: (result.usage.inputTokens.total ?? 0) + (result.usage.outputTokens.total ?? 0)
}
};
}
//#endregion
//#region src/tool/enterprise-web-search.ts
const enterpriseWebSearch = createProviderExecutedToolFactory({
id: "google.enterprise_web_search",
inputSchema: lazySchema(() => zodSchema(z.object({}))),
outputSchema: lazySchema(() => zodSchema(z.object({})))
});
//#endregion
//#region src/tool/file-search.ts
lazySchema(() => zodSchema(z.looseObject({
/** The names of the file_search_stores to retrieve from.
* Example: `fileSearchStores/my-file-search-store-123`
*/
fileSearchStoreNames: z.array(z.string()).describe("The names of the file_search_stores to retrieve from. Example: `fileSearchStores/my-file-search-store-123`"),
/** The number of file search retrieval chunks to retrieve. */
topK: z.number().int().positive().describe("The number of file search retrieval chunks to retrieve.").optional(),
/** Metadata filter to apply to the file search retrieval documents.
* See https://google.aip.dev/160 for the syntax of the filter expression.
*/
metadataFilter: z.string().describe("Metadata filter to apply to the file search retrieval documents. See https://google.aip.dev/160 for the syntax of the filter expression.").optional()
})));
const fileSearch = createProviderExecutedToolFactory({
id: "google.file_search",
inputSchema: lazySchema(() => zodSchema(z.object({}))),
outputSchema: lazySchema(() => zodSchema(z.object({})))
});
//#endregion
//#region src/google-tools.ts
const googleTools = {
/**
* Creates a Google search tool that gives Google direct access to real-time web content.
* Must have name "google_search".
*/
googleSearch,
/**
* Creates an Enterprise Web Search tool for grounding responses using a compliance-focused web index.
* Designed for highly-regulated industries (finance, healthcare, public sector).
* Does not log customer data and supports VPC service controls.
* Must have name "enterprise_web_search".
*
* @note Only available on Vertex AI. Requires Gemini 2.0 or newer.
*
* @see https://cloud.google.com/vertex-ai/generative-ai/docs/grounding/web-grounding-enterprise
*/
enterpriseWebSearch,
/**
* Creates a Google Maps grounding tool that gives the model access to Google Maps data.
* Must have name "google_maps".
*
* @see https://ai.google.dev/gemini-api/docs/maps-grounding
* @see https://cloud.google.com/vertex-ai/generative-ai/docs/grounding/grounding-with-google-maps
*/
googleMaps: createProviderExecutedToolFactory({
id: "google.google_maps",
inputSchema: lazySchema(() => zodSchema(z.object({}))),
outputSchema: lazySchema(() => zodSchema(z.object({})))
}),
/**
* Creates a URL context tool that gives Google direct access to real-time web content.
* Must have name "url_context".
*/
urlContext: createProviderExecutedToolFactory({
id: "google.url_context",
inputSchema: lazySchema(() => zodSchema(z.object({}))),
outputSchema: lazySchema(() => zodSchema(z.object({})))
}),
/**
* Enables Retrieval Augmented Generation (RAG) via the Gemini File Search tool.
* Must have name "file_search".
*
* @param fileSearchStoreNames - Fully-qualified File Search store resource names.
* @param metadataFilter - Optional filter expression to restrict the files that can be retrieved.
* @param topK - Optional result limit for the number of chunks returned from File Search.
*
* @see https://ai.google.dev/gemini-api/docs/file-search
*/
fileSearch,
/**
* A tool that enables the model to generate and run Python code.
* Must have name "code_execution".
*
* @note Ensure the selected model supports Code Execution.
* Multi-tool usage with the code execution tool is typically compatible with Gemini >=2 models.
*
* @see https://ai.google.dev/gemini-api/docs/code-execution (Google AI)
* @see https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/code-execution-api (Vertex AI)
*/
codeExecution,
/**
* Creates a Vertex RAG Store tool that enables the model to perform RAG searches against a Vertex RAG Store.
* Must have name "vertex_rag_store".
*/
vertexRagStore: createProviderExecutedToolFactory({
id: "google.vertex_rag_store",
inputSchema: lazySchema(() => zodSchema(z.object({}))),
outputSchema: lazySchema(() => zodSchema(z.object({})))
})
};
//#endregion
//#region src/google-image-model.ts
const googleImageModelsWithFileInputSupport = /* @__PURE__ */ new Set([
"gemini-2.5-flash-image",
"gemini-3-pro-image-preview",
"gemini-3.1-flash-image-preview",
"gemini-nano-banana-2.1"
]);
var GoogleImageModel = class GoogleImageModel {
static [WORKFLOW_SERIALIZE](model) {
return serializeModelOptions({
modelId: model.modelId,
config: model.config
});
}
static [WORKFLOW_DESERIALIZE](options) {
return new GoogleImageModel(options.modelId, {}, options.config);
}
get maxImagesPerCall() {
if (this.settings.maxImagesPerCall != null) return this.settings.maxImagesPerCall;
return 1;
}
get supportsFileInputs() {
return googleImageModelsWithFileInputSupport.has(this.modelId) ? true : void 0;
}
get supportsMaskInputs() {
return this.supportsFileInputs === true ? false : void 0;
}
get provider() {
return this.config.provider;
}
constructor(modelId, settings, config) {
this.modelId = modelId;
this.settings = settings;
this.config = config;
this.specificationVersion = "v4";
}
async doGenerate(options) {
if (!this.modelId.startsWith("gemini-")) throw new Error("Google image models other than Gemini are no longer supported. Use a model ID that starts with `gemini-`.");
const { prompt, size, aspectRatio, seed, providerOptions, headers, abortSignal, files, mask } = options;
const warnings = [];
if (mask != null) throw new Error("Gemini image models do not support mask-based image editing.");
if (size != null) warnings.push({
type: "unsupported",
feature: "size",
details: "This model does not support the `size` option. Use `aspectRatio` instead."
});
const userContent = [];
if (prompt != null) userContent.push({
type: "text",
text: prompt
});
if (files != null && files.length > 0) for (const file of files) if (file.type === "url") userContent.push({
type: "file",
data: {
type: "url",
url: new URL(file.url)
},
mediaType: "image/*"
});
else userContent.push({
type: "file",
data: {
type: "data",
data: typeof file.data === "string" ? file.data : new Uint8Array(file.data)
},
mediaType: file.mediaType
});
const languageModelPrompt = [{
role: "user",
content: userContent
}];
const googleImageOptions = await parseProviderOptions({
provider: "google",
providerOptions,
schema: googleImageModelOptionsSchema
});
const { googleSearch: _strippedGoogleSearch, responseModalities: _strippedResponseModalities, imageConfig: userImageConfig, ...passthroughGoogleOptions } = providerOptions?.google ?? {};
const result = await new GoogleLanguageModel(this.modelId, {
provider: this.config.provider,
baseURL: this.config.baseURL,
headers: this.config.headers ?? {},
fetch: this.config.fetch,
generateId: this.config.generateId ?? generateId
}).doGenerate({
prompt: languageModelPrompt,
seed,
providerOptions: { google: {
...passthroughGoogleOptions,
responseModalities: ["IMAGE"],
imageConfig: aspectRatio != null || userImageConfig != null ? {
...userImageConfig,
...aspectRatio != null ? { aspectRatio } : {}
} : void 0
} },
tools: googleImageOptions?.googleSearch != null ? [{
type: "provider",
id: "google.google_search",
name: "google_search",
args: googleImageOptions.googleSearch
}] : void 0,
headers,
abortSignal
});
const currentDate = this.config._internal?.currentDate?.() ?? /* @__PURE__ */ new Date();
const images = [];
for (const part of result.content) if (part.type === "file" && part.mediaType.startsWith("image/") && part.data.type === "data") images.push(convertToBase64(part.data.data));
const languageModelGoogleMetadata = result.providerMetadata?.google ?? {};
return {
images,
...result.finishReason.unified === "content-filter" ? { isRetryable: false } : {},
warnings,
providerMetadata: { google: {
...languageModelGoogleMetadata,
finishReason: result.finishReason.raw ?? null,
images: images.map(() => ({}))
} },
response: {
timestamp: currentDate,
modelId: this.modelId,
headers: result.response?.headers
},
usage: result.usage ? {
inputTokens: result.usage.inputTokens.total,
outputTokens: result.usage.outputTokens.total,
totalTokens: (result.usage.inputTokens.total ?? 0) + (result.usage.outputTokens.total ?? 0)
} : void 0
};
}
};
//#endregion
//#region src/google-files.ts
var GoogleFiles = class {
get provider() {
return this.config.provider;
}
constructor(config) {
this.config = config;
this.specificationVersion = "v4";
}
async uploadFile(options) {
const googleOptions = await parseProviderOptions({
provider: "google",
providerOptions: options.providerOptions,
schema: googleFilesUploadOptionsSchema
});
const getHeaders = async () => combineHeaders(await resolve(this.config.headers), options.headers);
const fetchFn = this.config.fetch ?? globalThis.fetch;
const warnings = [];
if (options.filename != null) warnings.push({
type: "unsupported",
feature: "filename"
});
const fileBytes = convertInlineFileDataToUint8Array(options.data);
const mediaType = options.mediaType;
const displayName = googleOptions?.displayName;
const initResponse = await fetchFn(`${this.config.baseURL.replace(/\/v1beta$/, "")}/upload/v1beta/files`, {
method: "POST",
headers: {
...await getHeaders(),
"X-Goog-Upload-Protocol": "resumable",
"X-Goog-Upload-Command": "start",
"X-Goog-Upload-Header-Content-Length": String(fileBytes.length),
"X-Goog-Upload-Header-Content-Type": mediaType,
"Content-Type": "application/json"
},
body: JSON.stringify({ file: { ...displayName != null ? { display_name: displayName } : {} } }),
signal: options.abortSignal
});
if (!initResponse.ok) {
const errorBody = await initResponse.text();
throw new AISDKError({
name: "GOOGLE_FILES_UPLOAD_ERROR",
message: `Failed to initiate resumable upload: ${initResponse.status} ${errorBody}`
});
}
const uploadUrl = initResponse.headers.get("x-goog-upload-url");
if (!uploadUrl) throw new AISDKError({
name: "GOOGLE_FILES_UPLOAD_ERROR",
message: "No upload URL returned from initiation request"
});
const uploadResponse = await fetchFn(uploadUrl, {
method: "POST",
headers: {
"X-Goog-Upload-Offset": "0",
"X-Goog-Upload-Command": "upload, finalize"
},
body: ensureArrayBufferBacked(fileBytes),
signal: options.abortSignal
});
if (!uploadResponse.ok) {
const errorBody = await uploadResponse.text();
throw new AISDKError({
name: "GOOGLE_FILES_UPLOAD_ERROR",
message: `Failed to upload file data: ${uploadResponse.status} ${errorBody}`
});
}
let file = (await uploadResponse.json()).file;
const pollIntervalMs = googleOptions?.pollIntervalMs ?? 2e3;
const pollTimeoutMs = googleOptions?.pollTimeoutMs ?? 3e5;
const startTime = Date.now();
while (file.state === "PROCESSING") {
if (Date.now() - startTime > pollTimeoutMs) throw new AISDKError({
name: "GOOGLE_FILES_UPLOAD_TIMEOUT",
message: `File processing timed out after ${pollTimeoutMs}ms`
});
await delay(pollIntervalMs, { abortSignal: options.abortSignal });
const fileNameMatch = /^files\/([^/]+)$/.exec(file.name);
const filePath = fileNameMatch != null ? `files/${encodePathSegment(fileNameMatch[1])}` : encodePathSegment(file.name);
const { value: fileStatus } = await getFromApi({
url: `${this.config.baseURL}/${filePath}`,
validateUrl: false,
headers: await getHeaders(),
successfulResponseHandler: createJsonResponseHandler(googleFileResponseSchema),
failedResponseHandler: googleFailedResponseHandler,
abortSignal: options.abortSignal,
fetch: this.config.fetch
});
file = fileStatus;
}
if (file.state === "FAILED") throw new AISDKError({
name: "GOOGLE_FILES_UPLOAD_FAILED",
message: `File processing failed for ${file.name}`
});
return {
warnings,
providerReference: { google: file.uri },
mediaType: file.mimeType ?? options.mediaType,
providerMetadata: { google: {
name: file.name,
displayName: file.displayName,
mimeType: file.mimeType,
sizeBytes: file.sizeBytes,
state: file.state,
uri: file.uri,
...file.createTime != null ? { createTime: file.createTime } : {},
...file.updateTime != null ? { updateTime: file.updateTime } : {},
...file.expirationTime != null ? { expirationTime: file.expirationTime } : {},
...file.sha256Hash != null ? { sha256Hash: file.sha256Hash } : {}
} }
};
}
};
function ensureArrayBufferBacked(data) {
if (data.buffer instanceof ArrayBuffer) return data;
return new Uint8Array(data);
}
const googleFileResponseSchema = lazySchema(() => zodSchema(z.object({
name: z.string(),
displayName: z.string().nullish(),
mimeType: z.string(),
sizeBytes: z.string().nullish(),
createTime: z.string().nullish(),
updateTime: z.string().nullish(),
expirationTime: z.string().nullish(),
sha256Hash: z.string().nullish(),
uri: z.string(),
state: z.string()
})));
const googleFilesUploadOptionsSchema = lazySchema(() => zodSchema(z.looseObject({
displayName: z.string().nullish(),
pollIntervalMs: z.number().positive().nullish(),
pollTimeoutMs: z.number().positive().nullish()
})));
//#endregion
//#region src/google-video-model-options.ts
const googleVideoModelOptionsSchema = lazySchema(() => zodSchema(z.looseObject({
pollIntervalMs: z.number().positive().nullish(),
pollTimeoutMs: z.number().positive().nullish(),
personGeneration: z.enum([
"dont_allow",
"allow_adult",
"allow_all"
]).nullish(),
negativePrompt: z.string().nullish(),
referenceImages: z.array(z.object({
bytesBase64Encoded: z.string().nullish(),
gcsUri: z.string().nullish()
})).nullish()
})));
//#endregion
//#region src/google-video-model.ts
function getFirstFrameImage(options) {
return options.frameImages?.find((frame) => frame.frameType === "first_frame")?.image;
}
function resolveStartImage(options) {
return getFirstFrameImage(options) ?? options.image;
}
function getLastFrameImage(options) {
return options.frameImages?.find((frame) => frame.frameType === "last_frame")?.image;
}
function getInputReferences(options) {
if (options.frameImages != null && options.frameImages.length > 0) return;
return options.inputReferences != null && options.inputReferences.length > 0 ? options.inputReferences : void 0;
}
function convertFileToGoogleImage(file, warnings) {
if (file.type === "url") {
if (file.url.startsWith("gs://")) return {
gcsUri: file.url,
mimeType: "image/png"
};
warnings.push({
type: "unsupported",
feature: "URL-based image input",
details: "Google Generative AI video models require base64-encoded images or GCS URIs. URL will be ignored."
});
return;
}
return {
bytesBase64Encoded: typeof file.data === "string" ? file.data : convertUint8ArrayToBase64(file.data),
mimeType: file.mediaType || "image/png"
};
}
function convertProviderReferenceImage(refImg) {
if (refImg.bytesBase64Encoded) return {
image: {
bytesBase64Encoded: refImg.bytesBase64Encoded,
mimeType: "image/png"
},
referenceType: "asset"
};
if (refImg.gcsUri) return {
image: {
gcsUri: refImg.gcsUri,
mimeType: "image/png"
},
referenceType: "asset"
};
return refImg;
}
function convertInputReferenceImage(file, warnings) {
const image = convertFileToGoogleImage(file, warnings);
return image != null ? {
image,
referenceType: "asset"
} : void 0;
}
var GoogleVideoModel = class {
get provider() {
return this.config.provider;
}
get maxVideosPerCall() {
return 4;
}
constructor(modelId, config) {
this.modelId = modelId;
this.config = config;
this.specificationVersion = "v4";
}
async buildRequest(options) {
const warnings = [];
const googleOptions = await parseProviderOptions({
provider: "google",
providerOptions: options.providerOptions,
schema: googleVideoModelOptionsSchema
});
const instances = [{}];
const instance = instances[0];
if (options.prompt != null) instance.prompt = options.prompt;
const startImage = resolveStartImage(options);
if (startImage != null) {
const image = convertFileToGoogleImage(startImage, warnings);
if (image != null) instance.image = image;
}
const lastFrameImage = getLastFrameImage(options);
if (lastFrameImage != null) {
const lastFrame = convertFileToGoogleImage(lastFrameImage, warnings);
if (lastFrame != null) instance.lastFrame = lastFrame;
}
const inputReferences = getInputReferences(options);
if (inputReferences != null) instance.referenceImages = inputReferences.flatMap((reference) => {
const converted = convertInputReferenceImage(reference, warnings);
return converted != null ? [converted] : [];
});
else if (googleOptions?.referenceImages != null) instance.referenceImages = googleOptions.referenceImages.map((refImg) => convertProviderReferenceImage(refImg));
const parameters = { sampleCount: options.n };
if (options.aspectRatio) parameters.aspectRatio = options.aspectRatio;
if (options.resolution) parameters.resolution = {
"1280x720": "720p",
"1920x1080": "1080p",
"3840x2160": "4k"
}[options.resolution] || options.resolution;
if (options.duration) parameters.durationSeconds = options.duration;
if (options.seed != null) parameters.seed = options.seed;
if (googleOptions != null) {
const opts = googleOptions;
if (opts.personGeneration !== void 0 && opts.personGeneration !== null) parameters.personGeneration = opts.personGeneration;
if (opts.negativePrompt !== void 0 && opts.negativePrompt !== null) parameters.negativePrompt = opts.negativePrompt;
for (const [key, value] of Object.entries(opts)) if (![
"pollIntervalMs",
"pollTimeoutMs",
"personGeneration",
"negativePrompt",
"referenceImages"
].includes(key)) parameters[key] = value;
}
return {
instances,
parameters,
warnings,
googleOptions
};
}
async buildCompletedResult(finalOperation, responseHeaders, warnings, currentDate) {
const response = finalOperation.response;
if (!response?.generateVideoResponse?.generatedSamples || response.generateVideoResponse.generatedSamples.length === 0) throw new AISDKError({
name: "GOOGLE_VIDEO_GENERATION_ERROR",
message: `No videos in response. Response: ${JSON.stringify(finalOperation)}`
});
const videos = [];
const videoMetadata = [];
const apiKey = (await resolve(this.config.headers))?.["x-goog-api-key"];
for (const generatedSample of response.generateVideoResponse.generatedSamples) if (generatedSample.video?.uri) {
const urlWithAuth = apiKey && isSameOrigin(generatedSample.video.uri, this.config.baseURL) ? `${generatedSample.video.uri}${generatedSample.video.uri.includes("?") ? "&" : "?"}key=${apiKey}` : generatedSample.video.uri;
videos.push({
type: "url",
url: urlWithAuth,
mediaType: "video/mp4"
});
videoMetadata.push({ uri: generatedSample.video.uri });
}
if (videos.length === 0) throw new AISDKError({
name: "GOOGLE_VIDEO_GENERATION_ERROR",
message: "No valid videos in response"
});
return {
status: "completed",
videos,
warnings,
response: {
timestamp: currentDate,
modelId: this.modelId,
headers: responseHeaders
},
providerMetadata: { google: { videos: videoMetadata } }
};
}
async doStart(options) {
const currentDate = this.config._internal?.currentDate?.() ?? /* @__PURE__ */ new Date();
const { instances, parameters, warnings } = await this.buildRequest(options);
const { value: operation, responseHeaders } = await postJsonToApi({
url: `${this.config.baseURL}/models/${this.modelId}:predictLongRunning`,
headers: combineHeaders(await resolve(this.config.headers), options.headers),
body: {
instances,
parameters
},
successfulResponseHandler: createJsonResponseHandler(googleOperationSchema),
failedResponseHandler: googleFailedResponseHandler,
abortSignal: options.abortSignal,
fetch: this.config.fetch
});
const operationName = operation.name;
if (!operationName) throw new AISDKError({
name: "GOOGLE_VIDEO_GENERATION_ERROR",
message: "No operation name returned from API"
});
return {
operation: { operationName },
warnings,
response: {
timestamp: currentDate,
modelId: this.modelId,
headers: responseHeaders
}
};
}
async doStatus(options) {
const currentDate = this.config._internal?.currentDate?.() ?? /* @__PURE__ */ new Date();
const { operationName } = options.operation;
const { value: statusOperation, responseHeaders } = await getFromApi({
url: `${this.config.baseURL}/${operationName}`,
validateUrl: false,
headers: combineHeaders(await resolve(this.config.headers), options.headers),
successfulResponseHandler: createJsonResponseHandler(googleOperationSchema),
failedResponseHandler: googleFailedResponseHandler,
abortSignal: options.abortSignal,
fetch: this.config.fetch
});
if (!statusOperation.done) return {
status: "pending",
response: {
timestamp: currentDate,
modelId: this.modelId,
headers: responseHeaders
}
};
if (statusOperation.error) return {
status: "error",
error: `Video generation failed: ${statusOperation.error.message}`,
response: {
timestamp: currentDate,
modelId: this.modelId,
headers: responseHeaders
}
};
return this.buildCompletedResult(statusOperation, responseHeaders, [], currentDate);
}
};
const googleOperationSchema = lazySchema(() => zodSchema(z.object({
name: z.string().nullish(),
done: z.boolean().nullish(),
error: z.object({
code: z.number().nullish(),
message: z.string(),
status: z.string().nullish()
}).nullish(),
response: z.object({ generateVideoResponse: z.object({ generatedSamples: z.array(z.object({ video: z.object({ uri: z.string().nullish() }).nullish() })).nullish() }).nullish() }).nullish()
})));
//#endregion
//#region src/google-speech-api.ts
/**
* Response schema for the Gemini `:generateContent` endpoint when called with
* `responseModalities: ['AUDIO']`. The generated audio is returned as base64
* encoded audio in the first inline-data part (WAV by default on Gemini 3.8,
* raw PCM on older models).
*/
const googleSpeechResponseSchema = lazySchema(() => zodSchema(z.object({
candidates: z.array(z.object({ content: z.object({ parts: z.array(z.object({ inlineData: z.object({
mimeType: z.string().nullish(),
data: z.string().nullish()
}).nullish() })).nullish() }).nullish() })).nullish(),
usageMetadata: z.record(z.string(), z.json()).nullish()
})));
//#endregion
//#region src/google-speech-input.ts
/**
* Inspects speech input without replacing provider option validation. This is
* also used by intermediaries that need the transcript or voice kind before
* invoking a model. Model support for structured turns is validated separately.
*/
function getGoogleSpeechInput({ text, voice, providerOptions }) {
const google = providerOptions?.google;
const options = google != null && typeof google === "object" ? google : void 0;
const turns = options && "turns" in options ? options.turns : void 0;
const turnTexts = [];
if (Array.isArray(turns) && turns.length > 0) {
for (const turn of turns) {
if (turn == null || typeof turn !== "object" || !("text" in turn) || typeof turn.text !== "string") break;
turnTexts.push(turn.text);
}
if (turnTexts.length === turns.length) text = turnTexts.join("");
}
const config = options && "multiSpeakerVoiceConfig" in options ? options.multiSpeakerVoiceConfig : void 0;
const speakers = config != null && typeof config === "object" && "speakerVoiceConfigs" in config && Array.isArray(config.speakerVoiceConfigs) ? config.speakerVoiceConfigs : [];
return {
text,
usesCustomVoice: voice?.startsWith("voice_") === true || voice?.startsWith("voicekey_") === true || speakers.some((speaker) => speaker != null && typeof speaker === "object" && "voiceConfig" in speaker && speaker.voiceConfig != null && typeof speaker.voiceConfig === "object" && "voice" in speaker.voiceConfig)
};
}
//#endregion
//#region src/google-speech-model-options.ts
const prebuiltVoiceConfigSchema = () => z.object({ voiceName: z.string() });
const voiceConfigSchema = () => z.object({
prebuiltVoiceConfig: prebuiltVoiceConfigSchema(),
voice: z.never().optional()
});
const speechMetadataSchema = () => z.object({
speaker: z.string().min(1).optional(),
style: z.string().optional()
});
const googleSpeechProviderOptionsSchema = lazySchema(() => zodSchema(z.object({
/** Turn-level directions for the top-level text, for Gemini 3.8 TTS. */
speechMetadata: speechMetadataSchema().optional(),
/**
* Structured transcript for Gemini 3.8 TTS. Replaces the top-level text;
* pass text: '' when using turns. Each multi-speaker turn must name a
* configured speaker in speechMetadata. Per-turn styles override instructions.
*/
turns: z.array(z.object({
text: z.string(),
speechMetadata: speechMetadataSchema().optional()
})).min(1).optional(),
/**
* Multi-speaker configuration for dialogue audio. When provided, this
* overrides the top-level `voice`. The Gemini TTS API supports up to two
* speakers. For Gemini 3.8, each turn's speechMetadata.speaker must match
* a configured speaker; older models use speaker labels in the text.
*
* https://ai.google.dev/gemini-api/docs/speech-generation#multi-speaker
*/
multiSpeakerVoiceConfig: z.object({ speakerVoiceConfigs: z.array(z.object({
speaker: z.string(),
voiceConfig: voiceConfigSchema()
})) }).optional()
})));
//#endregion
//#region src/google-speech-model.ts
const DEFAULT_VOICE = "Kore";
const DEFAULT_SAMPLE_RATE = 24e3;
var GoogleSpeechModel = class GoogleSpeechModel {
static [WORKFLOW_SERIALIZE](model) {
return serializeModelOptions({
modelId: model.modelId,
config: model.config
});
}
static [WORKFLOW_DESERIALIZE](options) {
return new GoogleSpeechModel(options.modelId, options.config);
}
get provider() {
return this.config.provider;
}
constructor(modelId, config) {
this.modelId = modelId;
this.config = config;
this.specificationVersion = "v4";
}
async getArgs({ text, voice = DEFAULT_VOICE, outputFormat, instructions, speed, language, providerOptions }) {
const warnings = [];
const providerOptionsNames = this.config.provider.includes("vertex") ? ["googleVertex", "vertex"] : ["google"];
let googleOptions;
for (const name of providerOptionsNames) {
googleOptions = await parseProviderOptions({
provider: name,
providerOptions,
schema: googleSpeechProviderOptionsSchema
});
if (googleOptions != null) break;
}
if (googleOptions == null && !providerOptionsNames.includes("google")) googleOptions = await parseProviderOptions({
provider: "google",
providerOptions,
schema: googleSpeechProviderOptionsSchema
});
const usesStructuredSpeech = !this.modelId.startsWith("gemini-2.5-") && !this.modelId.startsWith("gemini-3.1-");
const input = getGoogleSpeechInput({
text,
voice,
providerOptions: { google: googleOptions }
});
if (input.usesCustomVoice) throw new InvalidArgumentError({
argument: "voice",
message: "Custom voices are not supported. Use a prebuilt voice instead."
});
const multiSpeakerVoiceConfig = googleOptions?.multiSpeakerVoiceConfig;
const languageCode = language === "auto" ? void 0 : language;
const speechConfig = multiSpeakerVoiceConfig ? {
multiSpeakerVoiceConfig,
...languageCode != null && { languageCode }
} : {
voiceConfig: { prebuiltVoiceConfig: { voiceName: voice } },
...languageCode != null && { languageCode }
};
let promptText = text;
if (instructions != null && !usesStructuredSpeech) {
if (multiSpeakerVoiceConfig) warnings.push({
type: "unsupported",
feature: "instructions",
details: "Google Gemini TTS ignores `instructions` when `multiSpeakerVoiceConfig` is set, because prepending them would break multi-speaker transcript parsing."
});
else promptText = `${instructions}: ${text}`;
}
let parts = [{ text: promptText }];
if (usesStructuredSpeech) {
if (googleOptions?.turns && googleOptions.speechMetadata) throw new InvalidArgumentError({
argument: "providerOptions",
message: "Set speechMetadata on each turn when using turns."
});
if (googleOptions?.turns && text !== "") warnings.push({
type: "unsupported",
feature: "text",
details: "Google TTS turns replace the top-level text."
});
parts = (googleOptions?.turns ?? [{
text,
speechMetadata: googleOptions?.speechMetadata
}]).map((part) => {
const style = part.speechMetadata?.style ?? instructions;
const speaker = part.speechMetadata?.speaker;
if (multiSpeakerVoiceConfig && !multiSpeakerVoiceConfig.speakerVoiceConfigs.some((config) => config.speaker === speaker)) throw new InvalidArgumentError({
argument: "speechMetadata.speaker",
message: "Every multi-speaker turn must specify a speechMetadata.speaker matching a configured speaker."
});
return {
text: part.text,
...style != null || speaker != null ? { speechMetadata: {
style,
speaker
} } : {}
};
});
} else if (googleOptions?.turns || googleOptions?.speechMetadata) throw new InvalidArgumentError({
argument: "providerOptions",
message: "Structured speech metadata and turns require Gemini 3.8 TTS."
});
if (input.text.length === 0) throw new InvalidArgumentError({
argument: "text",
message: "Speech input must contain a non-empty transcript."
});
if (speed != null) warnings.push({
type: "unsupported",
feature: "speed",
details: "Google Gemini TTS models do not support the `speed` option. It was ignored."
});
const formats = usesStructuredSpeech ? {
wav: "AUDIO_WAV",
"audio/wav": "AUDIO_WAV",
pcm: "AUDIO_L16",
"audio/l16": "AUDIO_L16",
mulaw: "AUDIO_MULAW",
"audio/mulaw": "AUDIO_MULAW",
alaw: "AUDIO_ALAW",
"audio/alaw": "AUDIO_ALAW"
} : {
wav: "AUDIO_WAV",
pcm: "AUDIO_L16"
};
let resolvedOutputFormat = "wav";
if (outputFormat != null && Object.prototype.hasOwnProperty.call(formats, outputFormat)) resolvedOutputFormat = outputFormat;
else if (outputFormat != null) warnings.push({
type: "unsupported",
feature: "outputFormat",
details: `Unsupported output format: ${outputFormat}. Using wav instead.`
});
return {
requestBody: {
contents: [{
role: "user",
parts
}],
generationConfig: {
responseModalities: ["AUDIO"],
speechConfig,
...usesStructuredSpeech && outputFormat != null ? { responseFormat: { audio: { mimeType: formats[resolvedOutputFormat] } } } : {}
}
},
warnings,
outputFormat: formats[resolvedOutputFormat],
usesStructuredSpeech
};
}
async doGenerate(options) {
const currentDate = this.config._internal?.currentDate?.() ?? /* @__PURE__ */ new Date();
const { requestBody, warnings, outputFormat, usesStructuredSpeech } = await this.getArgs(options);
const { value: response, responseHeaders, rawValue: rawResponse } = await postJsonToApi({
url: `${this.config.baseURL}/models/${this.modelId}:generateContent`,
headers: combineHeaders(this.config.headers ? await resolve(this.config.headers) : void 0, options.headers),
body: requestBody,
failedResponseHandler: googleFailedResponseHandler,
successfulResponseHandler: createJsonResponseHandler(googleSpeechResponseSchema),
abortSignal: options.abortSignal,
fetch: this.config.fetch
});
let base64Audio;
let mimeType;
for (const candidate of response.candidates ?? []) {
for (const part of candidate.content?.parts ?? []) if (part.inlineData?.data) {
base64Audio = part.inlineData.data;
mimeType = part.inlineData.mimeType ?? void 0;
break;
}
if (base64Audio != null) break;
}
const sampleRate = parseSampleRate(mimeType) ?? DEFAULT_SAMPLE_RATE;
const bytes = base64Audio != null ? convertBase64ToUint8Array(base64Audio) : /* @__PURE__ */ new Uint8Array(0);
const isPcm = /^audio\/(?:l16|pcm)(?:;|$)/i.test(mimeType ?? "") || mimeType == null && !usesStructuredSpeech;
const audio = outputFormat === "AUDIO_WAV" && isPcm && bytes.length > 0 ? addWavHeader(bytes, sampleRate) : bytes;
if (outputFormat === "AUDIO_L16" && bytes.length > 0 && !usesStructuredSpeech) warnings.push({
type: "unsupported",
feature: "outputFormat",
details: `Returning raw PCM audio (signed 16-bit little-endian, mono, ${sampleRate} Hz). These bytes have no container header and are not directly playable; see providerMetadata.google for the sample rate and mime type.`
});
return {
audio,
warnings,
...response.usageMetadata != null && { usage: response.usageMetadata },
request: { body: JSON.stringify(requestBody) },
response: {
timestamp: currentDate,
modelId: this.modelId,
headers: responseHeaders,
body: rawResponse
},
providerMetadata: { google: {
sampleRate,
mimeType: mimeType ?? null
} }
};
}
};
/**
* Parses the sample rate from a PCM mime type such as `audio/L16;rate=24000`.
*/
function parseSampleRate(mimeType) {
if (mimeType == null) return;
const match = /rate=(\d+)/.exec(mimeType);
return match ? Number.parseInt(match[1], 10) : void 0;
}
/**
* Wraps raw signed 16-bit little-endian mono PCM in a minimal 44-byte WAV
* (RIFF/WAVE) container so the output is playable and detectable as `audio/wav`.
*/
function addWavHeader(pcm, sampleRate) {
const numChannels = 1;
const bitsPerSample = 16;
const blockAlign = 2;
const byteRate = sampleRate * blockAlign;
const dataSize = pcm.length;
const buffer = new ArrayBuffer(44 + dataSize);
const view = new DataView(buffer);
writeAscii(view, 0, "RIFF");
view.setUint32(4, 36 + dataSize, true);
writeAscii(view, 8, "WAVE");
writeAscii(view, 12, "fmt ");
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, numChannels, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, byteRate, true);
view.setUint16(32, blockAlign, true);
view.setUint16(34, bitsPerSample, true);
writeAscii(view, 36, "data");
view.setUint32(40, dataSize, true);
const out = new Uint8Array(buffer);
out.set(pcm, 44);
return out;
}
function writeAscii(view, offset, text) {
for (let i = 0; i < text.length; i++) view.setUint8(offset + i, text.charCodeAt(i));
}
//#endregion
//#region src/interactions/convert-google-interactions-usage.ts
function convertGoogleInteractionsUsage(usage) {
if (usage == null) return createNullLanguageModelUsage();
const totalInput = usage.total_input_tokens ?? 0;
const totalOutput = usage.total_output_tokens ?? 0;
const totalThought = usage.total_thought_tokens ?? 0;
const totalCached = usage.total_cached_tokens ?? 0;
return {
inputTokens: {
total: usage.total_input_tokens ?? void 0,
noCache: usage.total_input_tokens == null ? void 0 : totalInput - totalCached,
cacheRead: usage.total_cached_tokens ?? void 0,
cacheWrite: void 0
},
outputTokens: {
total: usage.total_output_tokens == null && usage.total_thought_tokens == null ? void 0 : totalOutput + totalThought,
text: usage.total_output_tokens ?? void 0,
reasoning: usage.total_thought_tokens ?? void 0
},
raw: usage
};
}
/**
* Extracts the per-modality output token breakdown from an Interactions usage
* record (e.g. `{ video: 57920, text: 12 }`).
*/
function getGoogleInteractionsOutputTokensByModality(usage) {
const byModality = usage?.output_tokens_by_modality;
if (byModality == null) return;
const result = {};
for (const entry of byModality) if (entry?.modality != null && entry.tokens != null) result[entry.modality] = entry.tokens;
return Object.keys(result).length > 0 ? result : void 0;
}
//#endregion
//#region src/interactions/extract-google-interactions-sources.ts
const KNOWN_DOC_EXTENSIONS = {
pdf: "application/pdf",
txt: "text/plain",
md: "text/markdown",
markdown: "text/markdown",
doc: "application/msword",
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
};
function inferDocMediaType(uriOrName) {
const lower = uriOrName.toLowerCase();
for (const [ext, media] of Object.entries(KNOWN_DOC_EXTENSIONS)) if (lower.endsWith(`.${ext}`)) return media;
return "application/octet-stream";
}
function basename(uriOrName) {
const parts = uriOrName.split("/");
const last = parts[parts.length - 1];
return last && last.length > 0 ? last : void 0;
}
/**
* Maps a single text-block annotation (`url_citation` / `file_citation` /
* `place_citation`) onto a `LanguageModelV4Source`. Returns `undefined` when
* the annotation lacks the minimum payload to form a source (e.g. a URL
* citation without a `url`).
*/
function annotationToSource({ annotation, generateId }) {
switch (annotation.type) {
case "url_citation": {
const urlCitation = annotation;
if (urlCitation.url == null || urlCitation.url.length === 0) return;
return {
type: "source",
sourceType: "url",
id: generateId(),
url: urlCitation.url,
...urlCitation.title != null ? { title: urlCitation.title } : {}
};
}
case "file_citation": {
const fileCitation = annotation;
const uri = fileCitation.url ?? fileCitation.document_uri ?? fileCitation.file_name;
if (uri == null || uri.length === 0) return void 0;
if (uri.startsWith("http://") || uri.startsWith("https://")) return {
type: "source",
sourceType: "url",
id: generateId(),
url: uri,
...fileCitation.file_name != null ? { title: fileCitation.file_name } : {}
};
const filename = fileCitation.file_name ?? basename(uri);
const mediaType = inferDocMediaType(uri);
return {
type: "source",
sourceType: "document",
id: generateId(),
mediaType,
title: fileCitation.file_name ?? filename ?? uri,
...filename != null ? { filename } : {}
};
}
case "place_citation": {
const placeCitation = annotation;
if (placeCitation.url == null || placeCitation.url.length === 0) return;
return {
type: "source",
sourceType: "url",
id: generateId(),
url: placeCitation.url,
...placeCitation.name != null ? { title: placeCitation.name } : {}
};
}
default: return;
}
}
/**
* Maps a built-in tool *result* content block to zero or more
* `LanguageModelV4Source` parts. The Interactions API exposes grounding
* sources both inline (via `text_annotation` deltas) and via tool-result
* content blocks; the latter is what this function consumes.
*
* Supported result kinds:
* - `url_context_result` -> URL sources for each fetched URL with `status: 'success'`
* - `google_search_result` -> URL sources (when `url` is present), search-suggestion
* entries are skipped (they are HTML widgets, not citations)
* - `google_maps_result` -> URL sources for each place with a `url`
* - `file_search_result` -> document sources (best-effort -- `result[]` is loosely typed)
*/
function builtinToolResultToSources({ block, generateId }) {
const sources = [];
switch (block.type) {
case "url_context_result": {
const result = block.result ?? [];
for (const entry of result) {
if (entry?.url == null || entry.url.length === 0) continue;
if (entry.status != null && entry.status !== "success") continue;
sources.push({
type: "source",
sourceType: "url",
id: generateId(),
url: entry.url
});
}
break;
}
case "google_search_result": {
const result = block.result ?? [];
for (const entry of result) {
const url = entry?.url;
if (url == null || url.length === 0) continue;
sources.push({
type: "source",
sourceType: "url",
id: generateId(),
url,
...entry.title != null ? { title: entry.title } : {}
});
}
break;
}
case "google_maps_result": {
const result = block.result ?? [];
for (const entry of result) for (const place of entry.places ?? []) {
if (place.url == null || place.url.length === 0) continue;
sources.push({
type: "source",
sourceType: "url",
id: generateId(),
url: place.url,
...place.name != null ? { title: place.name } : {}
});
}
break;
}
case "file_search_result": {
const result = block.result ?? [];
for (const raw of result) {
if (raw == null || typeof raw !== "object") continue;
const entry = raw;
const uri = entry.url ?? entry.document_uri ?? entry.file_name;
if (uri == null || uri.length === 0) continue;
if (uri.startsWith("http://") || uri.startsWith("https://")) {
sources.push({
type: "source",
sourceType: "url",
id: generateId(),
url: uri,
...entry.title != null ? { title: entry.title } : {}
});
continue;
}
const filename = entry.file_name ?? basename(uri);
const mediaType = inferDocMediaType(uri);
sources.push({
type: "source",
sourceType: "document",
id: generateId(),
mediaType,
title: entry.title ?? entry.file_name ?? filename ?? uri,
...filename != null ? { filename } : {}
});
}
break;
}
}
return sources;
}
/**
* Given a list of annotations attached to a single `text` content block,
* returns the corresponding `LanguageModelV4Source` parts (de-duplicated by
* URL/filename to avoid double-counting when the same citation reappears
* across deltas).
*/
function annotationsToSources({ annotations, generateId }) {
if (annotations == null) return [];
const seen = /* @__PURE__ */ new Set();
const sources = [];
for (const annotation of annotations) {
const source = annotationToSource({
annotation,
generateId
});
if (source == null) continue;
const key = source.sourceType === "url" ? `url:${source.url}` : `doc:${source.filename ?? source.title}`;
if (seen.has(key)) continue;
seen.add(key);
sources.push(source);
}
return sources;
}
//#endregion
//#region src/interactions/map-google-interactions-finish-reason.ts
function mapGoogleInteractionsFinishReason({ status, hasFunctionCall }) {
switch (status) {
case "completed": return hasFunctionCall ? "tool-calls" : "stop";
case "requires_action": return "tool-calls";
case "failed": return "error";
case "incomplete": return "length";
case "cancelled": return "other";
default: return "other";
}
}
//#endregion
//#region src/interactions/build-google-interactions-stream-transform.ts
const BUILTIN_TOOL_CALL_TYPES$1 = /* @__PURE__ */ new Set([
"google_search_call",
"code_execution_call",
"url_context_call",
"file_search_call",
"google_maps_call",
"mcp_server_tool_call"
]);
const BUILTIN_TOOL_RESULT_TYPES$1 = /* @__PURE__ */ new Set([
"google_search_result",
"code_execution_result",
"url_context_result",
"file_search_result",
"google_maps_result",
"mcp_server_tool_result"
]);
function builtinToolNameFromCallType$1(type) {
return type.replace(/_call$/, "");
}
function builtinToolNameFromResultType$1(type) {
return type.replace(/_result$/, "");
}
/**
* Builds a `TransformStream<ParseResult<GoogleInteractionsEvent>, LanguageModelV4StreamPart>`
* over the Interactions API SSE event stream.
*
* Surfaces text + thought (reasoning), function_call, image, built-in tool
* call/result steps, and `text_annotation` -> `source` parts.
*/
function buildGoogleInteractionsStreamTransform({ warnings, generateId, includeRawChunks, serviceTier: headerServiceTier }) {
let interactionId;
let usage;
let serviceTier = headerServiceTier;
let finishStatus;
let hasFunctionCall = false;
let hasCompletionEvent = false;
const openBlocks = /* @__PURE__ */ new Map();
const emittedSourceKeys = /* @__PURE__ */ new Set();
function sourceKey(source) {
return source.sourceType === "url" ? `url:${source.url}` : `doc:${source.filename ?? source.title}`;
}
return new TransformStream({
start(controller) {
controller.enqueue({
type: "stream-start",
warnings
});
},
transform(chunk, controller) {
if (includeRawChunks) controller.enqueue({
type: "raw",
rawValue: chunk.rawValue
});
if (!chunk.success) {
finishStatus = "failed";
controller.enqueue({
type: "error",
error: chunk.error
});
return;
}
const value = chunk.value;
const eventType = value.event_type;
switch (eventType) {
case "interaction.created": {
const interaction = value.interaction;
interactionId = interaction?.id != null && interaction.id.length > 0 ? interaction.id : void 0;
const created = interaction?.created;
let timestamp;
if (typeof created === "string") {
const parsed = new Date(created);
if (!Number.isNaN(parsed.getTime())) timestamp = parsed;
}
controller.enqueue({
type: "response-metadata",
...interactionId != null ? { id: interactionId } : {},
modelId: interaction?.model,
...timestamp ? { timestamp } : {}
});
break;
}
case "step.start": {
const event = value;
const step = event.step;
const index = event.index;
const blockId = `${interactionId ?? "interaction"}:${index}`;
const stepType = step?.type;
if (stepType === "model_output") {
const initial = step?.content?.[0];
if (initial?.type === "text") {
openBlocks.set(index, {
kind: "text",
id: blockId,
emittedSourceKeys: /* @__PURE__ */ new Set()
});
controller.enqueue({
type: "text-start",
id: blockId
});
const initialSources = annotationsToSources({
annotations: initial.annotations,
generateId
});
for (const source of initialSources) {
const key = sourceKey(source);
if (emittedSourceKeys.has(key)) continue;
emittedSourceKeys.add(key);
controller.enqueue(source);
}
} else if (initial?.type === "image") openBlocks.set(index, {
kind: "image",
id: blockId,
...initial.data != null ? { data: initial.data } : {},
...initial.mime_type != null ? { mimeType: initial.mime_type } : {},
...initial.uri != null ? { uri: initial.uri } : {}
});
else openBlocks.set(index, {
kind: "pending_model_output",
id: blockId
});
} else if (stepType === "thought") {
const signature = step?.signature;
openBlocks.set(index, {
kind: "reasoning",
id: blockId,
...signature != null ? { signature } : {}
});
controller.enqueue({
type: "reasoning-start",
id: blockId
});
if (Array.isArray(step?.summary)) {
for (const item of step.summary) if (item?.type === "text" && typeof item.text === "string") controller.enqueue({
type: "reasoning-delta",
id: blockId,
delta: item.text
});
}
} else if (stepType === "processing_call" || stepType === "processing_result") {
const google = {};
if (step?.signature != null) google.signature = step.signature;
if (interactionId != null) google.interactionId = interactionId;
if (stepType === "processing_call") google.processingId = step?.id || blockId;
else google.processingCallId = step?.call_id || blockId;
openBlocks.set(index, {
kind: "custom",
id: blockId,
customKind: `google.${stepType}`,
google
});
} else if (stepType === "function_call") {
const toolCallId = step?.id || blockId;
const toolName = step?.name ?? "unknown";
hasFunctionCall = true;
const state = {
kind: "function_call",
id: blockId,
toolCallId,
toolName,
argumentsAccum: "",
...step?.signature != null ? { signature: step.signature } : {}
};
openBlocks.set(index, state);
controller.enqueue({
type: "tool-input-start",
id: toolCallId,
toolName
});
} else if (stepType != null && BUILTIN_TOOL_CALL_TYPES$1.has(stepType)) {
const toolName = stepType === "mcp_server_tool_call" ? step?.name ?? "mcp_server_tool" : builtinToolNameFromCallType$1(stepType);
const state = {
kind: "builtin_tool_call",
id: blockId,
blockType: stepType,
toolCallId: step?.id || blockId,
toolName,
arguments: step?.arguments ?? {},
callEmitted: false
};
openBlocks.set(index, state);
} else if (stepType != null && BUILTIN_TOOL_RESULT_TYPES$1.has(stepType)) {
const toolName = stepType === "mcp_server_tool_result" ? step?.name ?? "mcp_server_tool" : builtinToolNameFromResultType$1(stepType);
const state = {
kind: "builtin_tool_result",
id: blockId,
blockType: stepType,
callId: step?.call_id || blockId,
toolName,
result: step?.result ?? null,
...step?.is_error != null ? { isError: step.is_error } : {},
resultEmitted: false
};
openBlocks.set(index, state);
} else openBlocks.set(index, {
kind: "unknown",
id: blockId
});
break;
}
case "step.delta": {
const event = value;
let open = openBlocks.get(event.index);
if (open == null) break;
const dtype = event.delta?.type;
if (open.kind === "pending_model_output") {
if (dtype === "text" || dtype === "text_annotation" || dtype === "text_annotation_delta") {
const promoted = {
kind: "text",
id: open.id,
emittedSourceKeys: /* @__PURE__ */ new Set()
};
openBlocks.set(event.index, promoted);
open = promoted;
controller.enqueue({
type: "text-start",
id: promoted.id
});
}
}
if (dtype === "image" && (open.kind === "pending_model_output" || open.kind === "text" || open.kind === "image")) {
const imageDelta = event.delta;
const google = {};
if (interactionId != null) google.interactionId = interactionId;
const providerMetadata = Object.keys(google).length > 0 ? { google } : void 0;
if (imageDelta?.data != null && imageDelta.data.length > 0) controller.enqueue({
type: "file",
mediaType: imageDelta.mime_type ?? "image/png",
data: {
type: "data",
data: imageDelta.data
},
...providerMetadata ? { providerMetadata } : {}
});
else if (imageDelta?.uri != null && imageDelta.uri.length > 0) controller.enqueue({
type: "file",
mediaType: imageDelta.mime_type ?? "image/png",
data: {
type: "url",
url: new URL(imageDelta.uri)
},
...providerMetadata ? { providerMetadata } : {}
});
if (open.kind === "image") {
open.data = void 0;
open.uri = void 0;
}
break;
}
if (dtype === "video" && (open.kind === "pending_model_output" || open.kind === "text")) {
const videoDelta = event.delta;
const google = {};
if (interactionId != null) google.interactionId = interactionId;
const providerMetadata = Object.keys(google).length > 0 ? { google } : void 0;
if (videoDelta?.data != null && videoDelta.data.length > 0) controller.enqueue({
type: "file",
mediaType: videoDelta.mime_type ?? "video/mp4",
data: {
type: "data",
data: videoDelta.data
},
...providerMetadata ? { providerMetadata } : {}
});
else if (videoDelta?.uri != null && videoDelta.uri.length > 0) controller.enqueue({
type: "file",
mediaType: videoDelta.mime_type ?? "video/mp4",
data: {
type: "url",
url: new URL(videoDelta.uri)
},
...providerMetadata ? { providerMetadata } : {}
});
break;
}
const delta = event.delta;
if (open.kind === "custom" && (delta?.type === "processing_call" || delta?.type === "processing_result")) {
if (delta.signature != null) open.google.signature = delta.signature;
if (delta.type === "processing_call" && delta.id != null && delta.id.length > 0) open.google.processingId = delta.id;
if (delta.type === "processing_result" && delta.call_id != null && delta.call_id.length > 0) open.google.processingCallId = delta.call_id;
} else if (open.kind === "text" && delta?.type === "text") {
const text = delta.text ?? "";
if (text.length > 0) controller.enqueue({
type: "text-delta",
id: open.id,
delta: text
});
} else if (open.kind === "text" && (delta?.type === "text_annotation" || delta?.type === "text_annotation_delta")) {
const sources = annotationsToSources({
annotations: delta.annotations,
generateId
});
for (const source of sources) {
const key = sourceKey(source);
if (emittedSourceKeys.has(key)) continue;
emittedSourceKeys.add(key);
open.emittedSourceKeys.add(key);
controller.enqueue(source);
}
} else if (open.kind === "image" && delta?.type === "image") {
if (delta.data != null) open.data = delta.data;
if (delta.mime_type != null) open.mimeType = delta.mime_type;
if (delta.uri != null) open.uri = delta.uri;
} else if (open.kind === "reasoning") {
if (delta?.type === "thought_summary") {
const item = delta.content;
if (item?.type === "text" && typeof item.text === "string") controller.enqueue({
type: "reasoning-delta",
id: open.id,
delta: item.text
});
} else if (delta?.type === "thought_signature") {
const signature = delta.signature;
if (signature != null) open.signature = signature;
}
} else if (open.kind === "function_call" && delta?.type === "arguments_delta") {
const slice = typeof delta.arguments === "string" ? delta.arguments : "";
if (slice.length > 0) {
open.argumentsAccum += slice;
controller.enqueue({
type: "tool-input-delta",
id: open.toolCallId,
delta: slice
});
}
if (delta.id != null && delta.id.length > 0) open.toolCallId = delta.id;
if (delta.signature != null) open.signature = delta.signature;
hasFunctionCall = true;
} else if (open.kind === "builtin_tool_call" && delta?.type === open.blockType) {
if (delta.id != null && delta.id.length > 0) open.toolCallId = delta.id;
if (delta.arguments != null && typeof delta.arguments === "object") open.arguments = delta.arguments;
if (delta.name != null && open.blockType === "mcp_server_tool_call") open.toolName = delta.name;
} else if (open.kind === "builtin_tool_result" && delta?.type === open.blockType) {
if (delta.call_id != null && delta.call_id.length > 0) open.callId = delta.call_id;
if (delta.result !== void 0) open.result = delta.result;
if (delta.is_error != null) open.isError = delta.is_error;
if (delta.name != null && open.blockType === "mcp_server_tool_result") open.toolName = delta.name;
}
break;
}
case "step.stop": {
const event = value;
const open = openBlocks.get(event.index);
if (open == null) break;
if (open.kind === "text") {
const textProviderMetadata = interactionId != null ? { google: { interactionId } } : void 0;
controller.enqueue({
type: "text-end",
id: open.id,
...textProviderMetadata ? { providerMetadata: textProviderMetadata } : {}
});
} else if (open.kind === "reasoning") {
const google = {};
if (open.signature != null) google.signature = open.signature;
if (interactionId != null) google.interactionId = interactionId;
const providerMetadata = Object.keys(google).length > 0 ? { google } : void 0;
controller.enqueue({
type: "reasoning-end",
id: open.id,
...providerMetadata ? { providerMetadata } : {}
});
} else if (open.kind === "image") {
const google = {};
if (interactionId != null) google.interactionId = interactionId;
const providerMetadata = Object.keys(google).length > 0 ? { google } : void 0;
if (open.data != null && open.data.length > 0) controller.enqueue({
type: "file",
mediaType: open.mimeType ?? "image/png",
data: {
type: "data",
data: open.data
},
...providerMetadata ? { providerMetadata } : {}
});
else if (open.uri != null && open.uri.length > 0) controller.enqueue({
type: "file",
mediaType: open.mimeType ?? "image/png",
data: {
type: "url",
url: new URL(open.uri)
},
...providerMetadata ? { providerMetadata } : {}
});
} else if (open.kind === "function_call") {
const accumulated = open.argumentsAccum.length > 0 ? open.argumentsAccum : "{}";
controller.enqueue({
type: "tool-input-end",
id: open.toolCallId
});
const google = {};
if (open.signature != null) google.signature = open.signature;
if (interactionId != null) google.interactionId = interactionId;
const providerMetadata = Object.keys(google).length > 0 ? { google } : void 0;
controller.enqueue({
type: "tool-call",
toolCallId: open.toolCallId,
toolName: open.toolName,
input: accumulated,
...providerMetadata ? { providerMetadata } : {}
});
} else if (open.kind === "custom") controller.enqueue({
type: "custom",
kind: open.customKind,
providerMetadata: { google: open.google }
});
else if (open.kind === "builtin_tool_call" && !open.callEmitted) {
controller.enqueue({
type: "tool-call",
toolCallId: open.toolCallId,
toolName: open.toolName,
input: JSON.stringify(open.arguments ?? {}),
providerExecuted: true
});
open.callEmitted = true;
} else if (open.kind === "builtin_tool_result" && !open.resultEmitted) {
controller.enqueue({
type: "tool-result",
toolCallId: open.callId,
toolName: open.toolName,
result: open.result ?? null
});
open.resultEmitted = true;
const sources = builtinToolResultToSources({
block: {
type: open.blockType,
call_id: open.callId,
result: open.result
},
generateId
});
for (const source of sources) {
const key = sourceKey(source);
if (emittedSourceKeys.has(key)) continue;
emittedSourceKeys.add(key);
controller.enqueue(source);
}
}
openBlocks.delete(event.index);
break;
}
case "interaction.status_update":
case "interaction.in_progress":
case "interaction.requires_action": {
const event = value;
if (event.status != null) finishStatus = event.status;
else if (eventType === "interaction.requires_action") finishStatus = "requires_action";
else finishStatus = "in_progress";
break;
}
case "interaction.completed": {
hasCompletionEvent = true;
const interaction = value.interaction;
if (interaction?.id != null && interaction.id.length > 0) interactionId = interaction.id;
if (interaction?.status != null) finishStatus = interaction.status;
if (interaction?.usage != null) usage = interaction.usage;
if (interaction?.service_tier != null) serviceTier = interaction.service_tier;
break;
}
case "error": {
const event = value;
finishStatus = "failed";
controller.enqueue({
type: "error",
error: createProviderStreamError({
message: event.error?.message ?? "Unknown interaction error",
type: event.event_type,
code: event.error?.code ?? void 0,
data: event
})
});
break;
}
}
},
flush(controller) {
const isInterrupted = !hasCompletionEvent && (finishStatus == null || finishStatus === "in_progress");
const finishReason = {
unified: isInterrupted ? "interrupted" : mapGoogleInteractionsFinishReason({
status: finishStatus,
hasFunctionCall
}),
raw: isInterrupted ? void 0 : finishStatus
};
const outputTokensByModality = getGoogleInteractionsOutputTokensByModality(usage);
const providerMetadata = { google: {
...interactionId != null ? { interactionId } : {},
...serviceTier != null ? { serviceTier } : {},
...outputTokensByModality != null ? { outputTokensByModality } : {}
} };
controller.enqueue({
type: "finish",
finishReason,
usage: convertGoogleInteractionsUsage(usage),
providerMetadata
});
}
});
}
//#endregion
//#region src/interactions/convert-to-google-interactions-input.ts
/**
* Converts an AI SDK `LanguageModelV4Prompt` into the Gemini Interactions
* request shape (`{ input: Array<Step>, system_instruction }`).
*
* Prior assistant content round-trips as discrete steps:
* - text / image content → `model_output` step with a single `content` array
* - reasoning → `thought` step (`signature` + `summary`)
* - tool-call → `function_call` step
* User turns (and tool-result turns from the previous round) are sent as
* `user_input` steps whose `content[]` holds the user's parts (text, files,
* and — for tool-result turns — `function_result` blocks).
*
* Handles text parts, file parts (image / audio / document / video, all four
* `data.type` shapes), tool-call/tool-result round-tripping, per-step
* `signature` round-tripping, and statefulness compaction (drop assistant/tool
* turns whose `providerOptions.google.interactionId === previousInteractionId`).
*/
function convertToGoogleInteractionsInput({ prompt, previousInteractionId, store, mediaResolution }) {
const warnings = [];
const incoherentCombo = previousInteractionId != null && store === false;
const shouldCompact = previousInteractionId != null && store !== false;
if (incoherentCombo) warnings.push({
type: "other",
message: "google.interactions: providerOptions.google.previousInteractionId was set together with store: false. These are incoherent (the prior interaction cannot be referenced when nothing was stored on the server); the full history will be sent and previous_interaction_id will still be emitted."
});
const compactedPrompt = shouldCompact ? compactPromptForPreviousInteraction({
prompt,
previousInteractionId
}) : prompt;
const systemTexts = [];
const steps = [];
for (const message of compactedPrompt) switch (message.role) {
case "system":
systemTexts.push(message.content);
break;
case "user": {
const content = [];
for (const part of message.content) if (part.type === "text") content.push({
type: "text",
text: part.text
});
else if (part.type === "file") {
const fileBlock = convertFilePartToContent({
part,
warnings,
mediaResolution
});
if (fileBlock != null) content.push(fileBlock);
}
const merged = mergeAdjacentTextContent(content);
if (merged.length > 0) steps.push({
type: "user_input",
content: merged
});
break;
}
case "assistant": {
let pendingModelOutput = [];
const flushModelOutput = () => {
if (pendingModelOutput.length > 0) {
steps.push({
type: "model_output",
content: pendingModelOutput
});
pendingModelOutput = [];
}
};
for (const part of message.content) if (part.type === "text") pendingModelOutput.push({
type: "text",
text: part.text
});
else if (part.type === "reasoning") {
flushModelOutput();
const signature = part.providerOptions?.google?.signature;
steps.push({
type: "thought",
...signature != null ? { signature } : {},
summary: part.text.length > 0 ? [{
type: "text",
text: part.text
}] : void 0
});
} else if (part.type === "file") {
const fileBlock = convertFilePartToContent({
part,
warnings,
mediaResolution
});
if (fileBlock != null) pendingModelOutput.push(fileBlock);
} else if (part.type === "custom") {
flushModelOutput();
const google = part.providerOptions?.google;
const signature = typeof google?.signature === "string" ? google.signature : void 0;
if (part.kind === "google.processing_call" && typeof google?.processingId === "string") steps.push({
type: "processing_call",
id: google.processingId,
...signature != null ? { signature } : {}
});
else if (part.kind === "google.processing_result" && typeof google?.processingCallId === "string") steps.push({
type: "processing_result",
call_id: google.processingCallId,
...signature != null ? { signature } : {}
});
else warnings.push({
type: "other",
message: `google.interactions: unsupported or invalid custom assistant content part "${part.kind}"; part dropped.`
});
} else if (part.type === "tool-call") {
flushModelOutput();
const signature = part.providerOptions?.google?.signature;
const args = typeof part.input === "string" ? safeParseToolArgs(part.input) : part.input ?? {};
steps.push({
type: "function_call",
id: part.toolCallId,
name: part.toolName,
arguments: args,
...signature != null ? { signature } : {}
});
} else warnings.push({
type: "other",
message: `google.interactions: unsupported assistant content part type "${part.type}"; part dropped.`
});
flushModelOutput();
break;
}
case "tool": {
const content = [];
for (const part of message.content) {
if (part.type !== "tool-result") {
warnings.push({
type: "other",
message: `google.interactions: unsupported tool message part type "${part.type}"; part dropped.`
});
continue;
}
const block = convertToolResultPart({
toolCallId: part.toolCallId,
toolName: part.toolName,
output: part.output,
signature: part.providerOptions?.google?.signature,
warnings
});
content.push(block);
}
if (content.length > 0) steps.push({
type: "user_input",
content
});
break;
}
}
return {
input: steps,
systemInstruction: systemTexts.length > 0 ? systemTexts.join("\n\n") : void 0,
warnings
};
}
/**
* Maps a single AI SDK `LanguageModelV4FilePart` to a Gemini Interactions
* content block (`image` / `audio` / `document` / `video`).
*/
function convertFilePartToContent({ part, warnings, mediaResolution }) {
if (part.data.type === "text") return {
type: "text",
text: part.data.text
};
const topLevel = getTopLevelMediaType(part.mediaType);
let kind;
switch (topLevel) {
case "image":
kind = "image";
break;
case "audio":
kind = "audio";
break;
case "video":
kind = "video";
break;
case "application":
case "text":
kind = "document";
break;
default: kind = void 0;
}
if (kind == null) {
warnings.push({
type: "other",
message: `google.interactions: unsupported file media type "${part.mediaType}"; part dropped.`
});
return;
}
const resolutionField = mediaResolution != null && (kind === "image" || kind === "video") ? { resolution: mediaResolution } : {};
const processingField = kind === "video" ? getVideoProcessingField({
part,
warnings
}) : {};
switch (part.data.type) {
case "data": {
const mimeType = resolveFullMediaType({ part });
return {
type: kind,
data: convertToBase64(part.data.data),
mime_type: mimeType,
...resolutionField,
...processingField
};
}
case "url": return {
type: kind,
uri: part.data.url.toString(),
...isFullMediaType(part.mediaType) ? { mime_type: part.mediaType } : {},
...resolutionField,
...processingField
};
case "reference": {
const uri = resolveProviderReference({
reference: part.data.reference,
provider: "google"
});
return {
type: kind,
uri,
...isFullMediaType(part.mediaType) ? { mime_type: part.mediaType } : {},
...resolutionField,
...processingField
};
}
}
}
function getVideoProcessingField({ part, warnings }) {
const processing = (part.providerOptions?.google)?.processing;
if (processing == null) return {};
if (processing === "agentic" || processing === "static") return { processing };
if (typeof processing === "object" && !Array.isArray(processing) && processing.type === "static") {
const config = processing;
return { processing: {
type: "static",
...typeof config.startOffset === "number" ? { start_offset: `${config.startOffset}s` } : {},
...typeof config.endOffset === "number" ? { end_offset: `${config.endOffset}s` } : {},
...typeof config.fps === "number" ? { fps: config.fps } : {}
} };
}
warnings.push({
type: "other",
message: "google.interactions: invalid providerOptions.google.processing on video file part; expected \"agentic\", \"static\", or a static processing configuration. Option dropped."
});
return {};
}
function compactPromptForPreviousInteraction({ prompt, previousInteractionId }) {
const out = [];
const droppedToolCallIds = /* @__PURE__ */ new Set();
for (const message of prompt) {
if (message.role === "assistant") {
if (message.content.some((part) => {
return part.providerOptions?.google?.interactionId === previousInteractionId;
})) {
for (const part of message.content) if (part.type === "tool-call") droppedToolCallIds.add(part.toolCallId);
continue;
}
out.push(message);
continue;
}
if (message.role === "tool") {
const remaining = message.content.filter((part) => {
if (part.type !== "tool-result") return true;
return !droppedToolCallIds.has(part.toolCallId);
});
if (remaining.length === 0) continue;
out.push({
...message,
content: remaining
});
continue;
}
out.push(message);
}
return out;
}
function safeParseToolArgs(input) {
try {
const parsed = secureJsonParse(input);
if (parsed != null && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
return { value: parsed };
} catch {
return { value: input };
}
}
function convertToolResultPart({ toolCallId, toolName, output, signature, warnings }) {
const base = {
type: "function_result",
call_id: toolCallId,
name: toolName,
...signature != null ? { signature } : {}
};
switch (output.type) {
case "text": return {
...base,
result: output.value
};
case "json": return {
...base,
result: JSON.stringify(output.value)
};
case "error-text": return {
...base,
is_error: true,
result: output.value
};
case "error-json": return {
...base,
is_error: true,
result: JSON.stringify(output.value)
};
case "execution-denied": return {
...base,
is_error: true,
result: output.reason ?? "Tool execution denied by user."
};
case "content": {
const blocks = [];
for (const item of output.value) if (item.type === "text") blocks.push({
type: "text",
text: item.text
});
else if (item.type === "file") {
if (getTopLevelMediaType(item.mediaType) !== "image") {
warnings.push({
type: "other",
message: `google.interactions: tool-result file with mediaType "${item.mediaType}" is not supported (Interactions \`function_result.result\` accepts only text and image content); part dropped.`
});
continue;
}
const imageBlock = filePartToImageBlock({
part: item,
warnings
});
if (imageBlock != null) blocks.push(imageBlock);
} else warnings.push({
type: "other",
message: `google.interactions: tool-result content part type "${item.type}" is not supported; part dropped.`
});
return {
...base,
result: blocks
};
}
}
}
function filePartToImageBlock({ part, warnings }) {
switch (part.data.type) {
case "data": {
const mimeType = isFullMediaType(part.mediaType) ? part.mediaType : resolveFullMediaType({ part: {
type: "file",
mediaType: part.mediaType,
data: part.data
} });
return {
type: "image",
data: convertToBase64(part.data.data),
mime_type: mimeType
};
}
case "url": return {
type: "image",
uri: part.data.url.toString(),
...isFullMediaType(part.mediaType) ? { mime_type: part.mediaType } : {}
};
case "reference": return {
type: "image",
uri: resolveProviderReference({
reference: part.data.reference,
provider: "google"
}),
...isFullMediaType(part.mediaType) ? { mime_type: part.mediaType } : {}
};
case "text":
warnings.push({
type: "other",
message: "google.interactions: tool-result image part with `data.type === \"text\"` is not representable as an image; part dropped."
});
return;
}
}
function mergeAdjacentTextContent(content) {
if (content.length < 2) return content;
const result = [];
for (const block of content) {
const last = result[result.length - 1];
if (block.type === "text" && last != null && last.type === "text" && last.annotations == null && block.annotations == null) {
const merged = {
type: "text",
text: `${last.text}\n\n${block.text}`
};
result[result.length - 1] = merged;
continue;
}
result.push(block);
}
return result;
}
//#endregion
//#region src/interactions/google-interactions-api.ts
const tokenByModalitySchema = () => z.object({
modality: z.string().nullish(),
tokens: z.number().nullish()
}).loose();
const usageSchema = () => z.object({
total_input_tokens: z.number().nullish(),
total_output_tokens: z.number().nullish(),
total_thought_tokens: z.number().nullish(),
total_cached_tokens: z.number().nullish(),
total_tool_use_tokens: z.number().nullish(),
total_tokens: z.number().nullish(),
input_tokens_by_modality: z.array(tokenByModalitySchema()).nullish(),
output_tokens_by_modality: z.array(tokenByModalitySchema()).nullish(),
cached_tokens_by_modality: z.array(tokenByModalitySchema()).nullish(),
tool_use_tokens_by_modality: z.array(tokenByModalitySchema()).nullish(),
grounding_tool_count: z.array(z.object({
type: z.string().nullish(),
count: z.number().nullish()
}).loose()).nullish()
}).loose();
const interactionStatusSchema = () => z.enum([
"in_progress",
"requires_action",
"completed",
"failed",
"cancelled",
"incomplete"
]);
const annotationSchema = () => {
const urlCitation = z.object({
type: z.literal("url_citation"),
url: z.string().nullish(),
title: z.string().nullish(),
start_index: z.number().nullish(),
end_index: z.number().nullish()
}).loose();
const fileCitation = z.object({
type: z.literal("file_citation"),
file_name: z.string().nullish(),
document_uri: z.string().nullish(),
url: z.string().nullish(),
page_number: z.number().nullish(),
media_id: z.string().nullish(),
start_index: z.number().nullish(),
end_index: z.number().nullish(),
custom_metadata: z.record(z.string(), z.unknown()).nullish()
}).loose();
const placeCitation = z.object({
type: z.literal("place_citation"),
name: z.string().nullish(),
url: z.string().nullish(),
place_id: z.string().nullish(),
start_index: z.number().nullish(),
end_index: z.number().nullish()
}).loose();
return z.union([
urlCitation,
fileCitation,
placeCitation,
z.object({ type: z.string() }).loose()
]);
};
const thoughtSummaryItemSchema = () => z.object({
type: z.string(),
text: z.string().nullish(),
data: z.string().nullish(),
mime_type: z.string().nullish()
}).loose();
const contentBlockSchema = () => {
const textContent = z.object({
type: z.literal("text"),
text: z.string(),
annotations: z.array(annotationSchema()).nullish()
}).loose();
const imageContent = z.object({
type: z.literal("image"),
data: z.string().nullish(),
mime_type: z.string().nullish(),
resolution: z.enum([
"low",
"medium",
"high",
"ultra_high"
]).nullish(),
uri: z.string().nullish()
}).loose();
const videoContent = z.object({
type: z.literal("video"),
data: z.string().nullish(),
mime_type: z.string().nullish(),
uri: z.string().nullish()
}).loose();
return z.union([
textContent,
imageContent,
videoContent,
z.object({ type: z.string() }).loose()
]);
};
const BUILTIN_TOOL_CALL_STEP_TYPES = [
"google_search_call",
"code_execution_call",
"url_context_call",
"file_search_call",
"google_maps_call",
"mcp_server_tool_call"
];
const BUILTIN_TOOL_RESULT_STEP_TYPES = [
"google_search_result",
"code_execution_result",
"url_context_result",
"file_search_result",
"google_maps_result",
"mcp_server_tool_result"
];
const stepSchema = () => {
const userInputStep = z.object({
type: z.literal("user_input"),
content: z.array(contentBlockSchema()).nullish()
}).loose();
const modelOutputStep = z.object({
type: z.literal("model_output"),
content: z.array(contentBlockSchema()).nullish()
}).loose();
const functionCallStep = z.object({
type: z.literal("function_call"),
id: z.string(),
name: z.string(),
arguments: z.record(z.string(), z.unknown()).nullish(),
signature: z.string().nullish()
}).loose();
const thoughtStep = z.object({
type: z.literal("thought"),
signature: z.string().nullish(),
summary: z.array(thoughtSummaryItemSchema()).nullish()
}).loose();
const processingCallStep = z.object({
type: z.literal("processing_call"),
id: z.string(),
signature: z.string().nullish()
}).loose();
const processingResultStep = z.object({
type: z.literal("processing_result"),
call_id: z.string(),
signature: z.string().nullish()
}).loose();
const builtinToolCallStep = z.object({
type: z.enum(BUILTIN_TOOL_CALL_STEP_TYPES),
id: z.string(),
arguments: z.record(z.string(), z.unknown()).nullish(),
name: z.string().nullish(),
server_name: z.string().nullish(),
search_type: z.string().nullish(),
signature: z.string().nullish()
}).loose();
const builtinToolResultStep = z.object({
type: z.enum(BUILTIN_TOOL_RESULT_STEP_TYPES),
call_id: z.string(),
result: z.unknown().nullish(),
is_error: z.boolean().nullish(),
name: z.string().nullish(),
server_name: z.string().nullish(),
signature: z.string().nullish()
}).loose();
return z.union([
userInputStep,
modelOutputStep,
functionCallStep,
thoughtStep,
processingCallStep,
processingResultStep,
builtinToolCallStep,
builtinToolResultStep,
z.object({ type: z.string() }).loose()
]);
};
const googleInteractionsResponseSchema = lazySchema(() => zodSchema(z.object({
id: z.string().nullish(),
created: z.string().nullish(),
updated: z.string().nullish(),
status: interactionStatusSchema(),
model: z.string().nullish(),
agent: z.string().nullish(),
steps: z.array(stepSchema()).nullish(),
usage: usageSchema().nullish(),
service_tier: z.string().nullish(),
previous_interaction_id: z.string().nullish(),
response_modalities: z.array(z.string()).nullish()
}).loose()));
const googleInteractionsEventSchema = lazySchema(() => zodSchema((() => {
const status = interactionStatusSchema();
const annotation = annotationSchema();
const thoughtSummaryItem = thoughtSummaryItemSchema();
const interactionCreatedEvent = z.object({
event_type: z.literal("interaction.created"),
event_id: z.string().nullish(),
interaction: z.object({
id: z.string().nullish(),
created: z.string().nullish(),
model: z.string().nullish(),
agent: z.string().nullish(),
status: status.nullish()
}).loose()
}).loose();
const stepStartEvent = z.object({
event_type: z.literal("step.start"),
event_id: z.string().nullish(),
index: z.number(),
step: stepSchema()
}).loose();
const stepDeltaText = z.object({
type: z.literal("text"),
text: z.string()
}).loose();
const stepDeltaThoughtSummary = z.object({
type: z.literal("thought_summary"),
content: thoughtSummaryItem.nullish()
}).loose();
const stepDeltaThoughtSignature = z.object({
type: z.literal("thought_signature"),
signature: z.string().nullish()
}).loose();
const stepDeltaArgumentsDelta = z.object({
type: z.literal("arguments_delta"),
arguments: z.string().nullish(),
id: z.string().nullish(),
signature: z.string().nullish()
}).loose();
const stepDeltaTextAnnotation = z.object({
type: z.enum(["text_annotation_delta", "text_annotation"]),
annotations: z.array(annotation).nullish()
}).loose();
const stepDeltaImage = z.object({
type: z.literal("image"),
data: z.string().nullish(),
mime_type: z.string().nullish(),
resolution: z.enum([
"low",
"medium",
"high",
"ultra_high"
]).nullish(),
uri: z.string().nullish()
}).loose();
const stepDeltaVideo = z.object({
type: z.literal("video"),
data: z.string().nullish(),
mime_type: z.string().nullish(),
uri: z.string().nullish()
}).loose();
const stepDeltaBuiltinToolCall = z.object({
type: z.enum(BUILTIN_TOOL_CALL_STEP_TYPES),
id: z.string().nullish(),
arguments: z.record(z.string(), z.unknown()).nullish(),
name: z.string().nullish(),
server_name: z.string().nullish(),
search_type: z.string().nullish(),
signature: z.string().nullish()
}).loose();
const stepDeltaBuiltinToolResult = z.object({
type: z.enum(BUILTIN_TOOL_RESULT_STEP_TYPES),
call_id: z.string().nullish(),
result: z.unknown().nullish(),
is_error: z.boolean().nullish(),
name: z.string().nullish(),
server_name: z.string().nullish(),
signature: z.string().nullish()
}).loose();
const stepDeltaUnknown = z.object({ type: z.string() }).loose();
const stepDeltaUnion = z.union([
stepDeltaText,
stepDeltaImage,
stepDeltaVideo,
stepDeltaThoughtSummary,
stepDeltaThoughtSignature,
stepDeltaArgumentsDelta,
stepDeltaTextAnnotation,
stepDeltaBuiltinToolCall,
stepDeltaBuiltinToolResult,
stepDeltaUnknown
]);
const stepDeltaEvent = z.object({
event_type: z.literal("step.delta"),
event_id: z.string().nullish(),
index: z.number(),
delta: stepDeltaUnion
}).loose();
const stepStopEvent = z.object({
event_type: z.literal("step.stop"),
event_id: z.string().nullish(),
index: z.number()
}).loose();
const interactionStatusUpdateEvent = z.object({
event_type: z.literal("interaction.status_update"),
event_id: z.string().nullish(),
interaction_id: z.string().nullish(),
status: status.nullish()
}).loose();
const interactionInProgressEvent = z.object({
event_type: z.literal("interaction.in_progress"),
event_id: z.string().nullish(),
interaction_id: z.string().nullish(),
status: status.nullish()
}).loose();
const interactionRequiresActionEvent = z.object({
event_type: z.literal("interaction.requires_action"),
event_id: z.string().nullish(),
interaction_id: z.string().nullish(),
status: status.nullish()
}).loose();
const interactionCompletedEvent = z.object({
event_type: z.literal("interaction.completed"),
event_id: z.string().nullish(),
interaction: z.object({
id: z.string().nullish(),
status: status.nullish(),
usage: usageSchema().nullish(),
service_tier: z.string().nullish()
}).loose()
}).loose();
const errorEvent = z.object({
event_type: z.literal("error"),
event_id: z.string().nullish(),
error: z.object({
code: z.string().nullish(),
message: z.string().nullish()
}).loose().nullish()
}).loose();
const unknownEvent = z.object({ event_type: z.string() }).loose();
return z.union([
interactionCreatedEvent,
stepStartEvent,
stepDeltaEvent,
stepStopEvent,
interactionStatusUpdateEvent,
interactionInProgressEvent,
interactionRequiresActionEvent,
interactionCompletedEvent,
errorEvent,
unknownEvent
]);
})()));
//#endregion
//#region src/interactions/google-interactions-language-model-options.ts
/**
* Provider-options schema for `google.interactions(...)` calls. Read from the
* shared `providerOptions.google.*` namespace (per PRD); per-call options that
* the AI SDK doesn't natively expose live here.
*
* All fields are `.nullish()` per the existing google provider convention.
*/
const googleInteractionsLanguageModelOptions = lazySchema(() => zodSchema(z.object({
/** User-defined request metadata, including `safety_identifier`. */
labels: z.record(z.string(), z.string()).nullish(),
previousInteractionId: z.string().nullish(),
store: z.boolean().nullish(),
agent: z.string().nullish(),
agentConfig: z.union([z.object({ type: z.literal("dynamic") }).loose(), z.object({
type: z.literal("deep-research"),
thinkingSummaries: z.enum(["auto", "none"]).nullish(),
visualization: z.enum(["off", "auto"]).nullish(),
collaborativePlanning: z.boolean().nullish()
})]).nullish(),
thinkingLevel: z.enum([
"minimal",
"low",
"medium",
"high",
"xhigh"
]).nullish(),
thinkingSummaries: z.enum(["auto", "none"]).nullish(),
/**
* Output-format entries that map directly to the API's `response_format`
* array. Use this to request image, audio, video, or non-JSON text
* outputs with modality-specific controls.
*
* Entries are sent in order. The AI SDK call-level `responseFormat: {
* type: 'json', schema }` still drives JSON-mode and adds a matching
* text entry automatically; entries listed here are appended.
*/
responseFormat: z.array(z.union([
z.object({
type: z.literal("text"),
mimeType: z.string().nullish(),
schema: z.unknown().nullish()
}).loose(),
z.object({
type: z.literal("image"),
mimeType: z.string().nullish(),
aspectRatio: z.enum([
"1:1",
"2:3",
"3:2",
"3:4",
"4:3",
"4:5",
"5:4",
"9:16",
"16:9",
"21:9",
"1:8",
"8:1",
"1:4",
"4:1"
]).nullish(),
imageSize: z.enum([
"1K",
"2K",
"4K",
"512"
]).nullish()
}).loose(),
z.object({
type: z.literal("audio"),
mimeType: z.string().nullish()
}).loose(),
z.object({
type: z.literal("video"),
aspectRatio: z.enum(["16:9", "9:16"]).nullish(),
resolution: z.enum([
"360p",
"720p",
"1080p",
"4k"
]).nullish(),
duration: z.string().nullish(),
delivery: z.enum(["inline", "uri"]).nullish(),
gcsUri: z.string().nullish()
}).loose()
])).nullish(),
/**
* @deprecated Use `responseFormat` with a `{ type: 'image', ... }`
* entry instead. Retained for backwards compatibility; the SDK
* translates it into a matching `response_format` image entry and
* emits a warning when set.
*/
imageConfig: z.object({
aspectRatio: z.enum([
"1:1",
"2:3",
"3:2",
"3:4",
"4:3",
"4:5",
"5:4",
"9:16",
"16:9",
"21:9",
"1:8",
"8:1",
"1:4",
"4:1"
]).nullish(),
imageSize: z.enum([
"1K",
"2K",
"4K",
"512"
]).nullish()
}).nullish(),
mediaResolution: z.enum([
"low",
"medium",
"high",
"ultra_high"
]).nullish(),
responseModalities: z.array(z.enum([
"text",
"image",
"audio",
"video",
"document"
])).nullish(),
serviceTier: z.enum([
"flex",
"standard",
"priority"
]).nullish(),
/**
* Alternative to AI SDK `system` message. If both are set, the AI SDK
* `system` message wins and a warning is emitted.
*/
systemInstruction: z.string().nullish(),
/**
* Per-block signature for round-tripping `thought.signature` and
* `function_call.signature` blocks. Set by the SDK on output reasoning /
* tool-call parts; passed back unchanged on input parts so the API
* accepts the prior turn.
*/
signature: z.string().nullish(),
/**
* Set by the SDK on output assistant messages. The converter uses it to
* decide which messages to drop when compacting under
* `previousInteractionId`.
*/
interactionId: z.string().nullish(),
/**
* Maximum time, in milliseconds, to poll a background interaction (agent
* call) before giving up. Defaults to 30 minutes. Long-running agents
* such as deep research can take tens of minutes — increase if needed.
*/
pollingTimeoutMs: z.number().int().positive().nullish(),
/**
* Run the interaction in the background. Required for agents whose
* server-side workflow cannot complete within a single request/response.
* When `true`, the POST returns with a non-terminal status and the SDK
* polls `GET /interactions/{id}` until the work completes. Some agents
* reject `true`; see the agent's documentation for which mode it
* requires.
*/
background: z.boolean().nullish(),
/**
* Environment configuration for the agent sandbox. Only applies to agent
* calls (`google.interactions({ agent })`); ignored on model-id calls.
*
* - `"remote"`: provision a fresh sandbox for this call.
* - any other string: an existing `environment_id` to reuse.
* - object: provision a fresh sandbox and optionally preload `sources`
* and/or constrain outbound traffic via `network`.
*/
environment: z.union([z.string(), z.object({
type: z.literal("remote"),
sources: z.array(z.union([
z.object({
type: z.literal("gcs"),
source: z.string(),
target: z.string().nullish()
}),
z.object({
type: z.literal("repository"),
source: z.string(),
target: z.string().nullish()
}),
z.object({
type: z.literal("inline"),
content: z.string(),
target: z.string()
})
])).nullish(),
network: z.union([z.literal("disabled"), z.object({ allowlist: z.array(z.object({
domain: z.string(),
transform: z.array(z.record(z.string(), z.string())).nullish()
})) })]).nullish()
})]).nullish()
})));
//#endregion
//#region src/interactions/parse-google-interactions-outputs.ts
function googleProviderMetadata({ signature, interactionId, processingId, processingCallId }) {
const google = {};
if (signature != null) google.signature = signature;
if (interactionId != null) google.interactionId = interactionId;
if (processingId != null) google.processingId = processingId;
if (processingCallId != null) google.processingCallId = processingCallId;
return Object.keys(google).length > 0 ? { providerMetadata: { google } } : {};
}
const BUILTIN_TOOL_CALL_TYPES = /* @__PURE__ */ new Set([
"google_search_call",
"code_execution_call",
"url_context_call",
"file_search_call",
"google_maps_call",
"mcp_server_tool_call"
]);
const BUILTIN_TOOL_RESULT_TYPES = /* @__PURE__ */ new Set([
"google_search_result",
"code_execution_result",
"url_context_result",
"file_search_result",
"google_maps_result",
"mcp_server_tool_result"
]);
function builtinToolNameFromCallType(type) {
return type.replace(/_call$/, "");
}
function builtinToolNameFromResultType(type) {
return type.replace(/_result$/, "");
}
/**
* Walks the `steps[]` array of an Interactions response and emits AI SDK
* `LanguageModelV4Content[]`. Surfaces:
*
* - `model_output` steps: iterates `step.content[]` for `text` (with
* annotations → source parts) and `image` content blocks.
* - `thought` steps: emits a single `reasoning` part from `summary[*]`.
* - `function_call` steps: emits a `tool-call` part directly.
* - Built-in tool `*_call` / `*_result` steps (Google Search, Code Execution,
* URL Context, File Search, Google Maps, MCP Server): emits
* `tool-call`/`tool-result` parts with `providerExecuted: true`.
* - `user_input` steps are skipped (they echo the client's input).
*/
function parseGoogleInteractionsOutputs({ steps, generateId, interactionId }) {
const content = [];
let hasFunctionCall = false;
if (steps == null) return {
content,
hasFunctionCall
};
for (const step of steps) {
if (step == null || typeof step !== "object") continue;
const type = step.type;
if (typeof type !== "string") continue;
switch (type) {
case "user_input": break;
case "model_output": {
const blocks = step.content ?? [];
for (const block of blocks) {
if (block == null || typeof block !== "object") continue;
const blockType = block.type;
if (blockType === "text") {
const text = block.text ?? "";
const annotations = block.annotations;
content.push({
type: "text",
text,
...googleProviderMetadata({ interactionId })
});
const sources = annotationsToSources({
annotations,
generateId
});
for (const source of sources) content.push(source);
} else if (blockType === "image") {
const image = block;
if (image.data != null && image.data.length > 0) content.push({
type: "file",
mediaType: image.mime_type ?? "image/png",
data: {
type: "data",
data: image.data
},
...googleProviderMetadata({ interactionId })
});
else if (image.uri != null && image.uri.length > 0) content.push({
type: "file",
mediaType: image.mime_type ?? "image/png",
data: {
type: "url",
url: new URL(image.uri)
},
...googleProviderMetadata({ interactionId })
});
} else if (blockType === "video") {
const video = block;
if (video.data != null && video.data.length > 0) content.push({
type: "file",
mediaType: video.mime_type ?? "video/mp4",
data: {
type: "data",
data: video.data
},
...googleProviderMetadata({ interactionId })
});
else if (video.uri != null && video.uri.length > 0) content.push({
type: "file",
mediaType: video.mime_type ?? "video/mp4",
data: {
type: "url",
url: new URL(video.uri)
},
...googleProviderMetadata({ interactionId })
});
}
}
break;
}
case "thought": {
const thought = step;
const text = (Array.isArray(thought.summary) ? thought.summary : []).filter((item) => item?.type === "text" && typeof item.text === "string").map((item) => item.text).join("\n");
content.push({
type: "reasoning",
text,
...googleProviderMetadata({
signature: thought.signature,
interactionId
})
});
break;
}
case "processing_call": {
const call = step;
const processingId = call.id || generateId();
content.push({
type: "custom",
kind: "google.processing_call",
...googleProviderMetadata({
signature: call.signature,
interactionId,
processingId
})
});
break;
}
case "processing_result": {
const result = step;
const processingCallId = result.call_id || generateId();
content.push({
type: "custom",
kind: "google.processing_result",
...googleProviderMetadata({
signature: result.signature,
interactionId,
processingCallId
})
});
break;
}
case "function_call": {
hasFunctionCall = true;
const call = step;
content.push({
type: "tool-call",
toolCallId: call.id,
toolName: call.name,
input: JSON.stringify(call.arguments ?? {}),
...googleProviderMetadata({
signature: call.signature,
interactionId
})
});
break;
}
default: if (BUILTIN_TOOL_CALL_TYPES.has(type)) {
const call = step;
const toolName = type === "mcp_server_tool_call" ? call.name ?? "mcp_server_tool" : builtinToolNameFromCallType(type);
const input = JSON.stringify(call.arguments ?? {});
content.push({
type: "tool-call",
toolCallId: call.id || generateId(),
toolName,
input,
providerExecuted: true
});
} else if (BUILTIN_TOOL_RESULT_TYPES.has(type)) {
const result = step;
const toolName = type === "mcp_server_tool_result" ? result.name ?? "mcp_server_tool" : builtinToolNameFromResultType(type);
content.push({
type: "tool-result",
toolCallId: result.call_id || generateId(),
toolName,
result: result.result ?? null
});
const sources = builtinToolResultToSources({
block: step,
generateId
});
for (const source of sources) content.push(source);
}
}
}
return {
content,
hasFunctionCall
};
}
//#endregion
//#region src/interactions/cancel-google-interaction.ts
const getOriginalFetch = () => globalThis.fetch;
/**
* Best-effort `POST /interactions/{id}/cancel` to stop a background interaction
* on Google's side after the caller has aborted locally. Errors and non-2xx
* responses are swallowed so a cancel failure cannot mask the original abort.
*
* Skips the request entirely if `interactionId` is missing/empty -- e.g. when
* the interaction was created with `store: false` and the API did not return an
* id.
*/
async function cancelGoogleInteraction({ baseURL, interactionId, headers, fetch = getOriginalFetch() }) {
if (interactionId == null || interactionId.length === 0) return;
const url = `${baseURL}/interactions/${encodeURIComponent(interactionId)}/cancel`;
try {
const response = await fetch(url, {
method: "POST",
headers: withUserAgentSuffix(combineHeaders({ "Content-Type": "application/json" }, await resolve(headers)), getRuntimeEnvironmentUserAgent()),
body: "{}"
});
try {
await response.text();
} catch {}
} catch {}
}
//#endregion
//#region src/interactions/poll-google-interactions.ts
const TERMINAL_STATUSES = /* @__PURE__ */ new Set([
"completed",
"failed",
"cancelled",
"incomplete"
]);
function isTerminalStatus(status) {
return status != null && TERMINAL_STATUSES.has(status);
}
const DEFAULT_INITIAL_DELAY_MS = 1e3;
const DEFAULT_MAX_DELAY_MS = 1e4;
const DEFAULT_TIMEOUT_MS = 18e5;
/**
* 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.
*/
async function pollGoogleInteractionUntilTerminal({ baseURL, interactionId, headers, fetch, abortSignal, initialDelayMs = DEFAULT_INITIAL_DELAY_MS, maxDelayMs = DEFAULT_MAX_DELAY_MS, timeoutMs = DEFAULT_TIMEOUT_MS }) {
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)}`;
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,
validateUrl: false,
headers: await resolve(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;
}
}
//#endregion
//#region src/interactions/prepare-google-interactions-tools.ts
/**
* Maps AI SDK tool definitions and `toolChoice` onto the Gemini Interactions
* `tools[]` and `tool_choice` request fields.
*
* AI SDK function tools (`{ type: 'function', name, description, inputSchema }`)
* map to Interactions `{ type: 'function', name, description, parameters }`,
* with `parameters` passed through as plain JSON Schema (per
* `googleapis/js-genai` `samples/interactions_tool_call_with_functions.ts` and
* `src/interactions/resources/interactions.ts` `Function.parameters: unknown`).
*
* Provider-defined tools (`{ type: 'provider', id: 'google.<name>', args }`)
* map to the discriminated `Tool` union. The full set of
* provider-defined tool ids supported here:
*
* - `google.google_search` -> `{ type: 'google_search', search_types? }`
* - `google.code_execution` -> `{ type: 'code_execution' }`
* - `google.url_context` -> `{ type: 'url_context' }`
* - `google.file_search` -> `{ type: 'file_search', file_search_store_names?, top_k?, metadata_filter? }`
* - `google.google_maps` -> `{ type: 'google_maps', latitude?, longitude?, enable_widget? }`
* - `google.computer_use` -> `{ type: 'computer_use', environment?, excludedPredefinedFunctions? }`
* - `google.mcp_server` -> `{ type: 'mcp_server', name?, url?, headers?, allowed_tools? }`
* - `google.retrieval` -> `{ type: 'retrieval', retrieval_types?, vertex_ai_search_config? }`
*
* `toolChoice` shapes:
* - `'auto'` -> `'auto'`
* - `'required'` -> `'any'`
* - `'none'` -> omit all tools and `tool_choice`
* - `{ type: 'tool', toolName }` -> `{ allowed_tools: { mode: 'validated', tools: [name] } }`
* (Interactions `AllowedTools.tools` is an `Array<string>` of function
* names, not tool descriptors -- see `src/interactions/resources/interactions.ts`
* line ~151).
*/
function prepareGoogleInteractionsTools({ tools, toolChoice }) {
const toolWarnings = [];
if (toolChoice?.type === "none") return {
tools: void 0,
toolChoice: void 0,
toolWarnings
};
const normalized = tools?.length ? tools : void 0;
if (normalized == null) return {
tools: void 0,
toolChoice: void 0,
toolWarnings
};
const interactionsTools = [];
for (const tool of normalized) {
if (tool.type === "function") {
interactionsTools.push({
type: "function",
name: tool.name,
description: tool.description ?? "",
parameters: tool.inputSchema
});
continue;
}
if (tool.type === "provider") {
const args = tool.args ?? {};
switch (tool.id) {
case "google.google_search": {
const searchTypesArg = args.searchTypes;
let search_types;
if (searchTypesArg != null && typeof searchTypesArg === "object") {
const list = [];
if (searchTypesArg.webSearch != null) list.push("web_search");
if (searchTypesArg.imageSearch != null) list.push("image_search");
if (list.length > 0) search_types = list;
}
interactionsTools.push({
type: "google_search",
...search_types != null ? { search_types } : {}
});
break;
}
case "google.code_execution":
interactionsTools.push({ type: "code_execution" });
break;
case "google.url_context":
interactionsTools.push({ type: "url_context" });
break;
case "google.file_search":
interactionsTools.push({
type: "file_search",
...args.fileSearchStoreNames != null ? { file_search_store_names: args.fileSearchStoreNames } : {},
...args.topK != null ? { top_k: args.topK } : {},
...args.metadataFilter != null ? { metadata_filter: args.metadataFilter } : {}
});
break;
case "google.google_maps":
interactionsTools.push({
type: "google_maps",
...args.latitude != null ? { latitude: args.latitude } : {},
...args.longitude != null ? { longitude: args.longitude } : {},
...args.enableWidget != null ? { enable_widget: args.enableWidget } : {}
});
break;
case "google.computer_use":
interactionsTools.push({
type: "computer_use",
environment: args.environment ?? "browser",
...args.excludedPredefinedFunctions != null ? { excludedPredefinedFunctions: args.excludedPredefinedFunctions } : {}
});
break;
case "google.mcp_server":
interactionsTools.push({
type: "mcp_server",
...args.name != null ? { name: args.name } : {},
...args.url != null ? { url: args.url } : {},
...args.headers != null ? { headers: args.headers } : {},
...args.allowedTools != null ? { allowed_tools: args.allowedTools } : {}
});
break;
case "google.retrieval": {
const vertexAiSearchConfig = args.vertexAiSearchConfig ?? void 0;
interactionsTools.push({
type: "retrieval",
...args.retrievalTypes != null ? { retrieval_types: args.retrievalTypes } : { retrieval_types: ["vertex_ai_search"] },
...vertexAiSearchConfig != null ? { vertex_ai_search_config: vertexAiSearchConfig } : {}
});
break;
}
default: toolWarnings.push({
type: "unsupported",
feature: `provider-defined tool ${tool.id}`,
details: `provider-defined tool ${tool.id} is not supported by google.interactions; tool dropped.`
});
}
continue;
}
toolWarnings.push({
type: "unsupported",
feature: `tool of type ${tool.type}`,
details: "Only function tools and google.* provider-defined tools are supported by google.interactions; tool dropped."
});
}
const hasFunctionTool = interactionsTools.some((t) => t.type === "function");
let mappedToolChoice;
if (toolChoice != null && hasFunctionTool) switch (toolChoice.type) {
case "auto":
mappedToolChoice = "auto";
break;
case "required":
mappedToolChoice = "any";
break;
case "tool": mappedToolChoice = { allowed_tools: {
mode: "validated",
tools: [toolChoice.toolName]
} };
}
return {
tools: interactionsTools.length > 0 ? interactionsTools : void 0,
toolChoice: mappedToolChoice,
toolWarnings
};
}
//#endregion
//#region src/interactions/stream-google-interactions.ts
const DEFAULT_MAX_RETRIES = 3;
const DEFAULT_RETRY_DELAY_MS = 500;
/**
* Connects to `GET {baseURL}/interactions/{id}?stream=true` and surfaces the
* server-sent events as a `ReadableStream<ParseResult<GoogleInteractionsEvent>>`
* so the existing `buildGoogleInteractionsStreamTransform` can consume them
* unchanged.
*
* The connection can drop mid-run: long-running agents idle for long
* stretches between SSE events and undici's default body timeout terminates
* the request with `UND_ERR_BODY_TIMEOUT`. We track the last seen `event_id`
* and reconnect with `?last_event_id=<id>` on any unexpected end. After
* `maxRetries` consecutive failures the stream errors out so the caller can
* decide whether to fall back to polling.
*
* The stream completes cleanly when an `interaction.complete` event with a
* terminal status arrives, or when an `error` event arrives.
*/
function streamGoogleInteractionEvents({ baseURL, interactionId, headers, fetch, abortSignal, maxRetries = DEFAULT_MAX_RETRIES, retryDelayMs = DEFAULT_RETRY_DELAY_MS }) {
if (interactionId.length === 0) throw new Error("google.interactions: cannot stream a background interaction without an id.");
let lastEventId;
let complete = false;
let attempt = 0;
let receivedAnyEventThisAttempt = false;
let currentReader;
const internalAbort = new AbortController();
const upstreamAbortHandler = () => internalAbort.abort();
if (abortSignal != null) {
if (abortSignal.aborted) internalAbort.abort();
else abortSignal.addEventListener("abort", upstreamAbortHandler, { once: true });
}
const effectiveSignal = internalAbort.signal;
function buildUrl() {
const base = `${baseURL}/interactions/${encodeURIComponent(interactionId)}`;
const params = new URLSearchParams({ stream: "true" });
if (lastEventId != null) params.set("last_event_id", lastEventId);
return `${base}?${params.toString()}`;
}
async function openReader() {
const { value: stream } = await getFromApi({
url: buildUrl(),
validateUrl: false,
headers: {
...await resolve(headers),
accept: "text/event-stream"
},
failedResponseHandler: googleFailedResponseHandler,
successfulResponseHandler: createEventSourceResponseHandler(googleInteractionsEventSchema),
abortSignal: effectiveSignal,
fetch
});
return stream.getReader();
}
return new ReadableStream({
async start(controller) {
try {
while (!complete && !effectiveSignal.aborted) {
if (currentReader == null) try {
currentReader = await openReader();
receivedAnyEventThisAttempt = false;
} catch (error) {
if (isAbortError(error) || effectiveSignal.aborted) {
controller.error(error);
return;
}
attempt++;
if (attempt >= maxRetries) {
controller.error(error);
return;
}
await delay(retryDelayMs * attempt, { abortSignal: effectiveSignal });
continue;
}
try {
const { done, value } = await currentReader.read();
if (done) {
currentReader = void 0;
if (complete) break;
if (!receivedAnyEventThisAttempt) {
attempt++;
if (attempt >= maxRetries) {
controller.error(/* @__PURE__ */ new Error("google.interactions: SSE stream closed without producing any events."));
return;
}
await delay(retryDelayMs * attempt, { abortSignal: effectiveSignal });
} else attempt = 0;
continue;
}
receivedAnyEventThisAttempt = true;
if (value.success) {
const streamEvent = value.value;
if (typeof streamEvent.event_id === "string" && streamEvent.event_id.length > 0) lastEventId = streamEvent.event_id;
if (streamEvent.event_type === "interaction.completed" || streamEvent.event_type === "error") complete = true;
}
controller.enqueue(value);
} catch (error) {
if (isAbortError(error) || effectiveSignal.aborted) {
controller.error(error);
return;
}
currentReader = void 0;
attempt++;
if (attempt >= maxRetries) {
controller.error(error);
return;
}
await delay(retryDelayMs * attempt, { abortSignal: effectiveSignal });
}
}
controller.close();
} catch (error) {
controller.error(error);
} finally {
if (abortSignal != null) abortSignal.removeEventListener("abort", upstreamAbortHandler);
currentReader?.cancel().catch(() => {});
currentReader = void 0;
if (effectiveSignal.aborted && !complete) await cancelGoogleInteraction({
baseURL,
interactionId,
headers,
fetch
});
}
},
cancel() {
internalAbort.abort();
currentReader?.cancel().catch(() => {});
currentReader = void 0;
}
});
}
//#endregion
//#region src/interactions/synthesize-google-interactions-agent-stream.ts
/**
* Synthesizes a `LanguageModelV4StreamPart` stream from a fully-resolved
* Interaction response (i.e. the `response` returned after polling a
* `background: true` agent call to a terminal status).
*
* Agent calls cannot use SSE (`stream: true` is incompatible with
* `background: true`), so we deterministically replay the polled outputs as a
* stream sequence in the same order/shape `buildGoogleInteractionsStreamTransform`
* would produce. Each text/reasoning block is emitted as a single delta — the
* server has already produced the whole block by the time we synthesize.
*/
function synthesizeGoogleInteractionsAgentStream({ response, warnings, generateId, includeRawChunks, headerServiceTier }) {
return new ReadableStream({ start(controller) {
controller.enqueue({
type: "stream-start",
warnings
});
const interactionId = typeof response.id === "string" && response.id.length > 0 ? response.id : void 0;
let timestamp;
const created = response.created;
if (typeof created === "string") {
const parsed = new Date(created);
if (!Number.isNaN(parsed.getTime())) timestamp = parsed;
}
controller.enqueue({
type: "response-metadata",
...interactionId != null ? { id: interactionId } : {},
modelId: response.model ?? void 0,
...timestamp ? { timestamp } : {}
});
if (includeRawChunks) controller.enqueue({
type: "raw",
rawValue: response
});
const { content, hasFunctionCall } = parseGoogleInteractionsOutputs({
steps: response.steps ?? null,
generateId,
interactionId
});
let blockCounter = 0;
const nextBlockId = () => `${interactionId ?? "agent"}:${blockCounter++}`;
for (const part of content) switch (part.type) {
case "text": {
const id = nextBlockId();
const providerMetadata = part.providerMetadata;
controller.enqueue({
type: "text-start",
id
});
if (part.text.length > 0) controller.enqueue({
type: "text-delta",
id,
delta: part.text
});
controller.enqueue({
type: "text-end",
id,
...providerMetadata ? { providerMetadata } : {}
});
break;
}
case "reasoning": {
const id = nextBlockId();
const providerMetadata = part.providerMetadata;
controller.enqueue({
type: "reasoning-start",
id
});
if (part.text.length > 0) controller.enqueue({
type: "reasoning-delta",
id,
delta: part.text
});
controller.enqueue({
type: "reasoning-end",
id,
...providerMetadata ? { providerMetadata } : {}
});
break;
}
case "tool-call": {
const providerMetadata = part.providerMetadata;
controller.enqueue({
type: "tool-input-start",
id: part.toolCallId,
toolName: part.toolName,
...part.providerExecuted ? { providerExecuted: part.providerExecuted } : {}
});
controller.enqueue({
type: "tool-input-delta",
id: part.toolCallId,
delta: part.input
});
controller.enqueue({
type: "tool-input-end",
id: part.toolCallId
});
controller.enqueue({
type: "tool-call",
toolCallId: part.toolCallId,
toolName: part.toolName,
input: part.input,
...part.providerExecuted ? { providerExecuted: part.providerExecuted } : {},
...providerMetadata ? { providerMetadata } : {}
});
break;
}
case "tool-result":
controller.enqueue({
type: "tool-result",
toolCallId: part.toolCallId,
toolName: part.toolName,
result: part.result
});
break;
case "source":
case "file": controller.enqueue(part);
}
const serviceTier = response.service_tier ?? headerServiceTier;
const finishReason = {
unified: mapGoogleInteractionsFinishReason({
status: response.status,
hasFunctionCall
}),
raw: response.status
};
const providerMetadata = { google: {
...interactionId != null ? { interactionId } : {},
...serviceTier != null ? { serviceTier } : {}
} };
controller.enqueue({
type: "finish",
finishReason,
usage: convertGoogleInteractionsUsage(response.usage),
providerMetadata
});
controller.close();
} });
}
//#endregion
//#region src/interactions/google-interactions-language-model.ts
var GoogleInteractionsLanguageModel = class GoogleInteractionsLanguageModel {
static [WORKFLOW_SERIALIZE](model) {
return {
...serializeModelOptions({
modelId: model.modelId,
config: model.config
}),
agent: model.agent
};
}
static [WORKFLOW_DESERIALIZE](options) {
return new GoogleInteractionsLanguageModel(options.agent != null ? { agent: options.agent } : options.modelId, options.config);
}
constructor(modelOrAgent, config) {
this.specificationVersion = "v4";
if (typeof modelOrAgent === "string") {
this.modelId = modelOrAgent;
this.agent = void 0;
} else if ("managedAgent" in modelOrAgent) {
this.modelId = modelOrAgent.managedAgent;
this.agent = modelOrAgent.managedAgent;
} else {
this.modelId = modelOrAgent.agent;
this.agent = modelOrAgent.agent;
}
this.config = config;
}
get provider() {
return this.config.provider;
}
get supportedUrls() {
if (this.config.supportedUrls) return this.config.supportedUrls();
return {
"image/*": [/^https?:\/\/.+/],
"application/pdf": [/^https?:\/\/.+/],
"audio/*": [/^https?:\/\/.+/],
"video/*": [
/^https?:\/\/(www\.)?youtube\.com\/watch\?v=.+/,
/^https?:\/\/youtu\.be\/.+/,
/^gs:\/\/.+/
]
};
}
async getArgs(options) {
const warnings = [];
const googleOptions = await parseProviderOptions({
provider: "google",
providerOptions: options.providerOptions,
schema: googleInteractionsLanguageModelOptions
});
const isAgent = this.agent != null;
if (!isAgent) {
if (options.frequencyPenalty != null) warnings.push({
type: "unsupported",
feature: "frequencyPenalty"
});
if (options.presencePenalty != null) warnings.push({
type: "unsupported",
feature: "presencePenalty"
});
}
const hasTools = options.tools != null && options.tools.length > 0;
let toolsForBody;
let toolChoiceForBody;
if (hasTools) {
const prepared = prepareGoogleInteractionsTools({
tools: options.tools,
toolChoice: options.toolChoice
});
toolsForBody = prepared.tools;
toolChoiceForBody = prepared.toolChoice;
warnings.push(...prepared.toolWarnings);
}
const responseFormatEntries = [];
if (options.responseFormat?.type === "json") {
if (isAgent) warnings.push({
type: "other",
message: "google.interactions: structured output (responseFormat) is not supported when an agent is set; responseFormat will be ignored."
});
else {
const entry = {
type: "text",
mime_type: "application/json",
...options.responseFormat.schema != null ? { schema: options.responseFormat.schema } : {}
};
responseFormatEntries.push(entry);
}
}
if (googleOptions?.responseFormat != null) {
for (const entry of googleOptions.responseFormat) if (entry.type === "text") responseFormatEntries.push(pruneUndefined({
type: "text",
mime_type: entry.mimeType ?? void 0,
schema: entry.schema ?? void 0
}));
else if (entry.type === "image") responseFormatEntries.push(pruneUndefined({
type: "image",
mime_type: entry.mimeType ?? void 0,
aspect_ratio: entry.aspectRatio ?? void 0,
image_size: entry.imageSize ?? void 0
}));
else if (entry.type === "audio") responseFormatEntries.push(pruneUndefined({
type: "audio",
mime_type: entry.mimeType ?? void 0
}));
else if (entry.type === "video") responseFormatEntries.push(pruneUndefined({
type: "video",
aspect_ratio: entry.aspectRatio ?? void 0,
resolution: entry.resolution ?? void 0,
duration: entry.duration ?? void 0,
delivery: entry.delivery ?? void 0,
gcs_uri: entry.gcsUri ?? void 0
}));
}
const { input, systemInstruction: convertedSystemInstruction, warnings: convWarnings } = convertToGoogleInteractionsInput({
prompt: options.prompt,
previousInteractionId: googleOptions?.previousInteractionId ?? void 0,
store: googleOptions?.store ?? void 0,
mediaResolution: googleOptions?.mediaResolution ?? void 0
});
warnings.push(...convWarnings);
let systemInstruction = convertedSystemInstruction;
const optionSystemInstruction = googleOptions?.systemInstruction ?? void 0;
if (systemInstruction != null && optionSystemInstruction != null) warnings.push({
type: "other",
message: "google.interactions: both AI SDK system message and providerOptions.google.systemInstruction were set; using the AI SDK system message."
});
else if (systemInstruction == null && optionSystemInstruction != null) systemInstruction = optionSystemInstruction;
let generationConfig;
if (isAgent) {
const droppedFields = [];
if (options.temperature != null) droppedFields.push("temperature");
if (options.topP != null) droppedFields.push("topP");
if (options.topK != null) droppedFields.push("topK");
if (options.frequencyPenalty != null) droppedFields.push("frequencyPenalty");
if (options.presencePenalty != null) droppedFields.push("presencePenalty");
if (options.seed != null) droppedFields.push("seed");
if (options.stopSequences != null && options.stopSequences.length > 0) droppedFields.push("stopSequences");
if (options.maxOutputTokens != null) droppedFields.push("maxOutputTokens");
if (isCustomReasoning(options.reasoning)) droppedFields.push("reasoning");
if (googleOptions?.thinkingLevel != null) droppedFields.push("thinkingLevel");
if (googleOptions?.thinkingSummaries != null) droppedFields.push("thinkingSummaries");
if (googleOptions?.imageConfig != null) droppedFields.push("imageConfig");
if (droppedFields.length > 0) warnings.push({
type: "other",
message: `google.interactions: ${droppedFields.join(", ")} ${droppedFields.length === 1 ? "is" : "are"} not supported when an agent is set; use providerOptions.google.agentConfig instead. Dropped from the request body.`
});
generationConfig = void 0;
} else {
generationConfig = pruneUndefined({
temperature: options.temperature ?? void 0,
top_p: options.topP ?? void 0,
top_k: options.topK ?? void 0,
seed: options.seed ?? void 0,
stop_sequences: options.stopSequences != null && options.stopSequences.length > 0 ? options.stopSequences : void 0,
max_output_tokens: options.maxOutputTokens ?? void 0,
thinking_level: mapReasoningToProviderEffort({
reasoning: options.reasoning,
providerOptionsEffort: googleOptions?.thinkingLevel,
effortMap: Object.fromEntries((getGoogleLanguageModelEntry(this.modelId).capabilities.language.reasoning.efforts ?? []).map((level) => [level, level])),
warnings
}),
thinking_summaries: googleOptions?.thinkingSummaries ?? void 0,
tool_choice: toolChoiceForBody
});
if (googleOptions?.imageConfig != null) {
const alreadyHasImageEntry = responseFormatEntries.some((entry) => entry.type === "image");
warnings.push({
type: "other",
message: alreadyHasImageEntry ? "google.interactions: providerOptions.google.imageConfig is deprecated and was ignored because providerOptions.google.responseFormat already supplies an image entry. Use responseFormat exclusively." : "google.interactions: providerOptions.google.imageConfig is deprecated. Use providerOptions.google.responseFormat with a { type: \"image\", ... } entry instead."
});
if (!alreadyHasImageEntry) responseFormatEntries.push({
type: "image",
mime_type: "image/png",
...googleOptions.imageConfig.aspectRatio != null ? { aspect_ratio: googleOptions.imageConfig.aspectRatio } : {},
...googleOptions.imageConfig.imageSize != null ? { image_size: googleOptions.imageConfig.imageSize } : {}
});
}
}
let agentConfig;
if (isAgent && googleOptions?.agentConfig != null) {
const agentConfigOptions = googleOptions.agentConfig;
if (agentConfigOptions.type === "deep-research") agentConfig = pruneUndefined({
type: "deep-research",
thinking_summaries: agentConfigOptions.thinkingSummaries ?? void 0,
visualization: agentConfigOptions.visualization ?? void 0,
collaborative_planning: agentConfigOptions.collaborativePlanning ?? void 0
});
else if (agentConfigOptions.type === "dynamic") agentConfig = { type: "dynamic" };
}
let environment;
if (googleOptions?.environment != null) {
if (!isAgent) warnings.push({
type: "other",
message: "google.interactions: environment is only supported when an agent is set; environment will be omitted from the request body."
});
else if (typeof googleOptions.environment === "string") environment = googleOptions.environment;
else {
const environmentOptions = googleOptions.environment;
const sources = environmentOptions.sources?.map((source) => {
if (source.type === "inline") return {
type: "inline",
content: source.content,
target: source.target
};
return pruneUndefined({
type: source.type,
source: source.source,
target: source.target ?? void 0
});
});
let network;
if (environmentOptions.network === "disabled") network = "disabled";
else if (environmentOptions.network != null) network = { allowlist: environmentOptions.network.allowlist.map((entry) => pruneUndefined({
domain: entry.domain,
transform: entry.transform ?? void 0
})) };
environment = pruneUndefined({
type: "remote",
sources: sources != null && sources.length > 0 ? sources : void 0,
network
});
}
}
return {
args: pruneUndefined({
...isAgent ? { agent: this.agent } : { model: this.modelId },
input,
labels: googleOptions?.labels ?? void 0,
system_instruction: systemInstruction,
tools: toolsForBody,
response_format: responseFormatEntries.length > 0 ? responseFormatEntries : void 0,
response_modalities: googleOptions?.responseModalities != null ? googleOptions.responseModalities : void 0,
previous_interaction_id: googleOptions?.previousInteractionId ?? void 0,
service_tier: googleOptions?.serviceTier ?? void 0,
store: googleOptions?.store ?? void 0,
generation_config: generationConfig != null && Object.keys(generationConfig).length > 0 ? generationConfig : void 0,
agent_config: agentConfig,
environment,
background: googleOptions?.background ?? void 0
}),
warnings,
isAgent,
isBackground: googleOptions?.background === true,
pollingTimeoutMs: googleOptions?.pollingTimeoutMs ?? void 0
};
}
async doGenerate(options) {
const { args, warnings, isAgent, isBackground, pollingTimeoutMs } = await this.getArgs(options);
const url = `${this.config.baseURL}/interactions`;
const mergedHeaders = async () => combineHeaders(await resolve(this.config.headers), options.headers);
let { responseHeaders, value: response, rawValue: rawResponse } = await postJsonToApi({
url,
headers: await resolve(mergedHeaders),
body: args,
failedResponseHandler: googleFailedResponseHandler,
successfulResponseHandler: createJsonResponseHandler(googleInteractionsResponseSchema),
abortSignal: options.abortSignal,
fetch: this.config.fetch
});
if ((isAgent || isBackground) && !isTerminalStatus(response.status)) {
const polled = await pollGoogleInteractionUntilTerminal({
baseURL: this.config.baseURL,
interactionId: response.id,
headers: mergedHeaders,
fetch: this.config.fetch,
abortSignal: options.abortSignal,
timeoutMs: pollingTimeoutMs
});
response = polled.response;
rawResponse = polled.rawResponse;
responseHeaders = polled.responseHeaders ?? responseHeaders;
}
const interactionId = typeof response.id === "string" && response.id.length > 0 ? response.id : void 0;
const { content, hasFunctionCall } = parseGoogleInteractionsOutputs({
steps: response.steps ?? null,
generateId: this.config.generateId ?? generateId,
interactionId
});
const finishReason = {
unified: mapGoogleInteractionsFinishReason({
status: response.status,
hasFunctionCall
}),
raw: response.status
};
const serviceTier = response.service_tier ?? responseHeaders?.["x-gemini-service-tier"] ?? void 0;
const outputTokensByModality = getGoogleInteractionsOutputTokensByModality(response.usage);
const providerMetadata = { google: {
...interactionId != null ? { interactionId } : {},
...serviceTier != null ? { serviceTier } : {},
...outputTokensByModality != null ? { outputTokensByModality } : {}
} };
let timestamp;
if (typeof response.created === "string") {
const parsed = new Date(response.created);
if (!Number.isNaN(parsed.getTime())) timestamp = parsed;
}
return {
content,
finishReason,
usage: convertGoogleInteractionsUsage(response.usage),
warnings,
providerMetadata,
request: { body: args },
response: {
headers: responseHeaders,
body: rawResponse,
...interactionId != null ? { id: interactionId } : {},
...timestamp ? { timestamp } : {},
modelId: response.model ?? void 0
}
};
}
async doStream(options) {
const { args, warnings, isBackground, pollingTimeoutMs } = await this.getArgs(options);
const url = `${this.config.baseURL}/interactions`;
const mergedHeaders = async () => combineHeaders(await resolve(this.config.headers), options.headers);
if (isBackground) return this.doStreamBackground({
args,
warnings,
url,
mergedHeaders,
options,
pollingTimeoutMs
});
const body = {
...args,
stream: true
};
const { responseHeaders, value: response } = await postJsonToApi({
url,
headers: await resolve(mergedHeaders),
body,
failedResponseHandler: googleFailedResponseHandler,
successfulResponseHandler: createEventSourceResponseHandler(googleInteractionsEventSchema),
abortSignal: options.abortSignal,
fetch: this.config.fetch
});
const headerServiceTier = responseHeaders?.["x-gemini-service-tier"];
const transform = buildGoogleInteractionsStreamTransform({
warnings,
generateId: this.config.generateId ?? generateId,
includeRawChunks: options.includeRawChunks,
serviceTier: headerServiceTier
});
return {
stream: response.pipeThrough(transform),
request: { body },
response: { headers: responseHeaders }
};
}
async doStreamBackground({ args, warnings, url, mergedHeaders, options, pollingTimeoutMs }) {
const { responseHeaders: postHeaders, value: postResponse } = await postJsonToApi({
url,
headers: await resolve(mergedHeaders),
body: args,
failedResponseHandler: googleFailedResponseHandler,
successfulResponseHandler: createJsonResponseHandler(googleInteractionsResponseSchema),
abortSignal: options.abortSignal,
fetch: this.config.fetch
});
const interactionId = postResponse.id;
if (interactionId == null || interactionId.length === 0) throw new Error("google.interactions: background POST response did not include an interaction id; cannot stream the result.");
const headerServiceTier = postHeaders?.["x-gemini-service-tier"];
if (isTerminalStatus(postResponse.status)) return {
stream: synthesizeGoogleInteractionsAgentStream({
response: postResponse,
warnings,
generateId: this.config.generateId ?? generateId,
includeRawChunks: options.includeRawChunks,
headerServiceTier
}),
request: { body: args },
response: { headers: postHeaders }
};
const events = streamGoogleInteractionEvents({
baseURL: this.config.baseURL,
interactionId,
headers: mergedHeaders,
fetch: this.config.fetch,
abortSignal: options.abortSignal
});
const transform = buildGoogleInteractionsStreamTransform({
warnings,
generateId: this.config.generateId ?? generateId,
includeRawChunks: options.includeRawChunks,
serviceTier: headerServiceTier
});
return {
stream: events.pipeThrough(transform),
request: { body: args },
response: { headers: postHeaders }
};
}
};
function pruneUndefined(obj) {
const result = {};
for (const [key, value] of Object.entries(obj)) {
if (value === void 0) continue;
result[key] = value;
}
return result;
}
//#endregion
//#region src/get-realtime-base-url.ts
/**
* Returns the base URL for Live API (WebSocket) endpoints, stripping a
* trailing `/v1beta` or `/v1alpha` API version segment: Live API paths carry
* their own version and are not nested under it.
*/
function getRealtimeBaseURL(baseURL) {
const url = new URL(baseURL);
const pathSegments = url.pathname.split("/");
const version = pathSegments.at(-1);
if (version === "v1beta" || version === "v1alpha") {
pathSegments.pop();
url.pathname = pathSegments.join("/") || "/";
}
return url;
}
/**
* Builds a `wss://` Live API WebSocket URL for the given bidi service path
* (e.g. `google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent`).
*/
function getRealtimeWebSocketURL(baseURL, webSocketPath) {
const url = getRealtimeBaseURL(baseURL);
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
url.pathname = `${url.pathname.replace(/\/$/, "")}/ws/${webSocketPath}`;
return url;
}
//#endregion
//#region src/realtime/google-realtime-event-mapper.ts
/**
* Stateful event mapper for Google's Gemini Live API.
*
* Unlike OpenAI/xAI, Google's events don't have response/item IDs and
* a single message can contain multiple pieces of data. This class
* tracks turn state to generate consistent synthetic IDs.
*/
var GoogleRealtimeEventMapper = class {
constructor() {
this.turnCounter = 0;
this.inputTranscriptionCounter = 0;
this.inputTranscriptionBuffer = "";
this.inputTranscriptionBoundary = "none";
this.preserveInputTranscriptionAcrossTurnComplete = false;
this.hasAudio = false;
this.hasText = false;
this.hasTranscript = false;
this.turnClosed = false;
this.inputAudioRate = 16e3;
}
get responseId() {
return `google-resp-${this.turnCounter}`;
}
get itemId() {
return `google-item-${this.turnCounter}`;
}
/**
* Rolls over to the next turn lazily, once model content for the next turn
* arrives. `turnComplete` merely marks the current turn closed.
* This keeps an output transcript that arrives shortly after `turnComplete`
* attached to the turn it belongs to, since Google delivers transcription
* independently with no guaranteed ordering relative to `turnComplete`.
*/
beginTurnIfClosed() {
if (!this.turnClosed) return;
this.turnCounter++;
this.hasAudio = false;
this.hasText = false;
this.hasTranscript = false;
this.turnClosed = false;
this.preserveInputTranscriptionAcrossTurnComplete = false;
}
accumulateInputTranscription({ text, finished }) {
if (!text) {
if (finished === true) this.finishInputTranscription();
return;
}
const hasFinishedSignal = finished != null;
if (this.inputTranscriptionBuffer !== "" && (this.inputTranscriptionBoundary === "interrupted" || this.inputTranscriptionBoundary === "turn-complete" && !hasFinishedSignal)) this.finishInputTranscription();
this.inputTranscriptionBoundary = "none";
this.inputTranscriptionBuffer += text;
const result = {
itemId: `google-input-${this.inputTranscriptionCounter}`,
transcript: this.inputTranscriptionBuffer
};
if (finished === true) this.finishInputTranscription();
return result;
}
finishInputTranscription() {
if (this.inputTranscriptionBuffer === "") return;
this.inputTranscriptionCounter++;
this.inputTranscriptionBuffer = "";
}
parseServerEvent(raw) {
const events = this.parseEvent(raw);
const usage = mapGoogleUsage(raw.usageMetadata);
if (usage == null) return events;
const list = Array.isArray(events) ? events : [events];
if (list.some((event) => event.type === "response-done")) {
const mapped = list.map((event) => event.type === "response-done" ? {
...event,
usage
} : event);
return Array.isArray(events) ? mapped : mapped[0];
}
const update = {
type: "custom",
rawType: "usageMetadata",
usage,
raw
};
if (list.length === 1 && list[0].type === "custom" && list[0].rawType === "usageMetadata") return update;
return [...list, update];
}
parseEvent(raw) {
const data = raw;
if (data.setupComplete != null) return {
type: "session-created",
raw
};
if (data.toolCall != null) {
this.beginTurnIfClosed();
return (data.toolCall.functionCalls ?? []).flatMap((functionCall) => {
const args = JSON.stringify(functionCall.args ?? {});
return [{
type: "function-call-arguments-delta",
responseId: this.responseId,
itemId: this.itemId,
callId: functionCall.id,
delta: args,
raw
}, {
type: "function-call-arguments-done",
responseId: this.responseId,
itemId: this.itemId,
callId: functionCall.id,
name: functionCall.name,
arguments: args,
raw
}];
});
}
if (data.toolCallCancellation != null) return {
type: "custom",
rawType: "toolCallCancellation",
raw
};
if (data.goAway != null) return {
type: "custom",
rawType: "goAway",
raw
};
if (data.sessionResumptionUpdate != null) return {
type: "custom",
rawType: "sessionResumptionUpdate",
raw
};
if (data.serverContent != null) return this.parseServerContent(data.serverContent, raw);
if (data.inputTranscription != null) {
const transcription = this.accumulateInputTranscription(data.inputTranscription);
if (transcription != null) return {
type: "input-transcription-completed",
...transcription,
raw
};
}
return {
type: "custom",
rawType: String(Object.keys(data)[0]),
raw
};
}
parseServerContent(serverContent, raw) {
const events = [];
if (serverContent.interrupted) {
this.inputTranscriptionBoundary = "interrupted";
this.preserveInputTranscriptionAcrossTurnComplete = true;
this.turnClosed = true;
events.push({
type: "speech-started",
raw
});
}
if (serverContent.modelTurn?.parts) {
this.beginTurnIfClosed();
for (const part of serverContent.modelTurn.parts) {
if (part.inlineData?.data) {
this.hasAudio = true;
events.push({
type: "audio-delta",
responseId: this.responseId,
itemId: this.itemId,
delta: part.inlineData.data,
raw
});
}
if (part.text) {
this.hasText = true;
events.push({
type: "text-delta",
responseId: this.responseId,
itemId: this.itemId,
delta: part.text,
raw
});
}
}
}
if (serverContent.outputTranscription?.text) {
this.hasTranscript = true;
events.push({
type: "audio-transcript-delta",
responseId: this.responseId,
itemId: this.itemId,
delta: serverContent.outputTranscription.text,
raw
});
}
if (serverContent.inputTranscription != null) {
const transcription = this.accumulateInputTranscription(serverContent.inputTranscription);
if (transcription != null) events.push({
type: "input-transcription-completed",
...transcription,
raw
});
}
if (serverContent.generationComplete) events.push({
type: "custom",
rawType: "generationComplete",
raw
});
if (serverContent.interactionStatus != null) events.push({
type: "custom",
rawType: "interactionStatus",
raw
});
if (serverContent.waitingForInput) events.push({
type: "custom",
rawType: "waitingForInput",
raw
});
if (serverContent.turnComplete) {
if (this.hasAudio) events.push({
type: "audio-done",
responseId: this.responseId,
itemId: this.itemId,
raw
});
if (this.hasText) events.push({
type: "text-done",
responseId: this.responseId,
itemId: this.itemId,
raw
});
if (this.hasTranscript) events.push({
type: "audio-transcript-done",
responseId: this.responseId,
itemId: this.itemId,
raw
});
events.push({
type: "response-done",
responseId: this.responseId,
status: "completed",
raw
});
this.turnClosed = true;
if (this.preserveInputTranscriptionAcrossTurnComplete) this.preserveInputTranscriptionAcrossTurnComplete = false;
else if (this.inputTranscriptionBoundary !== "interrupted") this.inputTranscriptionBoundary = "turn-complete";
}
if (events.length === 0) return {
type: "custom",
rawType: "serverContent",
raw
};
return events.length === 1 ? events[0] : events;
}
serializeClientEvent(event, modelId) {
switch (event.type) {
case "session-update":
if (event.config.inputAudioFormat?.rate != null) this.inputAudioRate = event.config.inputAudioFormat.rate;
return { setup: buildGoogleSessionConfig(event.config, modelId) };
case "input-audio-append": return { realtimeInput: { audio: {
data: event.audio,
mimeType: `audio/pcm;rate=${this.inputAudioRate}`
} } };
case "input-audio-commit": return { realtimeInput: { audioStreamEnd: true } };
case "input-audio-clear":
case "response-create":
case "response-cancel":
case "conversation-item-truncate": return null;
case "conversation-item-create": {
const item = event.item;
switch (item.type) {
case "text-message": return { realtimeInput: { text: item.text } };
case "function-call-output": return serializeFunctionCallOutput(item);
case "audio-message": return null;
}
break;
}
}
return null;
}
};
/**
* Maps Gemini Live `usageMetadata` to normalized usage. Google reports usage
* per modality in `promptTokensDetails` / `responseTokensDetails`.
*
* Field names are Live-API-specific: the Live `UsageMetadata` uses
* `responseTokenCount` / `responseTokensDetails` for output, NOT the
* `candidatesTokenCount` / `candidatesTokensDetails` of the regular
* `generateContent` response (see google-language-model.ts). Do not "align"
* these to `candidates*` — they are different wire shapes.
*
* Gross totals preserve provider counts even when modality details are absent
* or contain modalities without a dedicated normalized bucket. Never guess a
* text/audio split from those totals.
*/
function mapGoogleUsage(usageMetadata) {
if (usageMetadata == null) return void 0;
const input = sumByModality(usageMetadata.promptTokensDetails);
const output = sumByModality(usageMetadata.responseTokensDetails);
return compactUsage({
inputTokens: usageMetadata.promptTokenCount != null ? usageMetadata.promptTokenCount + (usageMetadata.toolUsePromptTokenCount ?? 0) : void 0,
outputTokens: usageMetadata.responseTokenCount != null ? usageMetadata.responseTokenCount + (usageMetadata.thoughtsTokenCount ?? 0) : void 0,
cachedInputTokens: usageMetadata.cachedContentTokenCount,
inputTextTokens: input.text,
inputAudioTokens: input.audio,
outputTextTokens: output.text,
outputAudioTokens: output.audio
});
}
/** Aggregate Gemini per-modality token details into text/audio buckets. */
function sumByModality(details) {
if (details == null) return {};
const totals = {};
for (const detail of details) {
const count = detail.tokenCount;
if (count == null) continue;
const modality = detail.modality?.toUpperCase();
if (modality === "AUDIO") totals.audio = (totals.audio ?? 0) + count;
else if (modality === "TEXT") totals.text = (totals.text ?? 0) + count;
}
return totals;
}
/** Drop `undefined` fields; return `undefined` when nothing is observable. */
function compactUsage(usage) {
const result = {};
for (const [key, value] of Object.entries(usage)) if (value != null) result[key] = value;
return Object.keys(result).length > 0 ? result : void 0;
}
/**
* The value Gemini accepts for `functionResponse.response`.
*
* The field is typed `google.protobuf.Struct`, which is an object and nothing else, so
* a string, number, array or `null` on the wire is a protocol violation and the socket
* closes with 1007. `onToolCall` returns `unknown` and `addToolOutput` takes `unknown`,
* so those shapes reach here as perfectly valid JSON.
*
* The field's own docstring says what to do with one: "Use `output` key to specify
* function output ... If `output` and `error` keys are not specified, then whole
* `response` is treated as function output." An object is passed through unchanged and
* everything else is wrapped.
*/
function toFunctionResponseStruct(value) {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : { output: value };
}
async function serializeFunctionCallOutput(item) {
const parseResult = await safeParseJSON({ text: item.output });
const response = parseResult.success ? toFunctionResponseStruct(parseResult.value) : { output: item.output };
return { toolResponse: { functionResponses: [{
id: item.callId,
name: item.name,
response
}] } };
}
/**
* Live models that reason in the background (e.g. `gemini-3.8-live-extended-thinking`).
* Google requires exactly one of `thinkingLevel` / `thinkingBudget` in their setup and
* rejects `thinkingConfig` on every other Live model.
*/
function isThinkingLiveModel(modelId) {
const modelName = modelId.split("/").at(-1)?.toLowerCase() ?? "";
return /^gemini-\d+\.\d+-live\b.*thinking/.test(modelName);
}
/**
* Builds a Google-specific session configuration from a normalized config.
* Used to construct the `bidiGenerateContentSetup` payload for auth token creation.
*/
function buildGoogleSessionConfig(config, modelId) {
const setup = { model: getModelPath(modelId) };
const { google, ...restProviderOptions } = config?.providerOptions ?? {};
const googleOptions = isRecord(google) ? google : void 0;
const generationConfig = {};
if (config?.outputModalities != null) generationConfig.responseModalities = config.outputModalities.map((m) => m.toUpperCase());
else generationConfig.responseModalities = ["AUDIO"];
if (config?.voice != null) generationConfig.speechConfig = { voiceConfig: { prebuiltVoiceConfig: { voiceName: config.voice } } };
setup.generationConfig = generationConfig;
if (config?.instructions != null) setup.systemInstruction = { parts: [{ text: config.instructions }] };
if (config?.tools != null && config.tools.length > 0) setup.tools = [{ functionDeclarations: config.tools.map((tool) => ({
name: tool.name,
description: tool.description,
parametersJsonSchema: tool.parameters,
...googleOptions?.defaultToolBehavior != null ? { behavior: googleOptions.defaultToolBehavior } : {}
})) }];
if (config?.inputAudioTranscription != null) setup.inputAudioTranscription = {};
if (config?.outputAudioTranscription != null) setup.outputAudioTranscription = {};
const rawGenerationConfig = restProviderOptions.generationConfig;
const explicitThinkingConfig = googleOptions?.thinkingConfig ?? (isRecord(rawGenerationConfig) && isRecord(rawGenerationConfig.thinkingConfig) ? rawGenerationConfig.thinkingConfig : void 0);
const thinkingConfig = isThinkingLiveModel(modelId) && explicitThinkingConfig?.thinkingLevel == null && explicitThinkingConfig?.thinkingBudget == null ? {
...explicitThinkingConfig,
thinkingLevel: "low"
} : googleOptions?.thinkingConfig;
const applyThinkingConfig = () => {
if (thinkingConfig == null) return;
const target = isRecord(setup.generationConfig) ? setup.generationConfig : generationConfig;
setup.generationConfig = {
...target,
thinkingConfig
};
};
if (config?.providerOptions == null) {
applyThinkingConfig();
return setup;
}
Object.assign(setup, restProviderOptions);
if (googleOptions?.translationConfig != null) setup.generationConfig = {
...isRecord(setup.generationConfig) ? setup.generationConfig : generationConfig,
translationConfig: googleOptions.translationConfig
};
applyThinkingConfig();
return setup;
}
//#endregion
//#region src/realtime/google-realtime-model.ts
const realtimeWebSocketPath = "google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateContentConstrained";
const realtimeServerWebSocketPath = "google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent";
function getAuthTokensURL(baseURL) {
const url = getRealtimeBaseURL(baseURL);
url.pathname = `${url.pathname.replace(/\/$/, "")}/v1alpha/auth_tokens`;
return url.toString();
}
function getWebSocketURL(baseURL) {
return getRealtimeWebSocketURL(baseURL, realtimeWebSocketPath).toString();
}
function getServerWebSocketURL(baseURL) {
const url = getRealtimeBaseURL(baseURL);
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
url.pathname = `${url.pathname.replace(/\/$/, "")}/ws/${realtimeServerWebSocketPath}`;
return url.toString();
}
var GoogleRealtimeModel = class {
constructor(modelId, config) {
this.specificationVersion = "v4";
this.capabilities = {
conversation: "turn-based",
transports: ["websocket"],
connections: ["client-secret-websocket", "server-websocket"]
};
this.mapper = new GoogleRealtimeEventMapper();
this.modelId = modelId;
this.provider = config.provider;
this.config = config;
}
async doCreateClientSecret(options) {
const fetchFn = this.config.fetch ?? fetch;
const apiKey = (await resolve(this.config.headers))["x-goog-api-key"];
if (!apiKey) throw new Error("Google Generative AI API key is required for realtime token creation.");
const now = Date.now();
const openWindowMs = (options.expiresAfterSeconds ?? 60) * 1e3;
const newSessionExpireTime = new Date(now + openWindowMs).toISOString();
const expireTime = new Date(now + openWindowMs + 18e5).toISOString();
const setupPayload = buildGoogleSessionConfig(options.sessionConfig, this.modelId);
const response = await fetchFn(`${getAuthTokensURL(this.config.baseURL)}?key=${encodeURIComponent(apiKey)}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
uses: 0,
expireTime,
newSessionExpireTime,
bidiGenerateContentSetup: setupPayload
})
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Google realtime auth token request failed: ${response.status} ${text}`);
}
const data = await response.json();
return {
token: data.name,
url: getWebSocketURL(this.config.baseURL),
expiresAt: data.expireTime ? Math.floor(new Date(data.expireTime).getTime() / 1e3) : void 0
};
}
getWebSocketConfig(options) {
return { url: `${options.url}?access_token=${encodeURIComponent(options.token)}` };
}
getServerWebSocketConfig(options) {
this.assertConversationIntent(options);
return resolveWith(this.config.headers, (headers) => ({
url: getServerWebSocketURL(this.config.baseURL),
headers: removeUndefinedEntries(headers)
}));
}
assertConversationIntent(options) {
if (options?.intent != null && options.intent !== "conversation") throw new Error(`Google realtime does not support the '${options.intent}' session intent.`);
}
parseServerEvent(raw) {
return this.mapper.parseServerEvent(raw);
}
createServerEventParser() {
const mapper = new GoogleRealtimeEventMapper();
return (raw) => mapper.parseServerEvent(raw);
}
serializeClientEvent(event, options) {
this.assertConversationIntent(options);
return this.mapper.serializeClientEvent(event, this.modelId);
}
buildSessionConfig(config, options) {
this.assertConversationIntent(options);
return buildGoogleSessionConfig(config, this.modelId);
}
};
//#endregion
//#region src/transcription/google-transcription-model-options.ts
/**
* Speech recognition options shared by unary (`gemini-3.5-transcribe`) and
* live (`gemini-3.5-transcribe-live`) transcription. Maps onto Google's
* `AudioTranscriptionConfig`.
*/
const googleTranscriptionModelOptions = lazySchema(() => zodSchema(z.object({
/**
* BCP-47 language codes providing hints about the languages present in the
* audio. If omitted or empty, defaults to automatic language detection.
*/
languageCodes: z.array(z.string()).optional(),
/**
* Custom vocabulary phrases, which bias the speech recognition model
* toward recognizing specific terms.
*/
customVocabulary: z.array(z.string()).optional(),
/**
* Enables word-level timestamp generation.
*/
wordTimestamp: z.boolean().optional(),
/**
* Enables speaker diarization.
*/
diarization: z.boolean().optional(),
/**
* Transcription output formatting mode.
*
* - `VERBATIM` (default): exact literal transcript preserving filler
* words, repetitions, and false starts.
* - `SMART`: cleans up and structures the transcript in real time —
* disfluency removal, inline self-corrections, structured formatting
* (lists, numbers, dates, paragraph breaks), and grammar/casing polish.
*/
mode: z.enum(["SMART", "VERBATIM"]).optional()
})));
//#endregion
//#region src/transcription/google-transcription-model.ts
const liveWebSocketPath$1 = "google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent";
/**
* After the input audio has ended, finish when no terminal signal
* (`turnComplete` / idle `interactionStatus`) arrives within this window.
* Trailing transcripts reset the timer.
*/
const defaultFinishGraceMs$1 = 3e3;
function getLiveWebSocketURL$1(baseURL, apiKey) {
const url = getRealtimeWebSocketURL(baseURL, liveWebSocketPath$1);
url.searchParams.set("key", apiKey);
return url;
}
/** Live transcription is only supported by `*-live` model variants. */
function isLiveTranscriptionModelId(modelId) {
return modelId.includes("-live");
}
var GoogleTranscriptionModel = class GoogleTranscriptionModel {
static [WORKFLOW_SERIALIZE](model) {
return serializeModelOptions({
modelId: model.modelId,
config: model.config
});
}
static [WORKFLOW_DESERIALIZE](options) {
return new GoogleTranscriptionModel(options.modelId, options.config);
}
get provider() {
return this.config.provider;
}
constructor(modelId, config) {
this.modelId = modelId;
this.config = config;
this.specificationVersion = "v4";
}
async parseOptions(providerOptions) {
return parseProviderOptions({
provider: "google",
providerOptions,
schema: googleTranscriptionModelOptions
});
}
async doGenerate(options) {
if (isLiveTranscriptionModelId(this.modelId)) throw new InvalidArgumentError({
argument: "modelId",
message: `Model '${this.modelId}' only supports streaming transcription. Use experimental_streamTranscribe, or a unary model such as 'gemini-3.5-transcribe'.`
});
const currentDate = this.config._internal?.currentDate?.() ?? /* @__PURE__ */ new Date();
const warnings = [];
const transcriptionConfig = buildTranscriptionConfig(await this.parseOptions(options.providerOptions));
const requestBody = {
model: this.modelId,
input: [{
type: "audio",
data: convertToBase64(options.audio),
mime_type: options.mediaType
}],
...transcriptionConfig != null ? { generation_config: { transcription_config: transcriptionConfig } } : {}
};
const { value: response, responseHeaders, rawValue: rawResponse } = await postJsonToApi({
url: `${this.config.baseURL}/interactions`,
headers: combineHeaders(this.config.headers ? await resolve(this.config.headers) : void 0, options.headers),
body: requestBody,
failedResponseHandler: googleFailedResponseHandler,
successfulResponseHandler: createJsonResponseHandler(googleInteractionsTranscriptionResponseSchema),
abortSignal: options.abortSignal,
fetch: this.config.fetch
});
let text = "";
const segments = [];
for (const step of response.steps ?? []) for (const content of step.content ?? []) {
if (content.type !== "text" || content.text == null) continue;
text += content.text;
for (const annotation of content.annotations ?? []) {
if (annotation.type !== "word_info") continue;
const startSecond = parseOffsetSeconds(annotation.start_offset);
const endSecond = parseOffsetSeconds(annotation.end_offset);
if (annotation.text == null || startSecond == null || endSecond == null) continue;
segments.push({
text: annotation.text,
startSecond,
endSecond
});
}
}
return {
text,
segments,
language: void 0,
durationInSeconds: void 0,
warnings,
...response.usage != null && { usage: response.usage },
response: {
timestamp: currentDate,
modelId: this.modelId,
headers: responseHeaders,
body: rawResponse
},
...response.usage != null ? { providerMetadata: { google: { usage: response.usage } } } : {}
};
}
async doStream(options) {
if (!isLiveTranscriptionModelId(this.modelId)) throw new InvalidArgumentError({
argument: "modelId",
message: `Model '${this.modelId}' does not support streaming transcription. Use a live model such as 'gemini-3.5-transcribe-live'.`
});
const currentDate = this.config._internal?.currentDate?.() ?? /* @__PURE__ */ new Date();
const warnings = [];
const googleOptions = await this.parseOptions(options.providerOptions);
validateLiveInputAudioFormat(options.inputAudioFormat);
const headers = combineHeaders(this.config.headers ? await resolve(this.config.headers) : void 0, options.headers);
let apiKey;
for (const [key, value] of Object.entries(headers)) if (key.toLowerCase() === "x-goog-api-key" && value != null) apiKey = value;
if (apiKey == null) throw new Error("Google Generative AI API key is required for streaming transcription.");
const webSocketHeaders = Object.fromEntries(Object.entries(headers).filter(([key]) => key.toLowerCase() !== "x-goog-api-key"));
const setup = {
model: getModelPath(this.modelId),
inputAudioTranscription: buildAudioTranscriptionConfig(googleOptions) ?? {}
};
return {
request: { body: setup },
response: {
timestamp: currentDate,
modelId: this.modelId
},
stream: createGoogleLiveTranscriptionStream({
webSocket: this.config.webSocket,
url: getLiveWebSocketURL$1(this.config.baseURL, apiKey),
headers: webSocketHeaders,
setup,
inputAudioRate: options.inputAudioFormat.rate ?? 16e3,
finishGraceMs: this.config._internal?.finishGraceMs ?? defaultFinishGraceMs$1,
warnings,
audio: options.audio,
abortSignal: options.abortSignal,
includeRawChunks: options.includeRawChunks
})
};
}
};
function createGoogleLiveTranscriptionStream({ webSocket, url, headers, setup, inputAudioRate, finishGraceMs, warnings, audio, abortSignal, includeRawChunks }) {
let finished = false;
let cleanup = () => {};
return new ReadableStream({
start: (controller) => {
let audioReader;
let connection;
let resolveSetupComplete;
const setupComplete = new Promise((resolve) => {
resolveSetupComplete = resolve;
});
let segmentCounter = 0;
let segmentBuffer = "";
let fullText = "";
let latestInterim = "";
let language;
let audioEnded = false;
let usageMetadata;
let finishTimer;
const segmentId = () => `google-segment-${segmentCounter}`;
const cancelPendingFinish = () => {
if (finishTimer != null) {
clearTimeout(finishTimer);
finishTimer = void 0;
}
};
const schedulePendingFinish = () => {
if (finished || !audioEnded) return;
cancelPendingFinish();
finishTimer = setTimeout(() => {
finishTimer = void 0;
finish();
}, finishGraceMs);
};
cleanup = (closeCode) => {
cancelPendingFinish();
if (audioReader != null) audioReader.cancel().catch(() => {});
else audio.cancel().catch(() => {});
connection?.close(closeCode);
};
const finishWithError = (error) => {
if (finished) return;
finished = true;
cleanup();
controller.error(error);
};
const completeSegment = () => {
if (segmentBuffer === "") {
if (latestInterim === "") return;
segmentBuffer = latestInterim;
}
latestInterim = "";
controller.enqueue({
type: "transcript-final",
id: segmentId(),
text: segmentBuffer
});
fullText += fullText === "" ? segmentBuffer : ` ${segmentBuffer}`;
segmentBuffer = "";
segmentCounter++;
};
const finish = () => {
if (finished) return;
completeSegment();
finished = true;
controller.enqueue({
type: "finish",
text: fullText,
segments: [],
language,
durationInSeconds: void 0,
usage: usageMetadata,
...usageMetadata != null ? { providerMetadata: { google: { usageMetadata } } } : {}
});
controller.close();
cleanup(1e3);
};
const sendAudio = async (socket) => {
audioReader = audio.getReader();
try {
while (true) {
const { done, value } = await audioReader.read();
if (done || finished) break;
socket.send(JSON.stringify({ realtimeInput: { audio: {
data: convertToBase64(value),
mimeType: `audio/pcm;rate=${inputAudioRate}`
} } }));
await waitForWebSocketBufferDrain(socket);
}
} finally {
audioReader.releaseLock();
audioReader = void 0;
}
if (!finished) {
socket.send(JSON.stringify({ realtimeInput: { audioStreamEnd: true } }));
audioEnded = true;
schedulePendingFinish();
}
};
connection = connectToWebSocket({
url,
headers,
webSocket,
abortSignal,
onAbort: finishWithError,
onProcessingError: finishWithError,
onOpen: (socket) => {
controller.enqueue({
type: "stream-start",
warnings
});
socket.send(JSON.stringify({ setup }));
setupComplete.then(() => finished ? void 0 : sendAudio(socket)).catch(finishWithError);
},
onMessageText: async (text) => {
if (finished) return;
const parsed = await safeParseJSON({ text });
if (!parsed.success) return;
const message = parsed.value;
if (includeRawChunks) controller.enqueue({
type: "raw",
rawValue: message
});
if (message.setupComplete != null) resolveSetupComplete();
if (message.usageMetadata != null) usageMetadata = message.usageMetadata;
if (message.error != null) {
finishWithError(new Error(message.error.message ?? "Google Live API error"));
return;
}
const serverContent = message.serverContent;
const interim = serverContent?.interimInputTranscription;
if (interim?.text) {
schedulePendingFinish();
latestInterim = interim.text;
controller.enqueue({
type: "transcript-partial",
id: segmentId(),
text: interim.text
});
}
const transcription = serverContent?.inputTranscription ?? message.inputTranscription;
if (transcription != null) {
if (transcription.languageCode != null) language = transcription.languageCode;
if (transcription.text) {
schedulePendingFinish();
latestInterim = "";
segmentBuffer += transcription.text;
controller.enqueue({
type: "transcript-delta",
id: segmentId(),
delta: transcription.text
});
}
if (transcription.finished === true) completeSegment();
}
if (serverContent?.turnComplete) completeSegment();
const interactionStatus = serverContent?.interactionStatus;
if (audioEnded && (interactionStatus === "IDLE" || interactionStatus === "REQUIRES_ACTION" || serverContent?.turnComplete === true && interactionStatus == null)) finish();
},
onSocketError: () => {
finishWithError(/* @__PURE__ */ new Error("Google Live transcription error"));
},
onClose: ({ code, reason }) => {
if (finished) return;
if (audioEnded) {
finish();
return;
}
finishWithError(/* @__PURE__ */ new Error(`Google Live transcription WebSocket closed unexpectedly before finishing (code ${code ?? "unknown"}${reason ? `, reason: ${reason}` : ""}).`));
}
});
},
cancel: () => {
if (finished) return;
finished = true;
cleanup();
}
});
}
/**
* Builds Google's `AudioTranscriptionConfig` from provider options; returns
* undefined when no options are set.
*/
function buildAudioTranscriptionConfig(options) {
if (options == null) return void 0;
const config = {};
if (options.languageCodes != null) config.languageCodes = options.languageCodes;
if (options.customVocabulary != null) config.customVocabulary = options.customVocabulary;
if (options.wordTimestamp != null) config.wordTimestamp = options.wordTimestamp;
if (options.diarization != null) config.diarization = options.diarization;
if (options.mode != null) config.mode = options.mode;
return Object.keys(config).length > 0 ? config : void 0;
}
/**
* Builds the Interactions API `transcription_config` (snake_case wire) from
* provider options; returns undefined when no options are set. Diarization
* and word timestamps are expressed inside the `mode` object per
* https://ai.google.dev/gemini-api/docs/transcribe.
*/
function buildTranscriptionConfig(options) {
if (options == null) return void 0;
const config = {};
if (options.languageCodes != null) config.language_codes = options.languageCodes;
if (options.customVocabulary != null) config.custom_vocabulary = options.customVocabulary;
if (options.mode != null || options.diarization === true || options.wordTimestamp === true) config.mode = {
type: (options.mode ?? "VERBATIM").toLowerCase(),
...options.diarization === true ? { diarization_mode: "speaker" } : {},
...options.wordTimestamp === true ? { timestamp_granularities: ["word"] } : {}
};
return Object.keys(config).length > 0 ? config : void 0;
}
/** Parses a Google duration offset such as `"1s"` or `"9.400s"` to seconds. */
function parseOffsetSeconds(offset) {
if (offset == null) return void 0;
const parsed = Number.parseFloat(offset);
return Number.isFinite(parsed) ? parsed : void 0;
}
function validateLiveInputAudioFormat(inputAudioFormat) {
if (inputAudioFormat.type !== "audio/pcm" || inputAudioFormat.rate != null && inputAudioFormat.rate !== 16e3) throw new InvalidArgumentError({
argument: "inputAudioFormat",
message: "The Gemini Live transcription API only supports 16kHz 16-bit PCM input audio."
});
}
const googleInteractionsWordAnnotationSchema = () => z.object({
type: z.string().nullish(),
text: z.string().nullish(),
speaker: z.string().nullish(),
start_offset: z.string().nullish(),
end_offset: z.string().nullish()
});
const googleInteractionsTranscriptionResponseSchema = lazySchema(() => zodSchema(z.object({
status: z.string().nullish(),
steps: z.array(z.object({
type: z.string().nullish(),
content: z.array(z.object({
type: z.string().nullish(),
text: z.string().nullish(),
annotations: z.array(googleInteractionsWordAnnotationSchema()).nullish()
})).nullish()
})).nullish(),
usage: z.record(z.string(), z.json()).nullish()
})));
//#endregion
//#region src/speech-translation/google-speech-translation-model-options.ts
const googleSpeechTranslationModelOptions = lazySchema(() => zodSchema(z.object({
/**
* Whether input audio already in the target language should be echoed
* instead of producing silence.
*/
echoTargetLanguage: z.boolean().optional() })));
//#endregion
//#region src/speech-translation/google-speech-translation-model.ts
const liveWebSocketPath = "google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent";
/**
* After the input audio has ended, finish after this much trailing output
* silence. Live Translation is continuous and does not emit turnComplete.
*/
const defaultFinishGraceMs = 1e3;
const googleLiveOutputAudioRate = 24e3;
const pcm16SilenceAmplitudeThreshold = 128;
function getLiveWebSocketURL(baseURL, apiKey) {
const url = getRealtimeWebSocketURL(baseURL, liveWebSocketPath);
url.searchParams.set("key", apiKey);
return url;
}
var GoogleSpeechTranslationModel = class GoogleSpeechTranslationModel {
static [WORKFLOW_SERIALIZE](model) {
return serializeModelOptions({
modelId: model.modelId,
config: model.config
});
}
static [WORKFLOW_DESERIALIZE](options) {
return new GoogleSpeechTranslationModel(options.modelId, options.config);
}
get provider() {
return this.config.provider;
}
constructor(modelId, config) {
this.specificationVersion = "v4";
this.modelId = modelId;
this.config = config;
}
async doStream(options) {
if (options.targetLanguage == null) throw new InvalidArgumentError({
argument: "targetLanguage",
message: `targetLanguage is required for translation model '${this.modelId}'.`
});
const currentDate = this.config._internal?.currentDate?.() ?? /* @__PURE__ */ new Date();
const googleOptions = await parseProviderOptions({
provider: "google",
providerOptions: options.providerOptions,
schema: googleSpeechTranslationModelOptions
});
const warnings = [];
validateGoogleSpeechTranslationInputAudioFormat(options.inputAudioFormat);
if (options.sourceLanguage != null) warnings.push({
type: "unsupported",
feature: "sourceLanguage",
details: "The Gemini Live translation API auto-detects the source language and does not accept a source language."
});
if (options.outputAudioFormat != null) warnings.push({
type: "unsupported",
feature: "outputAudioFormat",
details: "The Gemini Live API always outputs 24kHz 16-bit PCM audio and does not accept an output audio format."
});
const headers = combineHeaders(await resolve(this.config.headers), options.headers);
let apiKey;
for (const [key, value] of Object.entries(headers)) if (key.toLowerCase() === "x-goog-api-key" && value != null) apiKey = value;
if (apiKey == null) throw new Error("Google Generative AI API key is required for streaming translation.");
const webSocketHeaders = Object.fromEntries(Object.entries(headers).filter(([key]) => key.toLowerCase() !== "x-goog-api-key"));
const setup = buildGoogleLiveSpeechTranslationSetup({
modelId: this.modelId,
targetLanguage: options.targetLanguage,
providerOptions: googleOptions
});
return {
request: { body: setup },
response: {
timestamp: currentDate,
modelId: this.modelId
},
stream: createGoogleLiveSpeechTranslationStream({
webSocket: this.config.webSocket,
url: getLiveWebSocketURL(this.config.baseURL, apiKey),
headers: webSocketHeaders,
setup,
inputAudioRate: options.inputAudioFormat.rate ?? 16e3,
finishGraceMs: this.config._internal?.finishGraceMs ?? defaultFinishGraceMs,
warnings,
audio: options.audio,
abortSignal: options.abortSignal,
includeRawChunks: options.includeRawChunks
})
};
}
};
function createGoogleLiveSpeechTranslationStream({ webSocket, url, headers, setup, inputAudioRate, finishGraceMs, warnings, audio, abortSignal, includeRawChunks }) {
let finished = false;
let cleanup = () => {};
return new ReadableStream({
start: (controller) => {
let audioReader;
let connection;
let resolveSetupComplete;
const setupComplete = new Promise((resolve) => {
resolveSetupComplete = resolve;
});
let turnCounter = 0;
let sourceText = "";
let sourceTurnBuffer = "";
let translationText = "";
let translationTurnBuffer = "";
let audioEnded = false;
let usage;
let openTurn = false;
let sawTurnComplete = false;
let trailingSilenceMs = 0;
let finishTimer;
const itemId = () => `google-item-${turnCounter}`;
const cancelPendingFinish = () => {
if (finishTimer != null) {
clearTimeout(finishTimer);
finishTimer = void 0;
}
};
const schedulePendingFinish = () => {
if (finished || finishTimer != null) return;
finishTimer = setTimeout(() => {
finishTimer = void 0;
finish();
}, finishGraceMs);
};
const onTurnActivity = () => {
openTurn = true;
trailingSilenceMs = 0;
cancelPendingFinish();
};
cleanup = (closeCode) => {
cancelPendingFinish();
if (audioReader != null) audioReader.cancel().catch(() => {});
else audio.cancel().catch(() => {});
connection?.close(closeCode);
};
const finishWithError = (error) => {
if (finished) return;
finished = true;
cleanup();
controller.error(error);
};
const finish = () => {
if (finished) return;
if (sourceTurnBuffer !== "" || translationTurnBuffer !== "") completeTurn();
finished = true;
controller.enqueue({
type: "finish",
sourceText,
outputText: translationText,
usage
});
controller.close();
cleanup(1e3);
};
const completeTurn = () => {
if (sourceTurnBuffer !== "") {
controller.enqueue({
type: "source-transcript-final",
id: itemId(),
text: sourceTurnBuffer
});
sourceText += sourceTurnBuffer;
sourceTurnBuffer = "";
}
if (translationTurnBuffer !== "") {
controller.enqueue({
type: "output-text-final",
id: itemId(),
text: translationTurnBuffer
});
translationText += translationTurnBuffer;
translationTurnBuffer = "";
}
turnCounter++;
};
const sendAudio = async (socket) => {
audioReader = audio.getReader();
try {
while (true) {
const { done, value } = await audioReader.read();
if (done || finished) break;
socket.send(JSON.stringify({ realtimeInput: { audio: {
data: convertToBase64(value),
mimeType: `audio/pcm;rate=${inputAudioRate}`
} } }));
await waitForWebSocketBufferDrain(socket);
}
} finally {
audioReader.releaseLock();
audioReader = void 0;
}
if (!finished) {
socket.send(JSON.stringify({ realtimeInput: { audioStreamEnd: true } }));
audioEnded = true;
if (sawTurnComplete && !openTurn) schedulePendingFinish();
}
};
connection = connectToWebSocket({
url,
headers,
webSocket,
abortSignal,
onAbort: finishWithError,
onProcessingError: finishWithError,
onOpen: (socket) => {
controller.enqueue({
type: "stream-start",
warnings
});
socket.send(JSON.stringify({ setup }));
setupComplete.then(() => finished ? void 0 : sendAudio(socket)).catch(finishWithError);
},
onMessageText: async (text) => {
if (finished) return;
const parsed = await safeParseJSON({ text });
if (!parsed.success) return;
const message = parsed.value;
if (includeRawChunks) controller.enqueue({
type: "raw",
rawValue: message
});
if (message.setupComplete != null) resolveSetupComplete();
if (message.usageMetadata != null) usage = accumulateGoogleLiveUsage(usage, message.usageMetadata);
if (message.error != null) {
finishWithError(new Error(message.error.message ?? "Google Live API error"));
return;
}
const inputTranscriptionText = message.serverContent?.inputTranscription?.text ?? message.inputTranscription?.text;
if (inputTranscriptionText) {
onTurnActivity();
sourceTurnBuffer += inputTranscriptionText;
controller.enqueue({
type: "source-transcript-delta",
id: itemId(),
delta: inputTranscriptionText
});
}
const serverContent = message.serverContent;
if (serverContent == null) return;
for (const part of serverContent.modelTurn?.parts ?? []) if (part.inlineData?.data) {
controller.enqueue({
type: "audio",
id: itemId(),
audio: part.inlineData.data
});
const silenceDurationMs = getPcm16SilenceDurationMs(part.inlineData.data);
if (audioEnded && silenceDurationMs != null) {
trailingSilenceMs += silenceDurationMs;
if (trailingSilenceMs >= finishGraceMs) {
finish();
return;
}
} else onTurnActivity();
}
if (serverContent.outputTranscription?.text) {
onTurnActivity();
translationTurnBuffer += serverContent.outputTranscription.text;
controller.enqueue({
type: "output-text-delta",
id: itemId(),
delta: serverContent.outputTranscription.text
});
}
if (serverContent.turnComplete) {
completeTurn();
openTurn = false;
sawTurnComplete = true;
if (audioEnded) schedulePendingFinish();
}
},
onSocketError: () => {
finishWithError(/* @__PURE__ */ new Error("Google Live translation error"));
},
onClose: ({ code, reason }) => {
if (finished) return;
if (finishTimer != null) {
finish();
return;
}
finishWithError(/* @__PURE__ */ new Error(`Google Live translation WebSocket closed unexpectedly before finishing (code ${code ?? "unknown"}${reason ? `, reason: ${reason}` : ""}).`));
}
});
},
cancel: () => {
if (finished) return;
finished = true;
cleanup();
}
});
}
function accumulateGoogleLiveUsage(usage, usageMetadata) {
let inputAudioTokens = usage?.inputAudioTokens;
let outputAudioTokens = usage?.outputAudioTokens;
for (const detail of usageMetadata.promptTokensDetails ?? []) if (detail.modality === "AUDIO" && detail.tokenCount != null) inputAudioTokens = (inputAudioTokens ?? 0) + detail.tokenCount;
for (const detail of usageMetadata.responseTokensDetails ?? []) if (detail.modality === "AUDIO" && detail.tokenCount != null) outputAudioTokens = (outputAudioTokens ?? 0) + detail.tokenCount;
if (inputAudioTokens == null && outputAudioTokens == null) return usage;
return {
...usage,
...inputAudioTokens != null ? { inputAudioTokens } : {},
...outputAudioTokens != null ? { outputAudioTokens } : {}
};
}
function getPcm16SilenceDurationMs(audio) {
let bytes;
try {
bytes = convertBase64ToUint8Array(audio);
} catch {
return;
}
if (bytes.byteLength < 2) return;
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const sampleCount = Math.floor(bytes.byteLength / 2);
for (let i = 0; i < sampleCount; i++) if (Math.abs(view.getInt16(i * 2, true)) > pcm16SilenceAmplitudeThreshold) return;
return sampleCount / googleLiveOutputAudioRate * 1e3;
}
function buildGoogleLiveSpeechTranslationSetup({ modelId, targetLanguage, providerOptions }) {
return {
model: getModelPath(modelId),
generationConfig: {
responseModalities: ["AUDIO"],
translationConfig: {
targetLanguageCode: targetLanguage,
...providerOptions?.echoTargetLanguage != null ? { echoTargetLanguage: providerOptions.echoTargetLanguage } : {}
}
},
inputAudioTranscription: {},
outputAudioTranscription: {}
};
}
function validateGoogleSpeechTranslationInputAudioFormat(inputAudioFormat) {
if (inputAudioFormat.type !== "audio/pcm" || inputAudioFormat.rate != null && inputAudioFormat.rate !== 16e3) throw new InvalidArgumentError({
argument: "inputAudioFormat",
message: "The Gemini Live translation API only supports 16kHz 16-bit PCM input audio."
});
}
//#endregion
//#region src/google-provider.ts
const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
const googleFilesUrlPattern = /^https:\/\/generativelanguage\.googleapis\.com\/v1beta\/files\/.*$/;
const supportedExternalUrlMediaTypes = [
"text/html",
"text/css",
"text/plain",
"text/xml",
"text/csv",
"text/rtf",
"text/javascript",
"application/json",
"application/pdf",
"image/bmp",
"image/jpeg",
"image/png",
"image/webp",
"video/mp4",
"video/mpeg",
"video/quicktime",
"video/avi",
"video/x-flv",
"video/mpg",
"video/webm",
"video/wmv",
"video/3gpp"
];
const externalHttpsUrlPattern = /^https:\/\/.*$/;
function supportsExternalFileUrls(modelId) {
return /(^|\/)gemini-/.test(modelId) && !/(^|\/)gemini-2\.0/.test(modelId);
}
/**
* Create a Google provider instance.
*/
function createGoogle(options = {}) {
const baseURL = withoutTrailingSlash(options.baseURL) ?? DEFAULT_BASE_URL;
const providerName = options.name ?? "google.generative-ai";
const getHeaders = () => resolveWith(options.apiKey, (apiKey) => withUserAgentSuffix({
"x-goog-api-key": loadApiKey({
apiKey,
environmentVariableName: "GOOGLE_GENERATIVE_AI_API_KEY",
description: "Google Generative AI"
}),
...options.headers
}, `ai-sdk-google/${VERSION}`));
const getSupportedUrls = (modelId, includeExternalUrls = modelId == null || supportsExternalFileUrls(modelId)) => ({
"*": [
googleFilesUrlPattern,
new RegExp(`^${baseURL}/files/.*$`),
new RegExp(`^https://(?:www\\.)?youtube\\.com/watch\\?v=[\\w-]+(?:&[\\w=&.-]*)?$`),
new RegExp(`^https://youtu\\.be/[\\w-]+(?:\\?[\\w=&.-]*)?$`)
],
...includeExternalUrls ? Object.fromEntries(supportedExternalUrlMediaTypes.map((mediaType) => [mediaType, [externalHttpsUrlPattern]])) : {}
});
const languageModelConfig = {
provider: providerName,
baseURL,
headers: getHeaders,
generateId: options.generateId ?? generateId,
fetch: options.fetch
};
const createChatModel = (modelId) => new GoogleLanguageModel(modelId, {
...languageModelConfig,
supportedUrls: () => getSupportedUrls(modelId)
});
const createBatch = () => new GoogleBatch({
provider: `${providerName.replace(/\.generative-ai$/, "")}.batch`,
maxLineBytes: options.batchResultDownloads?.maxLineBytes,
config: languageModelConfig,
supportedUrls: getSupportedUrls(void 0, false)
});
const createEmbeddingModel = (modelId) => new GoogleEmbeddingModel(modelId, {
provider: providerName,
baseURL,
headers: getHeaders,
fetch: options.fetch
});
const createImageModel = (modelId, settings = {}) => new GoogleImageModel(modelId, settings, {
provider: providerName,
baseURL,
headers: getHeaders,
fetch: options.fetch
});
const createFiles = () => new GoogleFiles({
provider: providerName,
baseURL,
headers: getHeaders,
fetch: options.fetch
});
const createVideoModel = (modelId) => new GoogleVideoModel(modelId, {
provider: providerName,
baseURL,
headers: getHeaders,
fetch: options.fetch,
generateId: options.generateId ?? generateId
});
const createRealtimeModel = (modelId) => new GoogleRealtimeModel(modelId, {
provider: `${providerName}.realtime`,
baseURL,
headers: getHeaders,
fetch: options.fetch
});
const createSpeechTranslationModel = (modelId) => new GoogleSpeechTranslationModel(modelId, {
provider: `${providerName}.speech-translation`,
baseURL,
headers: getHeaders,
webSocket: options.webSocket
});
const createSpeechModel = (modelId) => new GoogleSpeechModel(modelId, {
provider: `${providerName}.speech`,
baseURL,
headers: getHeaders,
fetch: options.fetch
});
const createTranscriptionModel = (modelId) => new GoogleTranscriptionModel(modelId, {
provider: `${providerName}.transcription`,
baseURL,
headers: getHeaders,
fetch: options.fetch,
webSocket: options.webSocket
});
const experimentalRealtimeFactory = Object.assign((modelId) => createRealtimeModel(modelId), { getToken: async (tokenOptions) => {
const secret = await createRealtimeModel(tokenOptions.model).doCreateClientSecret({
sessionConfig: tokenOptions.sessionConfig,
expiresAfterSeconds: tokenOptions.expiresAfterSeconds
});
return {
token: secret.token,
url: secret.url,
expiresAt: secret.expiresAt
};
} });
const createInteractionsModel = (modelIdOrAgent) => new GoogleInteractionsLanguageModel(modelIdOrAgent, {
provider: `${providerName}.interactions`,
baseURL,
headers: getHeaders,
generateId: options.generateId ?? generateId,
fetch: options.fetch
});
const provider = function(modelId) {
if (new.target) throw new Error("The Google Generative AI model function cannot be called with the new keyword.");
return createChatModel(modelId);
};
provider.specificationVersion = "v4";
provider.languageModel = createChatModel;
provider.chat = createChatModel;
provider.generativeAI = createChatModel;
provider.decisionModel = (modelId) => new DecisionGoogleModel(modelId, {
provider: `${providerName.replace(/\.generative-ai$/, "")}.decision`,
baseURL,
headers: getHeaders,
fetch: options.fetch
});
provider.evaluationModel = provider.decisionModel;
provider.extractionModel = (modelId) => new Experimental_ExtractionLanguageModel({
model: createChatModel(modelId),
provider: `${providerName.replace(/\.generative-ai$/, "")}.extraction`
});
provider.experimental_batch = createBatch;
provider.embedding = createEmbeddingModel;
provider.embeddingModel = createEmbeddingModel;
provider.textEmbedding = createEmbeddingModel;
provider.textEmbeddingModel = createEmbeddingModel;
provider.image = createImageModel;
provider.imageModel = createImageModel;
provider.video = createVideoModel;
provider.videoModel = createVideoModel;
provider.experimental_realtime = experimentalRealtimeFactory;
provider.files = createFiles;
provider.speech = createSpeechModel;
provider.speechModel = createSpeechModel;
provider.transcription = createTranscriptionModel;
provider.transcriptionModel = createTranscriptionModel;
provider.translation = createSpeechTranslationModel;
provider.speechTranslationModel = createSpeechTranslationModel;
provider.interactions = createInteractionsModel;
provider.tools = googleTools;
return provider;
}
/**
* Default Google Generative AI provider instance.
*/
const google = createGoogle();
//#endregion
export { GoogleRealtimeModel as Experimental_GoogleRealtimeModel, GoogleSpeechTranslationModel as Experimental_GoogleSpeechTranslationModel, GoogleSpeechTranslationModel as Experimental_GoogleTranslationModel, GoogleTranscriptionModel, VERSION, createGoogle, createGoogle as createGoogleGenerativeAI, google };
//# sourceMappingURL=index.js.map