Skip to content

Commit b144164

Browse files
authored
Vertex base url update (#28145)
1 parent 8cd5c0f commit b144164

4 files changed

Lines changed: 203 additions & 1 deletion

File tree

packages/cli/src/ui/hooks/useQuotaAndFallback.test.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,102 @@ Your admin might have disabled the access. Contact them to enable the Preview Re
525525
expect(result.current.proQuotaRequest).toBeNull();
526526
});
527527

528+
it('should handle ModelNotFoundError with Vertex AI by displaying region-specific availability message and documentation link', async () => {
529+
vi.spyOn(mockConfig, 'getContentGeneratorConfig').mockReturnValue({
530+
authType: AuthType.USE_VERTEX_AI,
531+
});
532+
vi.stubEnv('GOOGLE_CLOUD_LOCATION', 'us-central1');
533+
534+
const { result } = await renderHook(() =>
535+
useQuotaAndFallback({
536+
config: mockConfig,
537+
historyManager: mockHistoryManager,
538+
userTier: UserTierId.FREE,
539+
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
540+
onShowAuthSelection: mockOnShowAuthSelection,
541+
paidTier: null,
542+
settings: mockSettings,
543+
}),
544+
);
545+
546+
const handler = setFallbackHandlerSpy.mock
547+
.calls[0][0] as FallbackModelHandler;
548+
549+
let promise: Promise<FallbackIntent | null>;
550+
const error = new ModelNotFoundError('model not found', 404);
551+
552+
act(() => {
553+
promise = handler('gemini-3.5-flash', 'gemini-1.5-flash', error);
554+
});
555+
556+
const request = result.current.proQuotaRequest;
557+
expect(request).not.toBeNull();
558+
expect(request?.failedModel).toBe('gemini-3.5-flash');
559+
expect(request?.isModelNotFoundError).toBe(true);
560+
561+
const message = request!.message;
562+
expect(message).toBe(
563+
`Model "gemini-3.5-flash" is not available in region "us-central1".\n` +
564+
`To see which models are available in this region, please visit:\n` +
565+
`https://cloud.google.com/vertex-ai/generative-ai/docs/learn/locations\n` +
566+
`/model to switch models.`,
567+
);
568+
569+
act(() => {
570+
result.current.handleProQuotaChoice('retry_always');
571+
});
572+
573+
const intent = await promise!;
574+
expect(intent).toBe('retry_always');
575+
});
576+
577+
it('should handle ModelNotFoundError with Vertex AI and invalid model by displaying generic not found error message', async () => {
578+
vi.spyOn(mockConfig, 'getContentGeneratorConfig').mockReturnValue({
579+
authType: AuthType.USE_VERTEX_AI,
580+
});
581+
vi.stubEnv('GOOGLE_CLOUD_LOCATION', 'us-central1');
582+
583+
const { result } = await renderHook(() =>
584+
useQuotaAndFallback({
585+
config: mockConfig,
586+
historyManager: mockHistoryManager,
587+
userTier: UserTierId.FREE,
588+
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
589+
onShowAuthSelection: mockOnShowAuthSelection,
590+
paidTier: null,
591+
settings: mockSettings,
592+
}),
593+
);
594+
595+
const handler = setFallbackHandlerSpy.mock
596+
.calls[0][0] as FallbackModelHandler;
597+
598+
let promise: Promise<FallbackIntent | null>;
599+
const error = new ModelNotFoundError('model not found', 404);
600+
601+
act(() => {
602+
promise = handler('invalid-model-name', 'gemini-1.5-flash', error);
603+
});
604+
605+
const request = result.current.proQuotaRequest;
606+
expect(request).not.toBeNull();
607+
expect(request?.failedModel).toBe('invalid-model-name');
608+
expect(request?.isModelNotFoundError).toBe(true);
609+
610+
const message = request!.message;
611+
expect(message).toBe(
612+
`Model "invalid-model-name" was not found or is invalid.\n` +
613+
`/model to switch models.`,
614+
);
615+
616+
act(() => {
617+
result.current.handleProQuotaChoice('retry_always');
618+
});
619+
620+
const intent = await promise!;
621+
expect(intent).toBe('retry_always');
622+
});
623+
528624
it('should handle ModelNotFoundError with invalid model correctly', async () => {
529625
const { result } = await renderHook(() =>
530626
useQuotaAndFallback({

packages/cli/src/ui/hooks/useQuotaAndFallback.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,20 @@ export function useQuotaAndFallback({
135135
message = messageLines.join('\n');
136136
} else if (error instanceof ModelNotFoundError) {
137137
isModelNotFoundError = true;
138-
if (VALID_GEMINI_MODELS.has(failedModel)) {
138+
if (
139+
contentGeneratorConfig?.authType === AuthType.USE_VERTEX_AI &&
140+
VALID_GEMINI_MODELS.has(failedModel)
141+
) {
142+
const location =
143+
process.env['GOOGLE_CLOUD_LOCATION'] || 'your configured region';
144+
const messageLines = [
145+
`Model "${failedModel}" is not available in region "${location}".`,
146+
`To see which models are available in this region, please visit:`,
147+
`https://cloud.google.com/vertex-ai/generative-ai/docs/learn/locations`,
148+
`/model to switch models.`,
149+
];
150+
message = messageLines.join('\n');
151+
} else if (VALID_GEMINI_MODELS.has(failedModel)) {
139152
const messageLines = [
140153
`It seems like you don't have access to ${getDisplayString(failedModel)}.`,
141154
`Your admin might have disabled the access. Contact them to enable the Preview Release Channel.`,

packages/core/src/core/contentGenerator.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ describe('createContentGenerator', () => {
9393
resetVersionCache();
9494
vi.clearAllMocks();
9595
vi.stubEnv('ANTIGRAVITY_CLI_ALIAS', '');
96+
vi.stubEnv('GOOGLE_CLOUD_LOCATION', '');
9697
});
9798

9899
afterEach(() => {
@@ -483,6 +484,82 @@ describe('createContentGenerator', () => {
483484
);
484485
});
485486

487+
it('should use US REP endpoint for Vertex AI when location is us and no baseUrl is provided', async () => {
488+
const mockConfig = {
489+
getModel: vi.fn().mockReturnValue('gemini-pro'),
490+
getProxy: vi.fn().mockReturnValue(undefined),
491+
getUsageStatisticsEnabled: () => false,
492+
getClientName: vi.fn().mockReturnValue(undefined),
493+
} as unknown as Config;
494+
495+
const mockGenerator = {
496+
models: {},
497+
} as unknown as GoogleGenAI;
498+
vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator as never);
499+
500+
vi.stubEnv('GOOGLE_CLOUD_LOCATION', 'us');
501+
502+
await createContentGenerator(
503+
{
504+
apiKey: 'test-api-key',
505+
vertexai: true,
506+
authType: AuthType.USE_VERTEX_AI,
507+
},
508+
mockConfig,
509+
);
510+
511+
expect(GoogleGenAI).toHaveBeenCalledWith(
512+
expect.objectContaining({
513+
googleAuthOptions: expect.objectContaining({
514+
clientOptions: expect.objectContaining({
515+
apiEndpoint: 'https://aiplatform.us.rep.googleapis.com',
516+
}),
517+
}),
518+
httpOptions: expect.objectContaining({
519+
baseUrl: 'https://aiplatform.us.rep.googleapis.com',
520+
}),
521+
}),
522+
);
523+
});
524+
525+
it('should use EU REP endpoint for Vertex AI when location is eu and no baseUrl is provided', async () => {
526+
const mockConfig = {
527+
getModel: vi.fn().mockReturnValue('gemini-pro'),
528+
getProxy: vi.fn().mockReturnValue(undefined),
529+
getUsageStatisticsEnabled: () => false,
530+
getClientName: vi.fn().mockReturnValue(undefined),
531+
} as unknown as Config;
532+
533+
const mockGenerator = {
534+
models: {},
535+
} as unknown as GoogleGenAI;
536+
vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator as never);
537+
538+
vi.stubEnv('GOOGLE_CLOUD_LOCATION', 'eu');
539+
540+
await createContentGenerator(
541+
{
542+
apiKey: 'test-api-key',
543+
vertexai: true,
544+
authType: AuthType.USE_VERTEX_AI,
545+
},
546+
mockConfig,
547+
);
548+
549+
expect(GoogleGenAI).toHaveBeenCalledWith(
550+
expect.objectContaining({
551+
googleAuthOptions: expect.objectContaining({
552+
clientOptions: expect.objectContaining({
553+
apiEndpoint: 'https://aiplatform.eu.rep.googleapis.com',
554+
}),
555+
}),
556+
httpOptions: expect.objectContaining({
557+
baseUrl: 'https://aiplatform.eu.rep.googleapis.com',
558+
}),
559+
}),
560+
);
561+
});
562+
486563
it('should inject HttpsProxyAgent into googleAuthOptions when proxy URL uses https://', async () => {
487564
const mockConfigWithProxy = {
488565
getModel: vi.fn().mockReturnValue('gemini-pro'),

packages/core/src/core/contentGenerator.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,15 @@ const VERTEX_AI_REQUEST_TYPE_HEADER = 'X-Vertex-AI-LLM-Request-Type';
121121
const VERTEX_AI_SHARED_REQUEST_TYPE_HEADER =
122122
'X-Vertex-AI-LLM-Shared-Request-Type';
123123

124+
/**
125+
* Vertex AI Representative Endpoints (REP) for US and EU multi-regions.
126+
* These are used as a workaround for the client dynamically
127+
* constructing default legacy hostnames (e.g., 'us-aiplatform.googleapis.com')
128+
* instead of routing to the official REP endpoints.
129+
*/
130+
const VERTEX_AI_US_REP_ENDPOINT = 'https://aiplatform.us.rep.googleapis.com';
131+
const VERTEX_AI_EU_REP_ENDPOINT = 'https://aiplatform.eu.rep.googleapis.com';
132+
124133
function validateBaseUrl(baseUrl: string): void {
125134
try {
126135
new URL(baseUrl);
@@ -341,6 +350,13 @@ export async function createContentGenerator(
341350
if (envBaseUrl) {
342351
validateBaseUrl(envBaseUrl);
343352
baseUrl = envBaseUrl;
353+
} else if (config.authType === AuthType.USE_VERTEX_AI) {
354+
const location = process.env['GOOGLE_CLOUD_LOCATION'];
355+
if (location === 'us') {
356+
baseUrl = VERTEX_AI_US_REP_ENDPOINT;
357+
} else if (location === 'eu') {
358+
baseUrl = VERTEX_AI_EU_REP_ENDPOINT;
359+
}
344360
}
345361
} else {
346362
validateBaseUrl(baseUrl);

0 commit comments

Comments
 (0)