@ai-sdk/google-vertex
Version:
239 lines (216 loc) • 6.54 kB
text/typescript
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,
};
}
}