Skip to content

Commit f18da1f

Browse files
authored
feat(js/plugins/google-genai): Added Veo and Lyria (#3364)
1 parent 46594d2 commit f18da1f

File tree

21 files changed

+3045
-1125
lines changed

21 files changed

+3045
-1125
lines changed

js/ai/src/formats/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import type { JSONSchema } from '@genkit-ai/core';
18-
import type { GenerateResponseChunk } from '../generate.js';
18+
import type { GenerateResponseChunk } from '../generate/chunk.js';
1919
import type { Message } from '../message.js';
2020
import type { ModelRequest } from '../model.js';
2121

js/ai/src/generate/action.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ import {
3333
import type { Formatter } from '../formats/types.js';
3434
import {
3535
GenerateResponse,
36-
GenerateResponseChunk,
3736
GenerationResponseError,
3837
tagAsPreamble,
3938
} from '../generate.js';
39+
import { GenerateResponseChunk } from '../generate/chunk.js';
4040
import {
4141
GenerateActionOptionsSchema,
4242
GenerateResponseChunkSchema,

js/plugins/google-genai/src/common/types.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export enum FunctionCallingMode {
4040
}
4141

4242
/**
43-
* The reason why the reponse is blocked.
43+
* The reason why the response is blocked.
4444
*/
4545
export enum BlockReason {
4646
/** Unspecified block reason. */
@@ -156,7 +156,7 @@ export declare interface GroundingSupport {
156156
/** Optional. Segment of the content this support belongs to. */
157157
segment?: GroundingSupportSegment;
158158
/**
159-
* Optional. A arrau of indices (into {@link GroundingChunk}) specifying the
159+
* Optional. A array of indices (into {@link GroundingChunk}) specifying the
160160
* citations associated with the claim. For instance [1,3,4] means
161161
* that grounding_chunk[1], grounding_chunk[3],
162162
* grounding_chunk[4] are the retrieved content attributed to the claim.
@@ -441,7 +441,7 @@ export declare interface GoogleDate {
441441
year?: number;
442442
/**
443443
* Month of the date. Must be from 1 to 12, or 0 to specify a year without a
444-
* monthi and day.
444+
* month and day.
445445
*/
446446
month?: number;
447447
/**
@@ -983,7 +983,7 @@ export declare interface GenerateContentRequest {
983983

984984
/**
985985
* Result from calling generateContentStream.
986-
* It constains both the stream and the final aggregated response.
986+
* It contains both the stream and the final aggregated response.
987987
* @public
988988
*/
989989
export declare interface GenerateContentStreamResult {

js/plugins/google-genai/src/common/utils.ts

Lines changed: 112 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ import {
1818
EmbedderReference,
1919
GenkitError,
2020
JSONSchema,
21+
MediaPart,
2122
ModelReference,
23+
Part,
2224
z,
2325
} from 'genkit';
2426
import { GenerateRequest } from 'genkit/model';
25-
import { ImagenInstance } from './types';
2627

2728
/**
2829
* Safely extracts the error message from the error.
@@ -64,8 +65,9 @@ export function extractVersion(
6465
export function modelName(name?: string): string | undefined {
6566
if (!name) return name;
6667

67-
// Remove any of these prefixes: (but keep tunedModels e.g.)
68-
const prefixesToRemove = /models\/|embedders\/|googleai\/|vertexai\//g;
68+
// Remove any of these prefixes:
69+
const prefixesToRemove =
70+
/background-model\/|model\/|models\/|embedders\/|googleai\/|vertexai\//g;
6971
return name.replace(prefixesToRemove, '');
7072
}
7173

@@ -95,20 +97,114 @@ export function extractText(request: GenerateRequest) {
9597
);
9698
}
9799

98-
export function extractImagenImage(
99-
request: GenerateRequest
100-
): ImagenInstance['image'] | undefined {
101-
const image = request.messages
102-
.at(-1)
103-
?.content.find(
104-
(p) => !!p.media && (!p.metadata?.type || p.metadata?.type === 'base')
105-
)
106-
?.media?.url.split(',')[1];
107-
108-
if (image) {
109-
return { bytesBase64Encoded: image };
100+
const KNOWN_MIME_TYPES = {
101+
jpg: 'image/jpeg',
102+
jpeg: 'image/jpeg',
103+
png: 'image/png',
104+
mp4: 'video/mp4',
105+
pdf: 'application/pdf',
106+
};
107+
108+
export function extractMimeType(url?: string): string {
109+
if (!url) {
110+
return '';
111+
}
112+
113+
const dataPrefix = 'data:';
114+
if (!url.startsWith(dataPrefix)) {
115+
// Not a data url, try suffix
116+
url.lastIndexOf('.');
117+
const key = url.substring(url.lastIndexOf('.') + 1);
118+
if (Object.keys(KNOWN_MIME_TYPES).includes(key)) {
119+
return KNOWN_MIME_TYPES[key];
120+
}
121+
return '';
122+
}
123+
124+
const commaIndex = url.indexOf(',');
125+
if (commaIndex == -1) {
126+
// Invalid - missing separator
127+
return '';
128+
}
129+
130+
// The part between 'data:' and the comma
131+
let mimeType = url.substring(dataPrefix.length, commaIndex);
132+
const base64Marker = ';base64';
133+
if (mimeType.endsWith(base64Marker)) {
134+
mimeType = mimeType.substring(0, mimeType.length - base64Marker.length);
135+
}
136+
137+
return mimeType.trim();
138+
}
139+
140+
export function checkSupportedMimeType(
141+
media: MediaPart['media'],
142+
supportedTypes: string[]
143+
) {
144+
if (!supportedTypes.includes(media.contentType ?? '')) {
145+
throw new GenkitError({
146+
status: 'INVALID_ARGUMENT',
147+
message: `Invalid mimeType for ${displayUrl(media.url)}: "${media.contentType}". Supported mimeTypes: ${supportedTypes.join(', ')}`,
148+
});
149+
}
150+
}
151+
152+
/**
153+
*
154+
* @param url The url to show (e.g. in an error message)
155+
* @returns The appropriately sized url
156+
*/
157+
export function displayUrl(url: string): string {
158+
if (url.length <= 50) {
159+
return url;
110160
}
111-
return undefined;
161+
162+
return url.substring(0, 25) + '...' + url.substring(url.length - 25);
163+
}
164+
165+
/**
166+
*
167+
* @param request A generate request to extract from
168+
* @param metadataType The media must have metadata matching this type if isDefault is false
169+
* @param isDefault 'true' allows missing metadata type to match as well.
170+
* @returns
171+
*/
172+
export function extractMedia(
173+
request: GenerateRequest,
174+
params: {
175+
metadataType?: string;
176+
/* Is there is no metadata type, it will match if isDefault is true */
177+
isDefault?: boolean;
178+
}
179+
): MediaPart['media'] | undefined {
180+
const predicate = (part: Part) => {
181+
const media = part.media;
182+
if (!media) {
183+
return false;
184+
}
185+
if (params.metadataType || params.isDefault) {
186+
// We need to check the metadata type
187+
const metadata = part.metadata;
188+
if (!metadata?.type) {
189+
return !!params.isDefault;
190+
} else {
191+
return metadata.type == params.metadataType;
192+
}
193+
}
194+
return true;
195+
};
196+
197+
const media = request.messages.at(-1)?.content.find(predicate)?.media;
198+
199+
// Add the mimeType
200+
if (media && !media?.contentType) {
201+
return {
202+
url: media.url,
203+
contentType: extractMimeType(media.url),
204+
};
205+
}
206+
207+
return media;
112208
}
113209

114210
/**

js/plugins/google-genai/src/googleai/utils.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@
1616

1717
import { GenerateRequest, GenkitError } from 'genkit';
1818
import process from 'process';
19-
import { VeoImage } from './types.js';
19+
import { extractMedia } from '../common/utils.js';
20+
import { ImagenInstance, VeoImage } from './types.js';
2021

2122
export {
2223
checkModelName,
2324
cleanSchema,
24-
extractImagenImage,
2525
extractText,
2626
extractVersion,
2727
modelName,
@@ -143,3 +143,17 @@ export function extractVeoImage(
143143
}
144144
return undefined;
145145
}
146+
147+
export function extractImagenImage(
148+
request: GenerateRequest
149+
): ImagenInstance['image'] | undefined {
150+
const image = extractMedia(request, {
151+
metadataType: 'base',
152+
isDefault: true,
153+
})?.url.split(',')[1];
154+
155+
if (image) {
156+
return { bytesBase64Encoded: image };
157+
}
158+
return undefined;
159+
}

js/plugins/google-genai/src/vertexai/client.ts

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,14 @@ import {
3131
ImagenPredictRequest,
3232
ImagenPredictResponse,
3333
ListModelsResponse,
34+
LyriaPredictRequest,
35+
LyriaPredictResponse,
3436
Model,
37+
VeoOperation,
38+
VeoOperationRequest,
39+
VeoPredictRequest,
3540
} from './types';
36-
import { calculateApiKey, checkIsSupported } from './utils';
41+
import { calculateApiKey, checkSupportedResourceMethod } from './utils';
3742

3843
export async function listModels(
3944
clientOptions: ClientOptions
@@ -94,25 +99,37 @@ export async function generateContentStream(
9499
return processStream(response);
95100
}
96101

97-
export async function embedContent(
102+
async function internalPredict(
98103
model: string,
99-
embedContentRequest: EmbedContentRequest,
104+
body: string,
100105
clientOptions: ClientOptions
101-
): Promise<EmbedContentResponse> {
106+
): Promise<Response> {
102107
const url = getVertexAIUrl({
103108
includeProjectAndLocation: true,
104109
resourcePath: `publishers/google/models/${model}`,
105-
resourceMethod: 'predict', // embedContent is a Vertex API predict call
110+
resourceMethod: 'predict',
106111
clientOptions,
107112
});
108113

109114
const fetchOptions = await getFetchOptions({
110115
method: 'POST',
111116
clientOptions,
112-
body: JSON.stringify(embedContentRequest),
117+
body,
113118
});
114119

115-
const response = await makeRequest(url, fetchOptions);
120+
return await makeRequest(url, fetchOptions);
121+
}
122+
123+
export async function embedContent(
124+
model: string,
125+
embedContentRequest: EmbedContentRequest,
126+
clientOptions: ClientOptions
127+
): Promise<EmbedContentResponse> {
128+
const response = await internalPredict(
129+
model,
130+
JSON.stringify(embedContentRequest),
131+
clientOptions
132+
);
116133
return response.json() as Promise<EmbedContentResponse>;
117134
}
118135

@@ -121,31 +138,78 @@ export async function imagenPredict(
121138
imagenPredictRequest: ImagenPredictRequest,
122139
clientOptions: ClientOptions
123140
): Promise<ImagenPredictResponse> {
141+
const response = await internalPredict(
142+
model,
143+
JSON.stringify(imagenPredictRequest),
144+
clientOptions
145+
);
146+
return response.json() as Promise<ImagenPredictResponse>;
147+
}
148+
149+
export async function lyriaPredict(
150+
model: string,
151+
lyriaPredictRequest: LyriaPredictRequest,
152+
clientOptions: ClientOptions
153+
): Promise<LyriaPredictResponse> {
154+
const response = await internalPredict(
155+
model,
156+
JSON.stringify(lyriaPredictRequest),
157+
clientOptions
158+
);
159+
return response.json() as Promise<LyriaPredictResponse>;
160+
}
161+
162+
export async function veoPredict(
163+
model: string,
164+
veoPredictRequest: VeoPredictRequest,
165+
clientOptions: ClientOptions
166+
): Promise<VeoOperation> {
124167
const url = getVertexAIUrl({
125168
includeProjectAndLocation: true,
126169
resourcePath: `publishers/google/models/${model}`,
127-
resourceMethod: 'predict',
170+
resourceMethod: 'predictLongRunning',
128171
clientOptions,
129172
});
130173

131174
const fetchOptions = await getFetchOptions({
132175
method: 'POST',
133176
clientOptions,
134-
body: JSON.stringify(imagenPredictRequest),
177+
body: JSON.stringify(veoPredictRequest),
135178
});
136179

137180
const response = await makeRequest(url, fetchOptions);
138-
return response.json() as Promise<ImagenPredictResponse>;
181+
return response.json() as Promise<VeoOperation>;
182+
}
183+
184+
export async function veoCheckOperation(
185+
model: string,
186+
veoOperationRequest: VeoOperationRequest,
187+
clientOptions: ClientOptions
188+
): Promise<VeoOperation> {
189+
const url = getVertexAIUrl({
190+
includeProjectAndLocation: true,
191+
resourcePath: `publishers/google/models/${model}`,
192+
resourceMethod: 'fetchPredictOperation',
193+
clientOptions,
194+
});
195+
const fetchOptions = await getFetchOptions({
196+
method: 'POST',
197+
clientOptions,
198+
body: JSON.stringify(veoOperationRequest),
199+
});
200+
201+
const response = await makeRequest(url, fetchOptions);
202+
return response.json() as Promise<VeoOperation>;
139203
}
140204

141205
export function getVertexAIUrl(params: {
142206
includeProjectAndLocation: boolean; // False for listModels, true for most others
143207
resourcePath: string;
144-
resourceMethod?: 'streamGenerateContent' | 'generateContent' | 'predict';
208+
resourceMethod?: string;
145209
queryParams?: string;
146210
clientOptions: ClientOptions;
147211
}): string {
148-
checkIsSupported(params);
212+
checkSupportedResourceMethod(params);
149213

150214
const DEFAULT_API_VERSION = 'v1beta1';
151215
const API_BASE_PATH = 'aiplatform.googleapis.com';

0 commit comments

Comments
 (0)