fix(vllm): remove requirement for api key for vllm (#2380)

This commit is contained in:
Waleed
2025-12-15 10:48:48 -08:00
committed by GitHub
parent 0acd86023c
commit 6009a7359f
5 changed files with 95 additions and 53 deletions

View File

@@ -45,7 +45,6 @@ export async function GET(request: NextRequest) {
host: OLLAMA_HOST,
})
// Return empty array instead of error to avoid breaking the UI
return NextResponse.json({ models: [] })
}
}

View File

@@ -20,10 +20,16 @@ export async function GET(request: NextRequest) {
baseUrl,
})
const headers: Record<string, string> = {
'Content-Type': 'application/json',
}
if (env.VLLM_API_KEY) {
headers.Authorization = `Bearer ${env.VLLM_API_KEY}`
}
const response = await fetch(`${baseUrl}/v1/models`, {
headers: {
'Content-Type': 'application/json',
},
headers,
next: { revalidate: 60 },
})
@@ -50,7 +56,6 @@ export async function GET(request: NextRequest) {
baseUrl,
})
// Return empty array instead of error to avoid breaking the UI
return NextResponse.json({ models: [] })
}
}

View File

@@ -53,19 +53,20 @@ describe('getApiKey', () => {
module.require = originalRequire
})
it('should return user-provided key when not in hosted environment', () => {
it.concurrent('should return user-provided key when not in hosted environment', () => {
isHostedSpy.mockReturnValue(false)
// For OpenAI
const key1 = getApiKey('openai', 'gpt-4', 'user-key-openai')
expect(key1).toBe('user-key-openai')
// For Anthropic
const key2 = getApiKey('anthropic', 'claude-3', 'user-key-anthropic')
expect(key2).toBe('user-key-anthropic')
const key3 = getApiKey('google', 'gemini-2.5-flash', 'user-key-google')
expect(key3).toBe('user-key-google')
})
it('should throw error if no key provided in non-hosted environment', () => {
it.concurrent('should throw error if no key provided in non-hosted environment', () => {
isHostedSpy.mockReturnValue(false)
expect(() => getApiKey('openai', 'gpt-4')).toThrow('API key is required for openai gpt-4')
@@ -74,63 +75,87 @@ describe('getApiKey', () => {
)
})
it('should fall back to user key in hosted environment if rotation fails', () => {
it.concurrent('should fall back to user key in hosted environment if rotation fails', () => {
isHostedSpy.mockReturnValue(true)
module.require = vi.fn(() => {
throw new Error('Rotation failed')
})
// Use gpt-4o which IS in the hosted models list
const key = getApiKey('openai', 'gpt-4o', 'user-fallback-key')
expect(key).toBe('user-fallback-key')
})
it('should throw error in hosted environment if rotation fails and no user key', () => {
isHostedSpy.mockReturnValue(true)
it.concurrent(
'should throw error in hosted environment if rotation fails and no user key',
() => {
isHostedSpy.mockReturnValue(true)
module.require = vi.fn(() => {
throw new Error('Rotation failed')
})
module.require = vi.fn(() => {
throw new Error('Rotation failed')
})
// Use gpt-4o which IS in the hosted models list
expect(() => getApiKey('openai', 'gpt-4o')).toThrow('No API key available for openai gpt-4o')
expect(() => getApiKey('openai', 'gpt-4o')).toThrow('No API key available for openai gpt-4o')
}
)
it.concurrent(
'should require user key for non-OpenAI/Anthropic providers even in hosted environment',
() => {
isHostedSpy.mockReturnValue(true)
const key = getApiKey('other-provider', 'some-model', 'user-key')
expect(key).toBe('user-key')
expect(() => getApiKey('other-provider', 'some-model')).toThrow(
'API key is required for other-provider some-model'
)
}
)
it.concurrent(
'should require user key for models NOT in hosted list even if provider matches',
() => {
isHostedSpy.mockReturnValue(true)
const key1 = getApiKey('anthropic', 'claude-sonnet-4-20250514', 'user-key-anthropic')
expect(key1).toBe('user-key-anthropic')
expect(() => getApiKey('anthropic', 'claude-sonnet-4-20250514')).toThrow(
'API key is required for anthropic claude-sonnet-4-20250514'
)
const key2 = getApiKey('openai', 'gpt-4o-2024-08-06', 'user-key-openai')
expect(key2).toBe('user-key-openai')
expect(() => getApiKey('openai', 'gpt-4o-2024-08-06')).toThrow(
'API key is required for openai gpt-4o-2024-08-06'
)
}
)
it.concurrent('should return empty for ollama provider without requiring API key', () => {
isHostedSpy.mockReturnValue(false)
const key = getApiKey('ollama', 'llama2')
expect(key).toBe('empty')
const key2 = getApiKey('ollama', 'codellama', 'user-key')
expect(key2).toBe('empty')
})
it('should require user key for non-OpenAI/Anthropic providers even in hosted environment', () => {
isHostedSpy.mockReturnValue(true)
it.concurrent(
'should return empty or user-provided key for vllm provider without requiring API key',
() => {
isHostedSpy.mockReturnValue(false)
const key = getApiKey('other-provider', 'some-model', 'user-key')
expect(key).toBe('user-key')
const key = getApiKey('vllm', 'vllm/qwen-3')
expect(key).toBe('empty')
expect(() => getApiKey('other-provider', 'some-model')).toThrow(
'API key is required for other-provider some-model'
)
})
it('should require user key for models NOT in hosted list even if provider matches', () => {
isHostedSpy.mockReturnValue(true)
// Models with version suffixes that are NOT in the hosted list should require user API key
// even though they're from anthropic/openai providers
// User provides their own key - should work
const key1 = getApiKey('anthropic', 'claude-sonnet-4-20250514', 'user-key-anthropic')
expect(key1).toBe('user-key-anthropic')
// No user key - should throw, NOT use server key
expect(() => getApiKey('anthropic', 'claude-sonnet-4-20250514')).toThrow(
'API key is required for anthropic claude-sonnet-4-20250514'
)
// Same for OpenAI versioned models not in list
const key2 = getApiKey('openai', 'gpt-4o-2024-08-06', 'user-key-openai')
expect(key2).toBe('user-key-openai')
expect(() => getApiKey('openai', 'gpt-4o-2024-08-06')).toThrow(
'API key is required for openai gpt-4o-2024-08-06'
)
})
const key2 = getApiKey('vllm', 'vllm/llama', 'user-key')
expect(key2).toBe('user-key')
}
)
})
describe('Model Capabilities', () => {
@@ -202,7 +227,6 @@ describe('Model Capabilities', () => {
it.concurrent(
'should inherit temperature support from provider for dynamically fetched models',
() => {
// OpenRouter models should inherit temperature support from provider capabilities
expect(supportsTemperature('openrouter/anthropic/claude-3.5-sonnet')).toBe(true)
expect(supportsTemperature('openrouter/openai/gpt-4')).toBe(true)
}

View File

@@ -630,13 +630,19 @@ export function getApiKey(provider: string, model: string, userProvidedKey?: str
// If user provided a key, use it as a fallback
const hasUserKey = !!userProvidedKey
// Ollama models don't require API keys - they run locally
// Ollama and vLLM models don't require API keys
const isOllamaModel =
provider === 'ollama' || useProvidersStore.getState().providers.ollama.models.includes(model)
if (isOllamaModel) {
return 'empty' // Ollama uses 'empty' as a placeholder API key
}
const isVllmModel =
provider === 'vllm' || useProvidersStore.getState().providers.vllm.models.includes(model)
if (isVllmModel) {
return userProvidedKey || 'empty' // vLLM uses 'empty' as a placeholder if no key provided
}
// 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'

View File

@@ -79,7 +79,15 @@ export const vllmProvider: ProviderConfig = {
}
try {
const response = await fetch(`${baseUrl}/v1/models`)
const headers: Record<string, string> = {
'Content-Type': 'application/json',
}
if (env.VLLM_API_KEY) {
headers.Authorization = `Bearer ${env.VLLM_API_KEY}`
}
const response = await fetch(`${baseUrl}/v1/models`, { headers })
if (!response.ok) {
useProvidersStore.getState().setProviderModels('vllm', [])
logger.warn('vLLM service is not available. The provider will be disabled.')