@ai-sdk/google-vertex
Version:
233 lines (212 loc) • 6.51 kB
text/typescript
import { getGoogleVertexRequestConfig } from './google-vertex-model-catalog-config';
import {
TooManyEmbeddingValuesForCallError,
type EmbeddingModelV4,
} from '@ai-sdk/provider';
import {
lazySchema,
zodSchema,
combineHeaders,
createJsonResponseHandler,
postJsonToApi,
resolve,
parseProviderOptions,
serializeModelOptions,
WORKFLOW_SERIALIZE,
WORKFLOW_DESERIALIZE,
} from '@ai-sdk/provider-utils';
import { z } from 'zod/v4';
import { googleVertexFailedResponseHandler } from './google-vertex-error';
import {
googleVertexEmbeddingModelOptions,
type GoogleVertexEmbeddingModelId,
} from './google-vertex-embedding-model-options';
import type { GoogleVertexConfig } from './google-vertex-config';
export class GoogleVertexEmbeddingModel implements EmbeddingModelV4 {
readonly specificationVersion = 'v4';
readonly modelId: GoogleVertexEmbeddingModelId;
readonly supportsParallelCalls = true;
private readonly config: GoogleVertexConfig;
static [WORKFLOW_SERIALIZE](model: GoogleVertexEmbeddingModel) {
return serializeModelOptions({
modelId: model.modelId,
config: model.config,
});
}
static [WORKFLOW_DESERIALIZE](options: {
modelId: string;
config: GoogleVertexConfig;
}) {
return new GoogleVertexEmbeddingModel(options.modelId, options.config);
}
get provider(): string {
return this.config.provider;
}
// gemini-embedding-2 models only support :embedContent (one value per call),
// not the :predict batch endpoint. https://github.com/vercel/ai/issues/15853
get maxEmbeddingsPerCall(): number {
return usesEmbedContentEndpoint(this.modelId) ? 1 : 250;
}
constructor(
modelId: GoogleVertexEmbeddingModelId,
config: GoogleVertexConfig,
) {
this.modelId = modelId;
this.config = config;
}
async doEmbed({
values,
dimensions,
headers,
abortSignal,
providerOptions,
}: Parameters<EmbeddingModelV4['doEmbed']>[0]): Promise<
Awaited<ReturnType<EmbeddingModelV4['doEmbed']>>
> {
let googleOptions = await parseProviderOptions({
provider: 'googleVertex',
providerOptions,
schema: googleVertexEmbeddingModelOptions,
});
if (googleOptions == null) {
googleOptions = await parseProviderOptions({
provider: 'vertex',
providerOptions,
schema: googleVertexEmbeddingModelOptions,
});
}
if (googleOptions == null) {
googleOptions = await parseProviderOptions({
provider: 'google',
providerOptions,
schema: googleVertexEmbeddingModelOptions,
});
}
googleOptions = googleOptions ?? {};
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) : undefined,
headers,
);
if (usesEmbedContentEndpoint(this.modelId)) {
const {
responseHeaders,
value: response,
rawValue,
} = await postJsonToApi({
url: `${this.config.baseURL}/models/${this.modelId}:embedContent`,
headers: mergedHeaders,
body: {
content: { parts: [{ text: values[0] }] },
embedContentConfig: {
outputDimensionality:
googleOptions.outputDimensionality ?? dimensions,
taskType: googleOptions.taskType,
title: googleOptions.title,
autoTruncate: googleOptions.autoTruncate,
},
},
failedResponseHandler: googleVertexFailedResponseHandler,
successfulResponseHandler: createJsonResponseHandler(
googleVertexEmbedContentResponseSchema,
),
abortSignal,
fetch: this.config.fetch,
});
return {
warnings: [],
embeddings: [response.embedding.values],
usage:
response.usageMetadata?.promptTokenCount == null
? undefined
: { tokens: response.usageMetadata.promptTokenCount },
response: { headers: responseHeaders, body: rawValue },
};
}
const url = `${this.config.baseURL}/models/${this.modelId}:predict`;
const {
responseHeaders,
value: response,
rawValue,
} = await postJsonToApi({
url,
headers: mergedHeaders,
body: {
instances: values.map(value => ({
content: value,
task_type: googleOptions.taskType,
title: googleOptions.title,
})),
parameters: {
outputDimensionality:
googleOptions.outputDimensionality ?? dimensions,
autoTruncate: googleOptions.autoTruncate,
},
},
failedResponseHandler: googleVertexFailedResponseHandler,
successfulResponseHandler: createJsonResponseHandler(
googleVertexTextEmbeddingResponseSchema,
),
abortSignal,
fetch: this.config.fetch,
});
return {
warnings: [],
embeddings: response.predictions.map(
prediction => prediction.embeddings.values,
),
usage: {
tokens: response.predictions.reduce(
(tokenCount, prediction) =>
tokenCount + prediction.embeddings.statistics.token_count,
0,
),
},
response: { headers: responseHeaders, body: rawValue },
};
}
}
// minimal version of the schema, focussed on what is needed for the implementation
// this approach limits breakages when the API changes and increases efficiency
const googleVertexTextEmbeddingResponseSchema = lazySchema(() =>
zodSchema(
z.object({
predictions: z.array(
z.object({
embeddings: z.object({
values: z.array(z.number()),
statistics: z.object({
token_count: z.number(),
}),
}),
}),
),
}),
),
);
const googleVertexEmbedContentResponseSchema = lazySchema(() =>
zodSchema(
z.object({
embedding: z.object({
values: z.array(z.number()),
}),
usageMetadata: z
.object({
promptTokenCount: z.number().nullish(),
})
.nullish(),
}),
),
);
function usesEmbedContentEndpoint(modelId: GoogleVertexEmbeddingModelId) {
return (
getGoogleVertexRequestConfig(modelId).embedding?.endpoint === 'embedContent'
);
}