Skip to content

Commit 9b7673b

Browse files
vijayupadyavijay upadya
andauthored
Gemini BYOK: Handle invalid key (microsoft#2733)
* Handle invalid API Key * update test --------- Co-authored-by: vijay upadya <vj@example.com>
1 parent fdc4126 commit 9b7673b

File tree

2 files changed

+100
-1
lines changed

2 files changed

+100
-1
lines changed

src/extension/byok/vscode-node/geminiNativeProvider.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@ export class GeminiNativeBYOKLMProvider implements BYOKModelProvider<LanguageMod
3333
@IRequestLogger private readonly _requestLogger: IRequestLogger
3434
) { }
3535

36+
private _isInvalidApiKeyError(error: unknown): boolean {
37+
if (!error) {
38+
return false;
39+
}
40+
41+
const message = typeof error === 'string' ? error : (error as Error).message;
42+
if (typeof message !== 'string') {
43+
return false;
44+
}
45+
46+
const lower = message.toLowerCase();
47+
return lower.includes('api key not valid') || lower.includes('api_key_invalid') || lower.includes('api key invalid');
48+
}
49+
3650
private async _getOrReadApiKey(): Promise<string | undefined> {
3751
if (!this._apiKey) {
3852
this._apiKey = await this._byokStorageService.getAPIKey(GeminiNativeBYOKLMProvider.providerName);
@@ -107,7 +121,20 @@ export class GeminiNativeBYOKLMProvider implements BYOKModelProvider<LanguageMod
107121
return [];
108122
}
109123
}
110-
} catch {
124+
} catch (error) {
125+
if (this._isInvalidApiKeyError(error)) {
126+
if (options.silent) {
127+
return [];
128+
}
129+
await this.updateAPIKey();
130+
if (this._apiKey) {
131+
try {
132+
return byokKnownModelsToAPIInfo(GeminiNativeBYOKLMProvider.providerName, await this.getAllModels(this._apiKey));
133+
} catch (retryError) {
134+
this._logService.error(`Error after re-prompting for API key: ${toErrorMessage(retryError, true)}`);
135+
}
136+
}
137+
}
111138
return [];
112139
}
113140
}

src/extension/byok/vscode-node/test/geminiNativeProvider.spec.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,4 +217,76 @@ describe('GeminiNativeBYOKLMProvider', () => {
217217
tokenSource.token
218218
)).rejects.toThrow(/No API key configured/i);
219219
});
220+
221+
it('prompts for a new API key when listing models fails with an invalid key', async () => {
222+
const { GeminiNativeBYOKLMProvider } = await import('../geminiNativeProvider');
223+
const genai = await import('@google/genai');
224+
const MockGoogleGenAI = genai.GoogleGenAI as unknown as { listModelsResult: AsyncIterable<any> };
225+
// Simulate the models.list() call throwing an invalid API key error when iterated
226+
MockGoogleGenAI.listModelsResult = (async function* () {
227+
throw new Error('ApiError: {"error":{"message":"API key not valid. Please pass a valid API key.","details":[{"reason":"API_KEY_INVALID"}]}}');
228+
})();
229+
230+
const storage = createStorageService({
231+
getAPIKey: vi.fn().mockResolvedValue('bad_key'),
232+
});
233+
234+
mockHandleAPIKeyUpdate.mockResolvedValue({ apiKey: undefined, deleted: false, cancelled: true });
235+
236+
const provider = new GeminiNativeBYOKLMProvider(undefined, storage, new TestLogService(), createRequestLogger());
237+
const tokenSource = new vscode.CancellationTokenSource();
238+
const models = await provider.provideLanguageModelChatInformation({ silent: false }, tokenSource.token);
239+
240+
// When the key is invalid, we should re-prompt for a new one
241+
// and handle the failure gracefully by returning an empty list.
242+
expect(models).toEqual([]);
243+
expect(mockHandleAPIKeyUpdate).toHaveBeenCalled();
244+
});
245+
246+
it('retries listing models after re-prompting with a valid API key', async () => {
247+
const { GeminiNativeBYOKLMProvider } = await import('../geminiNativeProvider');
248+
const genai = await import('@google/genai');
249+
const MockGoogleGenAI = genai.GoogleGenAI as unknown as { listModelsResult: AsyncIterable<any> };
250+
251+
let iterationCount = 0;
252+
let hasThrown = false;
253+
const modelId = 'test-model';
254+
255+
MockGoogleGenAI.listModelsResult = {
256+
async *[Symbol.asyncIterator]() {
257+
iterationCount++;
258+
if (!hasThrown) {
259+
hasThrown = true;
260+
throw new Error('ApiError: {"error":{"message":"API key not valid. Please pass a valid API key.","details":[{"reason":"API_KEY_INVALID"}]}}');
261+
}
262+
yield { name: modelId };
263+
}
264+
};
265+
266+
const storage = createStorageService({
267+
getAPIKey: vi.fn().mockResolvedValue('bad_key'),
268+
});
269+
270+
mockHandleAPIKeyUpdate.mockResolvedValue({ apiKey: 'k_new', deleted: false, cancelled: false });
271+
272+
const knownModels = {
273+
[modelId]: {
274+
name: 'Test Model',
275+
maxInputTokens: 1000,
276+
maxOutputTokens: 1000,
277+
toolCalling: false,
278+
vision: false
279+
}
280+
};
281+
282+
const provider = new GeminiNativeBYOKLMProvider(knownModels, storage, new TestLogService(), createRequestLogger());
283+
const tokenSource = new vscode.CancellationTokenSource();
284+
const models = await provider.provideLanguageModelChatInformation({ silent: false }, tokenSource.token);
285+
286+
// First attempt should fail with invalid key, then after re-prompting
287+
// we should retry listing models and succeed with the new key.
288+
expect(models.map(m => m.id)).toEqual([modelId]);
289+
expect(iterationCount).toBe(2);
290+
expect(mockHandleAPIKeyUpdate).toHaveBeenCalled();
291+
});
220292
});

0 commit comments

Comments
 (0)