Skip to content

Commit 2f7f9ba

Browse files
committed
fix(models): allow users to run gpt-4o on hosted version without bringing their own keys
1 parent 2060913 commit 2f7f9ba

File tree

5 files changed

+186
-18
lines changed

5 files changed

+186
-18
lines changed

sim/app/api/keys/openai/route.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { unstable_noStore as noStore } from 'next/cache'
2+
import { NextRequest, NextResponse } from 'next/server'
3+
import { createLogger } from '@/lib/logs/console-logger'
4+
import { getRotatingApiKey } from '@/lib/utils'
5+
6+
const logger = createLogger('OpenAIKeyAPI')
7+
8+
export const dynamic = 'force-dynamic'
9+
10+
/**
11+
* Get a rotating OpenAI API key for the specified model
12+
* This endpoint is designed to be used by client-side code
13+
* to get access to server-side environment variables
14+
*/
15+
export async function POST(request: NextRequest) {
16+
noStore()
17+
18+
try {
19+
const { model } = await request.json()
20+
21+
if (!model) {
22+
return NextResponse.json({ error: 'Model parameter is required' }, { status: 400 })
23+
}
24+
25+
// Only provide API key for gpt-4o models
26+
if (model !== 'gpt-4o') {
27+
return NextResponse.json(
28+
{ error: 'API key rotation is only available for gpt-4o models' },
29+
{ status: 400 }
30+
)
31+
}
32+
33+
// Check if we're on the hosted version - this is a server-side check
34+
const isHostedVersion = process.env.NEXT_PUBLIC_APP_URL === 'https://www.simstudio.ai'
35+
if (!isHostedVersion) {
36+
return NextResponse.json(
37+
{ error: 'API key rotation is only available on the hosted version' },
38+
{ status: 403 }
39+
)
40+
}
41+
42+
try {
43+
// Use the shared utility function to get a rotating key
44+
const apiKey = getRotatingApiKey('openai')
45+
logger.info(`Provided rotating API key for model: ${model}`)
46+
return NextResponse.json({ apiKey })
47+
} catch (error) {
48+
logger.error('Failed to get rotating API key:', error)
49+
return NextResponse.json({ error: 'No API keys configured for rotation' }, { status: 500 })
50+
}
51+
} catch (error) {
52+
logger.error('Error providing API key:', error)
53+
return NextResponse.json(
54+
{ error: 'Failed to provide API key', message: (error as Error).message },
55+
{ status: 500 }
56+
)
57+
}
58+
}

sim/blocks/blocks/agent.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { AgentIcon } from '@/components/icons'
2+
import { isHostedVersion } from '@/lib/utils'
23
import { useOllamaStore } from '@/stores/ollama/store'
34
import { MODELS_TEMP_RANGE_0_1, MODELS_TEMP_RANGE_0_2 } from '@/providers/model-capabilities'
45
import { getAllModelProviders, getBaseModelProviders } from '@/providers/utils'
56
import { ToolResponse } from '@/tools/types'
67
import { BlockConfig } from '../types'
78

8-
// Determine if we're running on the hosted version
9-
const isHostedVersion = typeof window !== 'undefined' &&
10-
process.env.NEXT_PUBLIC_APP_URL === 'https://www.simstudio.ai'
9+
const isHosted = isHostedVersion()
1110

1211
interface AgentResponse extends ToolResponse {
1312
output: {
@@ -96,11 +95,13 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
9695
password: true,
9796
connectionDroppable: false,
9897
// Hide API key for GPT-4o models when running on hosted version
99-
condition: isHostedVersion ? {
100-
field: 'model',
101-
value: 'gpt-4o',
102-
not: true // Show for all models EXCEPT GPT-4o models
103-
} : undefined, // Show for all models in non-hosted environments
98+
condition: isHosted
99+
? {
100+
field: 'model',
101+
value: 'gpt-4o',
102+
not: true, // Show for all models EXCEPT GPT-4o models
103+
}
104+
: undefined, // Show for all models in non-hosted environments
104105
},
105106
{
106107
id: 'tools',

sim/executor/handlers/agent/agent-handler.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createLogger } from '@/lib/logs/console-logger'
2+
import { isHostedVersion } from '@/lib/utils'
23
import { getAllBlocks } from '@/blocks'
34
import { BlockOutput } from '@/blocks/types'
45
import { executeProviderRequest } from '@/providers'
@@ -65,6 +66,17 @@ export class AgentBlockHandler implements BlockHandler {
6566
const providerId = getProviderFromModel(model)
6667
logger.info(`Using provider: ${providerId}, model: ${model}`)
6768

69+
// Check if we need to validate API key presence
70+
const isGPT4o = model === 'gpt-4o'
71+
const isHosted = isHostedVersion()
72+
73+
// For non-hosted version, or for models other than gpt-4o, API key is required
74+
if (!isHosted || !isGPT4o) {
75+
if (!inputs.apiKey) {
76+
throw new Error(`API key is required for ${model}`)
77+
}
78+
}
79+
6880
// Format tools for provider API
6981
const formattedTools = Array.isArray(inputs.tools)
7082
? (

sim/lib/utils.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,46 @@ export function formatDuration(durationMs: number): string {
197197
export function generateApiKey(): string {
198198
return `sim_${nanoid(32)}`
199199
}
200+
201+
/**
202+
* Determines if the application is running on the hosted/production version
203+
* @returns boolean indicating if the app is running on the hosted version
204+
*/
205+
export function isHostedVersion(): boolean {
206+
return (
207+
typeof window !== 'undefined' && process.env.NEXT_PUBLIC_APP_URL === 'https://www.simstudio.ai'
208+
)
209+
}
210+
211+
/**
212+
* Rotates through available API keys for a provider
213+
* @param provider - The provider to get a key for (e.g., 'openai')
214+
* @returns The selected API key
215+
* @throws Error if no API keys are configured for rotation
216+
*/
217+
export function getRotatingApiKey(provider: string): string {
218+
if (provider !== 'openai') {
219+
throw new Error(`No rotation implemented for provider: ${provider}`)
220+
}
221+
222+
// Get all OpenAI keys from environment
223+
const keys = []
224+
225+
// Add keys if they exist in environment variables
226+
if (process.env.OPENAI_API_KEY_1) keys.push(process.env.OPENAI_API_KEY_1)
227+
if (process.env.OPENAI_API_KEY_2) keys.push(process.env.OPENAI_API_KEY_2)
228+
if (process.env.OPENAI_API_KEY_3) keys.push(process.env.OPENAI_API_KEY_3)
229+
230+
if (keys.length === 0) {
231+
throw new Error(
232+
'No API keys configured for rotation. Please configure OPENAI_API_KEY_1, OPENAI_API_KEY_2, or OPENAI_API_KEY_3.'
233+
)
234+
}
235+
236+
// Simple round-robin rotation based on current minute
237+
// This distributes load across keys and is stateless
238+
const currentMinute = new Date().getMinutes()
239+
const keyIndex = currentMinute % keys.length
240+
241+
return keys[keyIndex]
242+
}

sim/providers/openai/index.ts

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,71 @@
11
import OpenAI from 'openai'
22
import { createLogger } from '@/lib/logs/console-logger'
3+
import { isHostedVersion } from '@/lib/utils'
34
import { executeTool } from '@/tools'
45
import { ProviderConfig, ProviderRequest, ProviderResponse, TimeSegment } from '../types'
56

67
const logger = createLogger('OpenAI Provider')
78

9+
/**
10+
* Helper function to handle API key rotation for GPT-4o
11+
* @param apiKey - The original API key from the request
12+
* @param model - The model being used
13+
* @returns The API key to use (original or rotating)
14+
*/
15+
async function getApiKey(apiKey: string | undefined, model: string): Promise<string> {
16+
// Check if we should use a rotating key
17+
const isHosted = isHostedVersion()
18+
const isGPT4o = model === 'gpt-4o'
19+
20+
// On hosted version, always use rotating key for GPT-4o models
21+
if (isHosted && isGPT4o) {
22+
try {
23+
// Use API endpoint to get the key from server-side env variables
24+
const response = await fetch('/api/keys/openai', {
25+
method: 'POST',
26+
headers: {
27+
'Content-Type': 'application/json',
28+
},
29+
body: JSON.stringify({ model }),
30+
})
31+
32+
if (!response.ok) {
33+
const errorData = await response.json()
34+
throw new Error(`API key service error: ${errorData.error || response.statusText}`)
35+
}
36+
37+
const data = await response.json()
38+
if (!data.apiKey) {
39+
throw new Error('API key service did not return a key')
40+
}
41+
42+
logger.info('Using API key from server rotation service')
43+
return data.apiKey
44+
} catch (error) {
45+
logger.warn('Failed to get API key from server', {
46+
error: error instanceof Error ? error.message : String(error),
47+
})
48+
49+
// If we couldn't get a rotating key and have a user key, use it as fallback
50+
// This should only happen if rotation system is misconfigured
51+
if (apiKey) {
52+
logger.info('Falling back to user-provided API key')
53+
return apiKey
54+
}
55+
56+
// No rotating key and no user key - throw specific error
57+
throw new Error('API key is required for OpenAI')
58+
}
59+
}
60+
61+
// For non-hosted versions or non-GPT4o models, require the provided key
62+
if (!apiKey) {
63+
throw new Error('API key is required for OpenAI')
64+
}
65+
66+
return apiKey
67+
}
68+
869
/**
970
* OpenAI provider configuration
1071
*/
@@ -26,18 +87,11 @@ export const openaiProvider: ProviderConfig = {
2687
hasResponseFormat: !!request.responseFormat,
2788
})
2889

29-
if (!request.apiKey) {
30-
logger.error('OpenAI API key missing in request', {
31-
hasModel: !!request.model,
32-
hasSystemPrompt: !!request.systemPrompt,
33-
hasMessages: !!request.messages,
34-
hasTools: !!request.tools,
35-
})
36-
throw new Error('API key is required for OpenAI')
37-
}
90+
// Get the API key - this will fetch from the server if needed for gpt-4o on hosted version
91+
const apiKey = await getApiKey(request.apiKey, request.model || 'gpt-4o')
3892

3993
const openai = new OpenAI({
40-
apiKey: request.apiKey,
94+
apiKey,
4195
dangerouslyAllowBrowser: true,
4296
})
4397

0 commit comments

Comments
 (0)