UNPKG

@ai-sdk/google-vertex

Version:
239 lines (216 loc) • 6.54 kB
import { getGoogleVertexRequestConfig } from './google-vertex-model-catalog-config'; import { getGoogleVertexImageModelEntry } from './google-vertex-model-catalog'; import type { GoogleLanguageModelOptions } from '@ai-sdk/google'; import { GoogleLanguageModel } from '@ai-sdk/google/internal'; import type { ImageModelV4, LanguageModelV4Prompt, SharedV4Warning, } from '@ai-sdk/provider'; import { convertToBase64, generateId as defaultGenerateId, serializeModelOptions, WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE, type Resolvable, } from '@ai-sdk/provider-utils'; import type { GoogleVertexImageModelId } from './google-vertex-image-settings'; interface GoogleVertexImageModelConfig { provider: string; baseURL: string; headers?: Resolvable<Record<string, string | undefined>>; fetch?: typeof fetch; generateId?: () => string; _internal?: { currentDate?: () => Date; }; } export class GoogleVertexImageModel implements ImageModelV4 { readonly specificationVersion = 'v4'; static [WORKFLOW_SERIALIZE](model: GoogleVertexImageModel) { return serializeModelOptions({ modelId: model.modelId, config: model.config, }); } static [WORKFLOW_DESERIALIZE](options: { modelId: string; config: GoogleVertexImageModelConfig; }) { return new GoogleVertexImageModel(options.modelId, options.config); } readonly maxImagesPerCall = 1; get supportsFileInputs(): boolean | undefined { return getGoogleVertexImageModelEntry(this.modelId).capabilities.image ?.fileInput === true ? true : undefined; } get supportsMaskInputs(): boolean | undefined { return this.supportsFileInputs === true ? false : undefined; } get provider(): string { return this.config.provider; } constructor( readonly modelId: GoogleVertexImageModelId, private config: GoogleVertexImageModelConfig, ) {} async doGenerate( options: Parameters<ImageModelV4['doGenerate']>[0], ): Promise<Awaited<ReturnType<ImageModelV4['doGenerate']>>> { if (!getGoogleVertexRequestConfig(this.modelId).image?.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: Array<SharedV4Warning> = []; 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: Array< | { type: 'text'; text: string } | { type: 'file'; data: | { type: 'data'; data: string | Uint8Array } | { type: 'url'; url: URL }; mediaType: string; } > = []; 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: LanguageModelV4Prompt = [ { role: 'user', content: userContent }, ]; const languageModel = new GoogleLanguageModel(this.modelId, { provider: this.config.provider, baseURL: this.config.baseURL, headers: this.config.headers ?? {}, fetch: this.config.fetch, generateId: this.config.generateId ?? defaultGenerateId, supportedUrls: () => ({ '*': [/^https?:\/\/.*$/, /^gs:\/\/.*$/], }), }); const { responseModalities: _strippedResponseModalities, imageConfig: userImageConfig, ...userVertexOptions } = ((providerOptions?.googleVertex ?? providerOptions?.vertex) as | GoogleLanguageModelOptions | undefined) ?? {}; const innerVertexOptions: GoogleLanguageModelOptions = { ...userVertexOptions, responseModalities: ['IMAGE'], imageConfig: aspectRatio != null || userImageConfig != null ? { ...userImageConfig, ...(aspectRatio != null ? { aspectRatio: aspectRatio as NonNullable< GoogleLanguageModelOptions['imageConfig'] >['aspectRatio'], } : {}), } : undefined, }; const result = await languageModel.doGenerate({ prompt: languageModelPrompt, seed, providerOptions: { googleVertex: innerVertexOptions, vertex: innerVertexOptions, }, headers, abortSignal, }); const currentDate = this.config._internal?.currentDate?.() ?? new Date(); const images: string[] = []; 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 geminiPayload = { images: images.map(() => ({})), }; return { images, ...(result.finishReason.unified === 'content-filter' ? { isRetryable: false } : {}), warnings, providerMetadata: { googleVertex: geminiPayload, vertex: geminiPayload, }, 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), } : undefined, }; } }