feat(models): host google gemini models (#2122)

* feat(models): host google gemini models

* remove unused primary key
This commit is contained in:
Waleed
2025-11-26 12:30:20 -08:00
committed by GitHub
parent 0830490d32
commit 8000394e98
7 changed files with 97 additions and 22 deletions

View File

@@ -49,32 +49,53 @@ The model breakdown shows:
<Tabs items={['Hosted Models', 'Bring Your Own API Key']}>
<Tab>
**Hosted Models** - Sim provides API keys with a 2.5x pricing multiplier:
**OpenAI**
| Model | Base Price (Input/Output) | Hosted Price (Input/Output) |
|-------|---------------------------|----------------------------|
| GPT-5.1 | $1.25 / $10.00 | $3.13 / $25.00 |
| GPT-5 | $1.25 / $10.00 | $3.13 / $25.00 |
| GPT-5 Mini | $0.25 / $2.00 | $0.63 / $5.00 |
| GPT-5 Nano | $0.05 / $0.40 | $0.13 / $1.00 |
| GPT-4o | $2.50 / $10.00 | $6.25 / $25.00 |
| GPT-4.1 | $2.00 / $8.00 | $5.00 / $20.00 |
| GPT-4.1 Mini | $0.40 / $1.60 | $1.00 / $4.00 |
| GPT-4.1 Nano | $0.10 / $0.40 | $0.25 / $1.00 |
| o1 | $15.00 / $60.00 | $37.50 / $150.00 |
| o3 | $2.00 / $8.00 | $5.00 / $20.00 |
| Claude 3.5 Sonnet | $3.00 / $15.00 | $7.50 / $37.50 |
| Claude Opus 4.0 | $15.00 / $75.00 | $37.50 / $187.50 |
| o4 Mini | $1.10 / $4.40 | $2.75 / $11.00 |
**Anthropic**
| Model | Base Price (Input/Output) | Hosted Price (Input/Output) |
|-------|---------------------------|----------------------------|
| Claude Opus 4.5 | $5.00 / $25.00 | $12.50 / $62.50 |
| Claude Opus 4.1 | $15.00 / $75.00 | $37.50 / $187.50 |
| Claude Sonnet 4.5 | $3.00 / $15.00 | $7.50 / $37.50 |
| Claude Sonnet 4.0 | $3.00 / $15.00 | $7.50 / $37.50 |
| Claude Haiku 4.5 | $1.00 / $5.00 | $2.50 / $12.50 |
**Google**
| Model | Base Price (Input/Output) | Hosted Price (Input/Output) |
|-------|---------------------------|----------------------------|
| Gemini 3 Pro Preview | $2.00 / $12.00 | $5.00 / $30.00 |
| Gemini 2.5 Pro | $0.15 / $0.60 | $0.38 / $1.50 |
| Gemini 2.5 Flash | $0.15 / $0.60 | $0.38 / $1.50 |
*The 2.5x multiplier covers infrastructure and API management costs.*
</Tab>
<Tab>
**Your Own API Keys** - Use any model at base pricing:
| Provider | Models | Input / Output |
|----------|---------|----------------|
| Google | Gemini 2.5 | $0.15 / $0.60 |
| Provider | Example Models | Input / Output |
|----------|----------------|----------------|
| Deepseek | V3, R1 | $0.75 / $1.00 |
| xAI | Grok 4, Grok 3 | $5.00 / $25.00 |
| Groq | Llama 4 Scout | $0.40 / $0.60 |
| Cerebras | Llama 3.3 70B | $0.94 / $0.94 |
| xAI | Grok 4 Latest, Grok 3 | $3.00 / $15.00 |
| Groq | Llama 4 Scout, Llama 3.3 70B | $0.11 / $0.34 |
| Cerebras | Llama 4 Scout, Llama 3.3 70B | $0.11 / $0.34 |
| Ollama | Local models | Free |
| VLLM | Local models | Free |
*Pay providers directly with no markup*
</Tab>
</Tabs>

View File

@@ -76,6 +76,9 @@ export const env = createEnv({
ANTHROPIC_API_KEY_1: z.string().min(1).optional(), // Primary Anthropic Claude API key
ANTHROPIC_API_KEY_2: z.string().min(1).optional(), // Additional Anthropic API key for load balancing
ANTHROPIC_API_KEY_3: z.string().min(1).optional(), // Additional Anthropic API key for load balancing
GEMINI_API_KEY_1: z.string().min(1).optional(), // Primary Gemini API key
GEMINI_API_KEY_2: z.string().min(1).optional(), // Additional Gemini API key for load balancing
GEMINI_API_KEY_3: z.string().min(1).optional(), // Additional Gemini API key for load balancing
OLLAMA_URL: z.string().url().optional(), // Ollama local LLM server URL
VLLM_BASE_URL: z.string().url().optional(), // vLLM self-hosted base URL (OpenAI-compatible)
VLLM_API_KEY: z.string().optional(), // Optional bearer token for vLLM

View File

@@ -9,6 +9,7 @@ import {
formatDuration,
formatTime,
getInvalidCharacters,
getRotatingApiKey,
getTimezoneAbbreviation,
isValidName,
redactApiKeys,
@@ -36,6 +37,15 @@ vi.mock('crypto', () => ({
vi.mock('@/lib/env', () => ({
env: {
ENCRYPTION_KEY: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
OPENAI_API_KEY_1: 'test-openai-key-1',
OPENAI_API_KEY_2: 'test-openai-key-2',
OPENAI_API_KEY_3: 'test-openai-key-3',
ANTHROPIC_API_KEY_1: 'test-anthropic-key-1',
ANTHROPIC_API_KEY_2: 'test-anthropic-key-2',
ANTHROPIC_API_KEY_3: 'test-anthropic-key-3',
GEMINI_API_KEY_1: 'test-gemini-key-1',
GEMINI_API_KEY_2: 'test-gemini-key-2',
GEMINI_API_KEY_3: 'test-gemini-key-3',
},
}))
@@ -383,3 +393,29 @@ describe('getInvalidCharacters', () => {
expect(result).toEqual(['@', '#', '$', '%'])
})
})
describe('getRotatingApiKey', () => {
it.concurrent('should return OpenAI API key based on current minute', () => {
const result = getRotatingApiKey('openai')
expect(result).toMatch(/^test-openai-key-[1-3]$/)
})
it.concurrent('should return Anthropic API key based on current minute', () => {
const result = getRotatingApiKey('anthropic')
expect(result).toMatch(/^test-anthropic-key-[1-3]$/)
})
it.concurrent('should return Gemini API key based on current minute', () => {
const result = getRotatingApiKey('gemini')
expect(result).toMatch(/^test-gemini-key-[1-3]$/)
})
it.concurrent('should throw error for unsupported provider', () => {
expect(() => getRotatingApiKey('unsupported')).toThrow('No rotation implemented for provider')
})
it.concurrent('should rotate keys based on minute modulo', () => {
const result = getRotatingApiKey('openai')
expect(['test-openai-key-1', 'test-openai-key-2', 'test-openai-key-3']).toContain(result)
})
})

View File

@@ -287,7 +287,7 @@ export function generatePassword(length = 24): string {
* @throws Error if no API keys are configured for rotation
*/
export function getRotatingApiKey(provider: string): string {
if (provider !== 'openai' && provider !== 'anthropic') {
if (provider !== 'openai' && provider !== 'anthropic' && provider !== 'gemini') {
throw new Error(`No rotation implemented for provider: ${provider}`)
}
@@ -301,6 +301,10 @@ export function getRotatingApiKey(provider: string): string {
if (env.ANTHROPIC_API_KEY_1) keys.push(env.ANTHROPIC_API_KEY_1)
if (env.ANTHROPIC_API_KEY_2) keys.push(env.ANTHROPIC_API_KEY_2)
if (env.ANTHROPIC_API_KEY_3) keys.push(env.ANTHROPIC_API_KEY_3)
} else if (provider === 'gemini') {
if (env.GEMINI_API_KEY_1) keys.push(env.GEMINI_API_KEY_1)
if (env.GEMINI_API_KEY_2) keys.push(env.GEMINI_API_KEY_2)
if (env.GEMINI_API_KEY_3) keys.push(env.GEMINI_API_KEY_3)
}
if (keys.length === 0) {

View File

@@ -1336,8 +1336,11 @@ export function getProvidersWithToolUsageControl(): string[] {
* Get all models that are hosted (don't require user API keys)
*/
export function getHostedModels(): string[] {
// Currently, OpenAI and Anthropic models are hosted
return [...getProviderModels('openai'), ...getProviderModels('anthropic')]
return [
...getProviderModels('openai'),
...getProviderModels('anthropic'),
...getProviderModels('google'),
]
}
/**

View File

@@ -445,17 +445,24 @@ describe('Cost Calculation', () => {
})
describe('getHostedModels', () => {
it.concurrent('should return OpenAI and Anthropic models as hosted', () => {
it.concurrent('should return OpenAI, Anthropic, and Google models as hosted', () => {
const hostedModels = getHostedModels()
// OpenAI models
expect(hostedModels).toContain('gpt-4o')
expect(hostedModels).toContain('claude-sonnet-4-0')
expect(hostedModels).toContain('o1')
// Anthropic models
expect(hostedModels).toContain('claude-sonnet-4-0')
expect(hostedModels).toContain('claude-opus-4-0')
// Google models
expect(hostedModels).toContain('gemini-2.5-pro')
expect(hostedModels).toContain('gemini-2.5-flash')
// Should not contain models from other providers
expect(hostedModels).not.toContain('gemini-2.5-pro')
expect(hostedModels).not.toContain('deepseek-v3')
expect(hostedModels).not.toContain('grok-4-latest')
})
it.concurrent('should return an array of strings', () => {

View File

@@ -637,15 +637,16 @@ export function getApiKey(provider: string, model: string, userProvidedKey?: str
return 'empty' // Ollama uses 'empty' as a placeholder API key
}
// Use server key rotation for all OpenAI models and Anthropic's Claude models on the hosted platform
// Use server key rotation for all OpenAI models, Anthropic's Claude models, and Google's Gemini models on the hosted platform
const isOpenAIModel = provider === 'openai'
const isClaudeModel = provider === 'anthropic'
const isGeminiModel = provider === 'google'
if (isHosted && (isOpenAIModel || isClaudeModel)) {
if (isHosted && (isOpenAIModel || isClaudeModel || isGeminiModel)) {
try {
// Import the key rotation function
const { getRotatingApiKey } = require('@/lib/utils')
const serverKey = getRotatingApiKey(provider)
const serverKey = getRotatingApiKey(isGeminiModel ? 'gemini' : provider)
return serverKey
} catch (_error) {
// If server key fails and we have a user key, fallback to that