diff --git a/examples/multimodal/.env.example b/examples/multimodal/.env.example new file mode 100644 index 00000000..f46b7408 --- /dev/null +++ b/examples/multimodal/.env.example @@ -0,0 +1,3 @@ +FISHJAM_ID="" +FISHJAM_TOKEN="" +GEMINI_API_KEY="" diff --git a/examples/multimodal/README.md b/examples/multimodal/README.md index 8b48f5ba..07e3590c 100644 --- a/examples/multimodal/README.md +++ b/examples/multimodal/README.md @@ -23,3 +23,13 @@ When the server is running, you can obtain peer tokens by going to - `FISHJAM_TOKEN`: your Fishjam management token, which you can get at -- `GEMINI_API_TOKEN`: your Gemini API token, which you can get at +- `GEMINI_API_KEY`: your Gemini API key, which you can get at Once you've set up your environment variables, all you need to do is run the following command: @@ -23,3 +23,13 @@ When the server is running, you can obtain peer tokens by going to { return new GoogleGenAI(finalOptions); }; +/** + * Verifies the API key by making a single lightweight authenticated call + * (`models.list`). Resolves on success; throws if the call fails — either + * because the key was rejected or because the request itself failed (e.g. + * network/connectivity). The original error is preserved as `cause`. + * + * Note: this catches the common cases (invalid / unauthorized / wrong-project / + * region-blocked keys). It does not guarantee the key can use a specific Live + * native-audio model — such model-specific rejections still surface only via the + * `live.connect` session callbacks (`onerror`/`onclose`). + * + * @param client A GoogleGenAI instance, e.g. from {@link createClient}. + */ +export const checkCredentials = async (client: GoogleGenAI): Promise => { + try { + await client.models.list(); + } catch (error) { + throw new Error( + 'Could not verify the Gemini API key. The key may be invalid/unauthorized (check the key and that ' + + 'the Gemini API is enabled for its project/region), or the request to Gemini failed (e.g. network ' + + 'connectivity). See the cause for details.', + { cause: error } + ); + } +}; + +/** + * Creates a GoogleGenAI client and verifies the API key before returning it, + * so misconfiguration fails fast. + * + * Throws if the key is rejected (see {@link checkCredentials}). + * + * @param options Configuration for the GoogleGenAI client. + * @returns A validated GoogleGenAI instance. + */ +export const createClientAndValidate = async (options: GoogleGenAIOptions): Promise => { + const client = createClient(options); + await checkCredentials(client); + return client; +}; + /** * Predefined audio settings for the agent's output track, * configured for Gemini's 24kHz audio output. diff --git a/packages/js-server-sdk/tests/gemini.test.ts b/packages/js-server-sdk/tests/gemini.test.ts new file mode 100644 index 00000000..695b00cc --- /dev/null +++ b/packages/js-server-sdk/tests/gemini.test.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from 'vitest'; +import { checkCredentials } from '../src/integrations/gemini'; +import type { GoogleGenAI } from '@google/genai'; + +// Fake just the one method checkCredentials touches (models.list), no network / no key. +const fakeClient = (list: () => Promise) => ({ models: { list } }) as unknown as GoogleGenAI; + +describe('checkCredentials', () => { + it('resolves when the key is accepted', async () => { + await expect(checkCredentials(fakeClient(async () => ({})))).resolves.toBeUndefined(); + }); + + it('throws a clear error wrapping the cause when verification fails', async () => { + const cause = new Error('401 API key not valid'); + await expect( + checkCredentials( + fakeClient(async () => { + throw cause; + }) + ) + ).rejects.toMatchObject({ message: expect.stringContaining('Could not verify the Gemini API key'), cause }); + }); +});