@ai-sdk/google
Version:
The **[Google Generative AI provider](https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai)** for the [AI SDK](https://ai-sdk.dev/docs) contains language model support for the [Google Generative AI](https://ai.google/discover/generativeai/)
246 lines (237 loc) • 7.94 kB
text/typescript
import type { LanguageModelV3Source } from '@ai-sdk/provider';
import type {
GoogleInteractionsAnnotation,
GoogleInteractionsBuiltinToolResultContent,
GoogleInteractionsFileCitation,
GoogleInteractionsGoogleMapsResultContent,
GoogleInteractionsGoogleSearchResultContent,
GoogleInteractionsPlaceCitation,
GoogleInteractionsURLCitation,
GoogleInteractionsURLContextResultContent,
} from './google-interactions-prompt';
const KNOWN_DOC_EXTENSIONS: Record<string, string> = {
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: string): string {
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: string): string | undefined {
const parts = uriOrName.split('/');
const last = parts[parts.length - 1];
return last && last.length > 0 ? last : undefined;
}
/**
* Maps a single text-block annotation (`url_citation` / `file_citation` /
* `place_citation`) onto a `LanguageModelV3Source`. Returns `undefined` when
* the annotation lacks the minimum payload to form a source (e.g. a URL
* citation without a `url`).
*/
export function annotationToSource({
annotation,
generateId,
}: {
annotation: GoogleInteractionsAnnotation | { type: string };
generateId: () => string;
}): LanguageModelV3Source | undefined {
switch (annotation.type) {
case 'url_citation': {
const a = annotation as GoogleInteractionsURLCitation;
if (a.url == null || a.url.length === 0) return undefined;
return {
type: 'source',
sourceType: 'url',
id: generateId(),
url: a.url,
...(a.title != null ? { title: a.title } : {}),
};
}
case 'file_citation': {
const a = annotation as GoogleInteractionsFileCitation;
const uri = a.url ?? a.document_uri ?? a.file_name;
if (uri == null || uri.length === 0) return undefined;
if (uri.startsWith('http://') || uri.startsWith('https://')) {
return {
type: 'source',
sourceType: 'url',
id: generateId(),
url: uri,
...(a.file_name != null ? { title: a.file_name } : {}),
};
}
const filename = a.file_name ?? basename(uri);
const mediaType = inferDocMediaType(uri);
return {
type: 'source',
sourceType: 'document',
id: generateId(),
mediaType,
title: a.file_name ?? filename ?? uri,
...(filename != null ? { filename } : {}),
};
}
case 'place_citation': {
const a = annotation as GoogleInteractionsPlaceCitation;
if (a.url == null || a.url.length === 0) return undefined;
return {
type: 'source',
sourceType: 'url',
id: generateId(),
url: a.url,
...(a.name != null ? { title: a.name } : {}),
};
}
default:
return undefined;
}
}
/**
* Maps a built-in tool *result* content block to zero or more
* `LanguageModelV3Source` 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)
*/
export function builtinToolResultToSources({
block,
generateId,
}: {
block: GoogleInteractionsBuiltinToolResultContent;
generateId: () => string;
}): Array<LanguageModelV3Source> {
const sources: Array<LanguageModelV3Source> = [];
switch (block.type) {
case 'url_context_result': {
const result =
(block as GoogleInteractionsURLContextResultContent).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 as GoogleInteractionsGoogleSearchResultContent).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 as GoogleInteractionsGoogleMapsResultContent).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 as { result?: Array<unknown> }).result ?? [];
for (const raw of result) {
if (raw == null || typeof raw !== 'object') continue;
const entry = raw as {
file_name?: string;
document_uri?: string;
url?: string;
title?: string;
};
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;
}
default:
break;
}
return sources;
}
/**
* Given a list of annotations attached to a single `text` content block,
* returns the corresponding `LanguageModelV3Source` parts (de-duplicated by
* URL/filename to avoid double-counting when the same citation reappears
* across deltas).
*/
export function annotationsToSources({
annotations,
generateId,
}: {
annotations:
| Array<GoogleInteractionsAnnotation | { type: string }>
| null
| undefined;
generateId: () => string;
}): Array<LanguageModelV3Source> {
if (annotations == null) return [];
const seen = new Set<string>();
const sources: Array<LanguageModelV3Source> = [];
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;
}