Skip to content

Commit 13b7683

Browse files
committed
Use union type instead of hard-coded GOOGLE_AI
1 parent d458724 commit 13b7683

File tree

9 files changed

+315
-9
lines changed

9 files changed

+315
-9
lines changed
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { GenAIError } from './errors';
19+
import { logger } from './logger';
20+
import {
21+
CitationMetadata,
22+
CountTokensRequest,
23+
GenerateContentCandidate,
24+
GenerateContentRequest,
25+
GenerateContentResponse,
26+
HarmSeverity,
27+
InlineDataPart,
28+
PromptFeedback,
29+
SafetyRating,
30+
GenAIErrorCode
31+
} from './types';
32+
import {
33+
GoogleAIGenerateContentResponse,
34+
GoogleAIGenerateContentCandidate,
35+
GoogleAICountTokensRequest
36+
} from './types/googleAI';
37+
38+
/**
39+
* This SDK supports both Vertex AI and Google AI APIs.
40+
* The public API prioritizes the Vertex AI API.
41+
* We avoid having two sets of types by translating requests and responses between the two API formats.
42+
* We want to avoid two sets of types so that developers can switch between Vertex AI and Google AI
43+
* with minimal changes to their code.
44+
*
45+
* In here are functions that map requests and responses between the two API formats.
46+
* VertexAI requests defined by the user are mapped to Google AI requests before they're sent.
47+
* Google AI responses are mapped to VertexAI responses so they can be returned to the user.
48+
*/
49+
50+
/**
51+
* Maps a Vertex AI {@link GenerateContentRequest} to a format that can be sent to Google AI.
52+
*
53+
* @param generateContentRequest The {@link GenerateContentRequest} to map.
54+
* @returns A {@link GenerateContentResponse} that conforms to the Google AI format.
55+
*
56+
* @throws If the request contains properties that are unsupported by Google AI.
57+
*/
58+
export function mapGenerateContentRequest(
59+
generateContentRequest: GenerateContentRequest
60+
): GenerateContentRequest {
61+
generateContentRequest.safetySettings?.forEach(safetySetting => {
62+
if (safetySetting.method) {
63+
throw new GenAIError(
64+
GenAIErrorCode.UNSUPPORTED,
65+
'SafetySetting.method is not supported in the Google AI. Please remove this property.'
66+
);
67+
}
68+
});
69+
70+
if (generateContentRequest.generationConfig?.topK) {
71+
logger.warn(
72+
'topK in GenerationConfig has been rounded to the nearest integer.'
73+
);
74+
generateContentRequest.generationConfig.topK = Math.round(
75+
generateContentRequest.generationConfig.topK
76+
);
77+
}
78+
79+
return generateContentRequest;
80+
}
81+
82+
/**
83+
* Maps a {@link GenerateContentResponse} from Google AI to the format of the
84+
* {@link GenerateContentResponse} that we get from VertexAI that is exposed in the public API.
85+
*
86+
* @param googleAIResponse The {@link GenerateContentResponse} from Google AI.
87+
* @returns A {@link GenerateContentResponse} that conforms to the public API's format.
88+
*/
89+
export function mapGenerateContentResponse(
90+
googleAIResponse: GoogleAIGenerateContentResponse
91+
): GenerateContentResponse {
92+
const generateContentResponse = {
93+
candidates: googleAIResponse.candidates
94+
? mapGenerateContentCandidates(googleAIResponse.candidates)
95+
: undefined,
96+
prompt: googleAIResponse.promptFeedback
97+
? mapPromptFeedback(googleAIResponse.promptFeedback)
98+
: undefined,
99+
usageMetadata: googleAIResponse.usageMetadata
100+
};
101+
102+
return generateContentResponse;
103+
}
104+
105+
/**
106+
* Maps a Vertex AI {@link CountTokensRequest} to a format that can be sent to Google AI.
107+
*
108+
* @param countTokensRequest The {@link CountTokensRequest} to map.
109+
* @returns A {@link CountTokensRequest} that conforms to the Google AI format.
110+
*/
111+
export function mapCountTokensRequest(
112+
countTokensRequest: CountTokensRequest,
113+
): GoogleAICountTokensRequest {
114+
const mappedCountTokensRequest: GoogleAICountTokensRequest = {
115+
generateContentRequest: {
116+
// model,
117+
contents: countTokensRequest.contents,
118+
systemInstruction: countTokensRequest.systemInstruction,
119+
tools: countTokensRequest.tools,
120+
generationConfig: countTokensRequest.generationConfig
121+
}
122+
};
123+
124+
return mappedCountTokensRequest;
125+
}
126+
127+
export function mapGenerateContentCandidates(
128+
candidates: GoogleAIGenerateContentCandidate[]
129+
): GenerateContentCandidate[] {
130+
const mappedCandidates: GenerateContentCandidate[] = [];
131+
if (mappedCandidates) {
132+
candidates.forEach(candidate => {
133+
// Map citationSources to citations.
134+
let citationMetadata: CitationMetadata | undefined;
135+
if (candidate.citationMetadata) {
136+
citationMetadata = {
137+
citations: candidate.citationMetadata.citationSources
138+
};
139+
}
140+
141+
// Assign missing candidate SafetyRatings properties to their defaults.
142+
if (candidate.safetyRatings) {
143+
logger.warn(
144+
"Candidate safety rating properties 'severity', 'severityScore', and 'probabilityScore' are not included in responses from Google AI. Properties have been assigned to default values."
145+
);
146+
candidate.safetyRatings.forEach(safetyRating => {
147+
safetyRating.severity = HarmSeverity.HARM_SEVERITY_UNSUPPORTED;
148+
safetyRating.probabilityScore = 0;
149+
safetyRating.severityScore = 0;
150+
});
151+
}
152+
153+
// videoMetadata is not supported.
154+
// Throw early since developers may send a long video as input and only expect to pay
155+
// for inference on a small portion of the video.
156+
if (
157+
candidate.content?.parts.some(
158+
part => (part as InlineDataPart)?.videoMetadata
159+
)
160+
) {
161+
throw new GenAIError(
162+
GenAIErrorCode.UNSUPPORTED,
163+
'Part.videoMetadata is not supported in Google AI. Please remove this property.'
164+
);
165+
}
166+
167+
const mappedCandidate = {
168+
index: candidate.index,
169+
content: candidate.content,
170+
finishReason: candidate.finishReason,
171+
finishMessage: candidate.finishMessage,
172+
safetyRatings: candidate.safetyRatings,
173+
citationMetadata,
174+
groundingMetadata: candidate.groundingMetadata
175+
};
176+
mappedCandidates.push(mappedCandidate);
177+
});
178+
}
179+
180+
return mappedCandidates;
181+
}
182+
183+
export function mapPromptFeedback(
184+
promptFeedback: PromptFeedback
185+
): PromptFeedback {
186+
// Assign missing PromptFeedback SafetyRatings properties to their defaults.
187+
const mappedSafetyRatings: SafetyRating[] = [];
188+
promptFeedback.safetyRatings.forEach(safetyRating => {
189+
mappedSafetyRatings.push({
190+
category: safetyRating.category,
191+
probability: safetyRating.probability,
192+
severity: HarmSeverity.HARM_SEVERITY_UNSUPPORTED,
193+
probabilityScore: 0,
194+
severityScore: 0,
195+
blocked: safetyRating.blocked
196+
});
197+
});
198+
logger.warn(
199+
"PromptFeedback safety ratings' properties severity, severityScore, and probabilityScore are not included in responses from Google AI. Properties have been assigned to default values."
200+
);
201+
202+
const mappedPromptFeedback: PromptFeedback = {
203+
blockReason: promptFeedback.blockReason,
204+
safetyRatings: mappedSafetyRatings,
205+
blockReasonMessage: promptFeedback.blockReasonMessage
206+
};
207+
return mappedPromptFeedback;
208+
}

packages/vertexai/src/methods/count-tokens.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
import { Task, makeRequest } from '../requests/request';
2424
import { ApiSettings } from '../types/internal';
2525
import * as GoogleAIMapper from '../googleAIMappers'; // FIXME: (code smell) Is there a better way to namespace this?
26+
import { BackendType } from '../public-types';
2627

2728
export async function countTokens(
2829
apiSettings: ApiSettings,
@@ -31,7 +32,7 @@ export async function countTokens(
3132
requestOptions?: RequestOptions
3233
): Promise<CountTokensResponse> {
3334
let body: string = '';
34-
if (apiSettings.backend.backendType === 'GOOGLE_AI') {
35+
if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) {
3536
const mappedParams = GoogleAIMapper.mapCountTokensRequest(
3637
params,
3738
);

packages/vertexai/src/methods/generate-content.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@ import { createEnhancedContentResponse } from '../requests/response-helpers';
2727
import { processStream } from '../requests/stream-reader';
2828
import { ApiSettings } from '../types/internal';
2929
import * as GoogleAIMapper from '../googleAIMappers'; // FIXME: (code smell) Is there a better way to namespace this?
30+
import { BackendType } from '../public-types';
3031

3132
export async function generateContentStream(
3233
apiSettings: ApiSettings,
3334
model: string,
3435
params: GenerateContentRequest,
3536
requestOptions?: RequestOptions
3637
): Promise<GenerateContentStreamResult> {
37-
if (apiSettings.backend.backendType === 'GOOGLE_AI') {
38+
if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) {
3839
params = GoogleAIMapper.mapGenerateContentRequest(params);
3940
}
4041
const response = await makeRequest(
@@ -54,7 +55,7 @@ export async function generateContent(
5455
params: GenerateContentRequest,
5556
requestOptions?: RequestOptions
5657
): Promise<GenerateContentResult> {
57-
if (apiSettings.backend.backendType === 'GOOGLE_AI') {
58+
if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) {
5859
params = GoogleAIMapper.mapGenerateContentRequest(params);
5960
}
6061
const response = await makeRequest(
@@ -82,7 +83,7 @@ async function handleGenerateContentResponse(
8283
apiSettings: ApiSettings
8384
): Promise<GenerateContentResponse> {
8485
const responseJson = await response.json();
85-
if (apiSettings.backend.backendType === 'GOOGLE_AI') {
86+
if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) {
8687
return GoogleAIMapper.mapGenerateContentResponse(responseJson);
8788
} else {
8889
return responseJson;

packages/vertexai/src/models/genai-model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export abstract class GenAIModel {
113113
modelName: string,
114114
backendType: BackendType
115115
): string {
116-
if (backendType === 'GOOGLE_AI') {
116+
if (backendType === BackendType.GOOGLE_AI) {
117117
return GenAIModel.normalizeGoogleAIModelName(modelName);
118118
} else {
119119
return GenAIModel.normalizeVertexAIModelName(modelName);

packages/vertexai/src/requests/request.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
PACKAGE_VERSION
2828
} from '../constants';
2929
import { logger } from '../logger';
30+
import { BackendType } from '../public-types';
3031

3132
export enum Task {
3233
GENERATE_CONTENT = 'generateContent',
@@ -47,7 +48,7 @@ export class RequestUrl {
4748
// TODO: allow user-set option if that feature becomes available
4849
const apiVersion = DEFAULT_API_VERSION;
4950
let url = `${this.getBaseUrl()}/${apiVersion}`;
50-
if (this.apiSettings.backend.backendType === 'GOOGLE_AI') {
51+
if (this.apiSettings.backend.backendType === BackendType.GOOGLE_AI) {
5152
url += `/${this.model}:${this.task}`;
5253
} else {
5354
url += `/projects/${this.apiSettings.project}/locations/${this.apiSettings.location}/${this.model}:${this.task}`;
@@ -59,7 +60,7 @@ export class RequestUrl {
5960
}
6061

6162
private getBaseUrl(): string {
62-
return this.apiSettings.backend.backendType === 'GOOGLE_AI'
63+
return this.apiSettings.backend.backendType === BackendType.GOOGLE_AI
6364
? this.requestOptions?.baseUrl || DEVELOPER_API_BASE_URL
6465
: this.requestOptions?.baseUrl || DEFAULT_BASE_URL;
6566
}

packages/vertexai/src/requests/stream-reader.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { createEnhancedContentResponse } from './response-helpers';
2828
import * as GoogleAIMapper from '../googleAIMappers';
2929
import { GoogleAIGenerateContentResponse } from '../types/googleAI';
3030
import { ApiSettings } from '../types/internal';
31+
import { BackendType } from '../public-types';
3132

3233
const responseLineRE = /^data\: (.*)(?:\n\n|\r\r|\r\n\r\n)/;
3334

@@ -65,7 +66,7 @@ async function getResponsePromise(
6566
const { done, value } = await reader.read();
6667
if (done) {
6768
let generateContentResponse = aggregateResponses(allResponses);
68-
if (apiSettings.backend.backendType === 'GOOGLE_AI') {
69+
if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) {
6970
generateContentResponse = GoogleAIMapper.mapGenerateContentResponse(
7071
generateContentResponse as GoogleAIGenerateContentResponse
7172
);
@@ -89,7 +90,7 @@ async function* generateResponseSequence(
8990
}
9091

9192
const enhancedResponse =
92-
apiSettings.backend.backendType === 'GOOGLE_AI'
93+
apiSettings.backend.backendType === BackendType.GOOGLE_AI
9394
? createEnhancedContentResponse(
9495
GoogleAIMapper.mapGenerateContentResponse(
9596
value as GoogleAIGenerateContentResponse
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
export * from './requests';
19+
export * from './responses';
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { Content, Part } from '../content';
19+
import { GenerationConfig, Tool } from '../requests';
20+
21+
export interface GoogleAICountTokensRequest {
22+
generateContentRequest: {
23+
// model: string; // models/model-name
24+
contents: Content[];
25+
systemInstruction?: string | Part | Content;
26+
tools?: Tool[];
27+
generationConfig?: GenerationConfig;
28+
};
29+
}

0 commit comments

Comments
 (0)