@ai-sdk/google-vertex
Version:
466 lines (410 loc) • 15 kB
text/typescript
import { getGoogleVertexRequestConfig } from './google-vertex-model-catalog-config';
import {
GoogleInteractionsLanguageModel,
GoogleLanguageModel,
GoogleSpeechModel,
type GoogleInteractionsAgentName,
type GoogleInteractionsModelId,
type GoogleInteractionsModelInput,
} from '@ai-sdk/google/internal';
import {
InvalidArgumentError,
LoadAPIKeyError,
type Experimental_VideoModelV4,
type ImageModelV4,
type LanguageModelV4,
type ProviderV4,
type SpeechModelV4,
type TranscriptionModelV4,
} from '@ai-sdk/provider';
import {
generateId,
isValidHostnamePart,
loadOptionalSetting,
loadSetting,
normalizeHeaders,
resolve,
withoutTrailingSlash,
withUserAgentSuffix,
type FetchFunction,
type Resolvable,
type WebSocketConstructor,
} from '@ai-sdk/provider-utils';
import { VERSION } from './version';
import type { GoogleVertexConfig } from './google-vertex-config';
import { GoogleVertexEmbeddingModel } from './google-vertex-embedding-model';
import type { GoogleVertexEmbeddingModelId } from './google-vertex-embedding-model-options';
import { GoogleVertexImageModel } from './google-vertex-image-model';
import type { GoogleVertexImageModelId } from './google-vertex-image-settings';
import type { GoogleVertexModelId } from './google-vertex-options';
import { GoogleVertexCloudTTSSpeechModel } from './google-vertex-cloud-tts-speech-model';
import { googleVertexTools } from './google-vertex-tools';
import { GoogleVertexTranscriptionModel } from './google-vertex-transcription-model';
import type { GoogleVertexTranscriptionModelId } from './google-vertex-transcription-model-options';
import { GoogleVertexGeminiTranscriptionModel } from './gemini-transcription/google-vertex-gemini-transcription-model';
import { GoogleVertexVideoModel } from './google-vertex-video-model';
import type { GoogleVertexVideoModelId } from './google-vertex-video-settings';
import type { GoogleVertexSpeechModelId } from './google-vertex-speech-model-options';
const EXPRESS_MODE_BASE_URL =
'https://aiplatform.googleapis.com/v1/publishers/google';
// Tuned models are served from a deployed endpoint and addressed by their
// `endpoints/{id}` resource, which replaces `publishers/google/models/{id}`.
// https://cloud.google.com/vertex-ai/generative-ai/docs/deploy/overview
function isEndpointModelId(modelId: string): boolean {
return getGoogleVertexRequestConfig(modelId).language?.endpoint === true;
}
// set `x-goog-api-key` header to API key for express mode
function createExpressModeFetch(
apiKey: Resolvable<string>,
customFetch?: FetchFunction,
): FetchFunction {
return async (url, init) => {
const resolvedApiKey = await resolve(apiKey);
if (
typeof apiKey !== 'string' &&
(typeof resolvedApiKey !== 'string' || !resolvedApiKey.trim())
) {
throw new LoadAPIKeyError({
message: 'Google Vertex API key must be a non-empty string.',
});
}
const modifiedInit: RequestInit = {
...init,
headers: {
...(init?.headers ? normalizeHeaders(init.headers) : {}),
'x-goog-api-key': resolvedApiKey,
},
};
return (customFetch ?? fetch)(url.toString(), modifiedInit);
};
}
export interface GoogleVertexProvider extends ProviderV4 {
/**
* Creates a model for text generation.
*/
(modelId: GoogleVertexModelId): LanguageModelV4;
languageModel: (modelId: GoogleVertexModelId) => LanguageModelV4;
/**
* Creates a language model targeting the Gemini Interactions API
* (`.../locations/{region}/interactions`) on Vertex, reusing the Vertex
* OAuth credentials. Pass a model id (e.g. `gemini-omni-flash-preview`) or an
* `{ agent }` / `{ managedAgent }` reference.
*/
interactions(
modelIdOrAgent:
| GoogleInteractionsModelId
| { agent: GoogleInteractionsAgentName }
| { managedAgent: string },
): LanguageModelV4;
/**
* Creates a model for image generation.
*/
image(modelId: GoogleVertexImageModelId): ImageModelV4;
/**
* Creates a model for image generation.
*/
imageModel(modelId: GoogleVertexImageModelId): ImageModelV4;
tools: typeof googleVertexTools;
/**
* @deprecated Use `embeddingModel` instead.
*/
textEmbeddingModel(
modelId: GoogleVertexEmbeddingModelId,
): GoogleVertexEmbeddingModel;
/**
* Creates a model for video generation.
*/
video(modelId: GoogleVertexVideoModelId): Experimental_VideoModelV4;
/**
* Creates a model for video generation.
*/
videoModel(modelId: GoogleVertexVideoModelId): Experimental_VideoModelV4;
/**
* Creates a model for speech generation (text-to-speech).
*/
speech(modelId: GoogleVertexSpeechModelId): SpeechModelV4;
/**
* Creates a model for speech generation (text-to-speech).
*/
speechModel(modelId: GoogleVertexSpeechModelId): SpeechModelV4;
/**
* Creates a model for transcription (speech-to-text).
*/
transcription(
modelId: GoogleVertexTranscriptionModelId,
): TranscriptionModelV4;
/**
* Creates a model for transcription (speech-to-text).
*/
transcriptionModel(
modelId: GoogleVertexTranscriptionModelId,
): TranscriptionModelV4;
}
export interface GoogleVertexProviderSettings {
/**
* Optional. The API key for the Google Cloud project. If provided, the
* provider will use express mode with API key authentication. Defaults to
* the value of the `GOOGLE_VERTEX_API_KEY` environment variable.
* A callback is resolved before each request and must return a non-empty key.
*/
apiKey?: Resolvable<string>;
/**
* Your Google Vertex location. Defaults to the environment variable `GOOGLE_VERTEX_LOCATION`.
*/
location?: string;
/**
* Your Google Vertex project. Defaults to the environment variable `GOOGLE_VERTEX_PROJECT`.
*/
project?: string;
/**
* Headers to use for requests. Can be:
* - A headers object
* - A Promise that resolves to a headers object
* - A function that returns a headers object
* - A function that returns a Promise of a headers object
*/
headers?: Resolvable<Record<string, string | undefined>>;
/**
* Custom fetch implementation. You can use it as a middleware to intercept requests,
* or to provide a custom fetch implementation for e.g. testing.
*/
fetch?: FetchFunction;
// for testing
generateId?: () => string;
/**
* Base URL for the Google Vertex API calls.
*/
baseURL?: string;
/**
* Custom WebSocket implementation for streaming transcription. Useful for
* runtimes that need a WebSocket constructor with header support (e.g. the
* `ws` package in Node.js, which Vertex's OAuth Bearer header requires).
*/
webSocket?: WebSocketConstructor;
/**
* Settings for downloading remote files in tool results before sending them
* to Vertex as inline data.
*/
toolResultDownloads?: {
/**
* Maximum size in bytes for each downloaded file. Defaults to 7 MiB.
*/
maxBytes?: number;
};
}
/**
* Create a Google Vertex AI provider instance.
*/
export function createGoogleVertex(
options: GoogleVertexProviderSettings = {},
): GoogleVertexProvider {
const apiKey =
typeof options.apiKey === 'string' || options.apiKey == null
? loadOptionalSetting({
settingValue: options.apiKey,
environmentVariableName: 'GOOGLE_VERTEX_API_KEY',
})
: options.apiKey;
const loadGoogleVertexProject = () =>
loadSetting({
settingValue: options.project,
settingName: 'project',
environmentVariableName: 'GOOGLE_VERTEX_PROJECT',
description: 'Google Vertex project',
});
const loadGoogleVertexLocation = () => {
const location = loadSetting({
settingValue: options.location,
settingName: 'location',
environmentVariableName: 'GOOGLE_VERTEX_LOCATION',
description: 'Google Vertex location',
});
if (!isValidHostnamePart(location)) {
throw new InvalidArgumentError({
argument: 'location',
message:
'Invalid Google Vertex location. Expected a single DNS label (letters, digits, and hyphens). Use `baseURL` for custom endpoints.',
});
}
return location;
};
// Tuned models are addressed via their deployed endpoint
// `.../locations/{region}/endpoints/{id}` instead of the base-model
// `.../publishers/google/models/{id}` path, so they omit the
// `/publishers/google` suffix from the base URL.
const loadBaseURL = ({ endpoint = false }: { endpoint?: boolean } = {}) => {
if (apiKey) {
return withoutTrailingSlash(options.baseURL) ?? EXPRESS_MODE_BASE_URL;
}
const baseURL = withoutTrailingSlash(options.baseURL);
if (baseURL != null) {
return baseURL;
}
const region = loadGoogleVertexLocation();
const project = loadGoogleVertexProject();
const getHost = () => {
if (region === 'global') {
return 'aiplatform.googleapis.com';
} else if (region === 'eu' || region === 'us') {
return `aiplatform.${region}.rep.googleapis.com`;
} else {
return `${region}-aiplatform.googleapis.com`;
}
};
return `https://${getHost()}/v1beta1/projects/${project}/locations/${region}${
endpoint ? '' : '/publishers/google'
}`;
};
const createConfig = (
name: string,
{ endpoint = false }: { endpoint?: boolean } = {},
): GoogleVertexConfig => {
const getHeaders = async () => {
const originalHeaders = await resolve(options.headers ?? {});
return withUserAgentSuffix(
originalHeaders,
`ai-sdk-google-vertex/${VERSION}`,
);
};
return {
provider: `google.vertex.${name}`,
headers: getHeaders,
fetch: apiKey
? createExpressModeFetch(apiKey, options.fetch)
: options.fetch,
baseURL: loadBaseURL({ endpoint }),
};
};
const createChatModel = (modelId: GoogleVertexModelId) => {
const endpoint = isEndpointModelId(modelId);
if (endpoint && apiKey) {
throw new Error(
'Google Vertex tuned models do not support Express Mode API keys. Use standard Google Cloud credentials instead.',
);
}
return new GoogleLanguageModel(modelId, {
...createConfig('chat', { endpoint }),
generateId: options.generateId ?? generateId,
supportedUrls: () => ({
'*': [
// HTTP URLs:
/^https?:\/\/.*$/,
// Google Cloud Storage URLs:
/^gs:\/\/.*$/,
],
}),
downloadToolResultFiles: {
maxBytes: options.toolResultDownloads?.maxBytes ?? 7 * 1024 * 1024,
supportsGoogleCloudStorageUrls: true,
fetch: options.fetch,
},
});
};
const createInteractionsModel = (
modelIdOrAgent:
| GoogleInteractionsModelId
| { agent: GoogleInteractionsAgentName }
| { managedAgent: string },
) => {
if (apiKey) {
throw new Error(
'Google Vertex Interactions models do not support Express Mode API keys. Use standard Google Cloud credentials instead.',
);
}
return new GoogleInteractionsLanguageModel(
modelIdOrAgent as GoogleInteractionsModelInput,
{
// The Interactions API is a location-scoped resource
// (`.../locations/{region}/interactions`), so it uses the
// endpoint-style base URL without the `/publishers/google` suffix that
// the base-model paths carry.
...createConfig('interactions', { endpoint: true }),
generateId: options.generateId ?? generateId,
},
);
};
const createEmbeddingModel = (modelId: GoogleVertexEmbeddingModelId) =>
new GoogleVertexEmbeddingModel(modelId, createConfig('embedding'));
const createImageModel = (modelId: GoogleVertexImageModelId) =>
new GoogleVertexImageModel(modelId, {
...createConfig('image'),
generateId: options.generateId ?? generateId,
});
const createVideoModel = (modelId: GoogleVertexVideoModelId) =>
new GoogleVertexVideoModel(modelId, {
...createConfig('video'),
generateId: options.generateId ?? generateId,
});
const createSpeechModel = (modelId: GoogleVertexSpeechModelId) => {
if (getGoogleVertexRequestConfig(modelId).speech?.api === 'cloud-tts') {
if (apiKey) {
throw new Error(
'Google Vertex Chirp speech models do not support Express Mode API keys. Use standard Google Cloud credentials instead.',
);
}
const config = createConfig('speech');
return new GoogleVertexCloudTTSSpeechModel(modelId, {
provider: config.provider,
headers: config.headers,
fetch: config.fetch,
});
}
return new GoogleSpeechModel(modelId, createConfig('speech'));
};
// Cloud Speech-to-Text reuses the Vertex auth headers from createConfig, but
// targets the Speech-to-Text API.
const createTranscriptionModel = (
modelId: GoogleVertexTranscriptionModelId,
) => {
if (apiKey) {
throw new Error(
'Google Vertex transcription models do not support Express Mode API keys. Use standard Google Cloud credentials instead.',
);
}
const config = createConfig('transcription');
// Gemini transcription models (`gemini-3.5-transcribe[-live]`) use the
// Vertex generateContent / Live API surfaces; everything else routes to
// Cloud Speech-to-Text (Chirp, telephony).
if (getGoogleVertexRequestConfig(modelId).transcription?.api === 'gemini') {
return new GoogleVertexGeminiTranscriptionModel(modelId, {
provider: config.provider,
baseURL: loadBaseURL(),
headers: config.headers,
fetch: config.fetch,
webSocket: options.webSocket,
project: loadGoogleVertexProject(),
location: loadGoogleVertexLocation(),
});
}
return new GoogleVertexTranscriptionModel(modelId, {
provider: config.provider,
headers: config.headers,
fetch: config.fetch,
project: loadGoogleVertexProject(),
location: loadGoogleVertexLocation(),
});
};
const provider = function (modelId: GoogleVertexModelId) {
if (new.target) {
throw new Error(
'The Google Vertex AI model function cannot be called with the new keyword.',
);
}
return createChatModel(modelId);
};
provider.specificationVersion = 'v4' as const;
provider.languageModel = createChatModel;
provider.interactions = createInteractionsModel;
provider.embeddingModel = createEmbeddingModel;
provider.textEmbeddingModel = createEmbeddingModel;
provider.image = createImageModel;
provider.imageModel = createImageModel;
provider.video = createVideoModel;
provider.videoModel = createVideoModel;
provider.speech = createSpeechModel;
provider.speechModel = createSpeechModel;
provider.transcription = createTranscriptionModel;
provider.transcriptionModel = createTranscriptionModel;
provider.tools = googleVertexTools;
return provider;
}