feat(llm): native Google Gemini provider via OpenAI-compatible endpoint
Add a first-class `gemini` provider so operators can drive missions with a direct GEMINI_API_KEY, instead of only reaching Gemini through OpenRouter. Gemini is wired exactly like xAI: dispatched through OpenAIAdapter against Google's OpenAI-compatible surface. The base URL is https://generativelanguage.googleapis.com/v1beta/openai — the `/openai` segment is required because OpenAIAdapter posts to `${baseUrl}/chat/completions`; without it requests hit `/v1beta/chat/completions`, which Google does not serve. Wires the provider through the type union, adapter factory, config (interface, defaults, model catalog, key env map, getLLMConfig, fallback chain, redaction, .env template), and adds regression tests pinning the `/openai` base-URL segment.
This commit is contained in:
4 files changed
+111
-9
No files matched your search
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { config, AVAILABLE_MODELS } from '../config/index.js';
|
||||
|
||||
// Regression coverage for #52: native Google Gemini support.
|
||||
// The engine dispatches Gemini through OpenAIAdapter, which posts to
|
||||
// `${baseUrl}/chat/completions`. Gemini's OpenAI-compatible surface lives under
|
||||
// /v1beta/openai, so the base URL MUST carry that path segment — the single most
|
||||
// important thing to get right, and the exact detail an earlier attempt missed.
|
||||
describe('Gemini native provider wiring (#52)', () => {
|
||||
it('routes gemini through the OpenAI-compatible endpoint including the /openai segment', () => {
|
||||
const cfg = config.getLLMConfig('gemini');
|
||||
expect(cfg.provider).toBe('gemini');
|
||||
expect(cfg.baseUrl).toBe('https://generativelanguage.googleapis.com/v1beta/openai');
|
||||
// Guard the regression directly: `${baseUrl}/chat/completions` must resolve under /v1beta/openai.
|
||||
expect(`${cfg.baseUrl}/chat/completions`).toBe(
|
||||
'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions',
|
||||
);
|
||||
});
|
||||
|
||||
it('defaults to a bare native Gemini model id (no google/ prefix)', () => {
|
||||
const cfg = config.getLLMConfig('gemini');
|
||||
expect(cfg.model).toBe('gemini-2.5-flash');
|
||||
expect(cfg.model.startsWith('google/')).toBe(false);
|
||||
});
|
||||
|
||||
it('publishes a non-empty native Gemini model catalog with bare ids', () => {
|
||||
expect(AVAILABLE_MODELS.gemini?.length ?? 0).toBeGreaterThan(0);
|
||||
for (const m of AVAILABLE_MODELS.gemini) {
|
||||
expect(m.id.startsWith('google/')).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('maps the gemini provider to the GEMINI_API_KEY environment variable', () => {
|
||||
const prev = process.env.GEMINI_API_KEY;
|
||||
process.env.GEMINI_API_KEY = 'test-gemini-key-0123456789';
|
||||
try {
|
||||
expect(config.hasApiKey('gemini')).toBe(true);
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env.GEMINI_API_KEY;
|
||||
else process.env.GEMINI_API_KEY = prev;
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user