Skip to content

Commit e562eee

Browse files
committed
Migrate openai to use responses api
1 parent 362f4c2 commit e562eee

File tree

7 files changed

+944
-398
lines changed

7 files changed

+944
-398
lines changed

apps/sim/app/api/wand/route.ts

Lines changed: 116 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { userStats, workflow } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { eq, sql } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
6-
import OpenAI, { AzureOpenAI } from 'openai'
76
import { getBYOKKey } from '@/lib/api-key/byok'
87
import { getSession } from '@/lib/auth'
98
import { logModelUsage } from '@/lib/billing/core/usage-log'
@@ -12,6 +11,7 @@ import { env } from '@/lib/core/config/env'
1211
import { getCostMultiplier, isBillingEnabled } from '@/lib/core/config/feature-flags'
1312
import { generateRequestId } from '@/lib/core/utils/request'
1413
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
14+
import { extractResponseText, parseResponsesUsage } from '@/providers/responses-utils'
1515
import { getModelPricing } from '@/providers/utils'
1616

1717
export const dynamic = 'force-dynamic'
@@ -28,18 +28,6 @@ const openaiApiKey = env.OPENAI_API_KEY
2828

2929
const useWandAzure = azureApiKey && azureEndpoint && azureApiVersion
3030

31-
const client = useWandAzure
32-
? new AzureOpenAI({
33-
apiKey: azureApiKey,
34-
apiVersion: azureApiVersion,
35-
endpoint: azureEndpoint,
36-
})
37-
: openaiApiKey
38-
? new OpenAI({
39-
apiKey: openaiApiKey,
40-
})
41-
: null
42-
4331
if (!useWandAzure && !openaiApiKey) {
4432
logger.warn(
4533
'Neither Azure OpenAI nor OpenAI API key found. Wand generation API will not function.'
@@ -202,20 +190,18 @@ export async function POST(req: NextRequest) {
202190
}
203191

204192
let isBYOK = false
205-
let activeClient = client
206-
let byokApiKey: string | null = null
193+
let activeOpenAIKey = openaiApiKey
207194

208195
if (workspaceId && !useWandAzure) {
209196
const byokResult = await getBYOKKey(workspaceId, 'openai')
210197
if (byokResult) {
211198
isBYOK = true
212-
byokApiKey = byokResult.apiKey
213-
activeClient = new OpenAI({ apiKey: byokResult.apiKey })
199+
activeOpenAIKey = byokResult.apiKey
214200
logger.info(`[${requestId}] Using BYOK OpenAI key for wand generation`)
215201
}
216202
}
217203

218-
if (!activeClient) {
204+
if (!useWandAzure && !activeOpenAIKey) {
219205
logger.error(`[${requestId}] AI client not initialized. Missing API key.`)
220206
return NextResponse.json(
221207
{ success: false, error: 'Wand generation service is not configured.' },
@@ -276,17 +262,18 @@ Use this context to calculate relative dates like "yesterday", "last week", "beg
276262
)
277263

278264
const apiUrl = useWandAzure
279-
? `${azureEndpoint}/openai/deployments/${wandModelName}/chat/completions?api-version=${azureApiVersion}`
280-
: 'https://api.openai.com/v1/chat/completions'
265+
? `${azureEndpoint}/openai/v1/responses?api-version=${azureApiVersion}`
266+
: 'https://api.openai.com/v1/responses'
281267

282268
const headers: Record<string, string> = {
283269
'Content-Type': 'application/json',
270+
'OpenAI-Beta': 'responses=v1',
284271
}
285272

286273
if (useWandAzure) {
287274
headers['api-key'] = azureApiKey!
288275
} else {
289-
headers.Authorization = `Bearer ${byokApiKey || openaiApiKey}`
276+
headers.Authorization = `Bearer ${activeOpenAIKey}`
290277
}
291278

292279
logger.debug(`[${requestId}] Making streaming request to: ${apiUrl}`)
@@ -296,11 +283,10 @@ Use this context to calculate relative dates like "yesterday", "last week", "beg
296283
headers,
297284
body: JSON.stringify({
298285
model: useWandAzure ? wandModelName : 'gpt-4o',
299-
messages: messages,
286+
input: messages,
300287
temperature: 0.2,
301-
max_tokens: 10000,
288+
max_output_tokens: 10000,
302289
stream: true,
303-
stream_options: { include_usage: true },
304290
}),
305291
})
306292

@@ -331,6 +317,7 @@ Use this context to calculate relative dates like "yesterday", "last week", "beg
331317
let buffer = ''
332318
let chunkCount = 0
333319
let finalUsage: any = null
320+
let activeEventType: string | undefined
334321

335322
while (true) {
336323
const { done, value } = await reader.read()
@@ -348,27 +335,49 @@ Use this context to calculate relative dates like "yesterday", "last week", "beg
348335
buffer = lines.pop() || ''
349336

350337
for (const line of lines) {
351-
if (line.startsWith('data: ')) {
352-
const data = line.slice(6).trim()
338+
const trimmed = line.trim()
339+
if (!trimmed) {
340+
continue
341+
}
353342

354-
if (data === '[DONE]') {
355-
logger.info(`[${requestId}] Received [DONE] signal`)
343+
if (trimmed.startsWith('event:')) {
344+
activeEventType = trimmed.slice(6).trim()
345+
continue
346+
}
356347

357-
if (finalUsage) {
358-
await updateUserStatsForWand(session.user.id, finalUsage, requestId, isBYOK)
359-
}
348+
if (!trimmed.startsWith('data:')) {
349+
continue
350+
}
351+
352+
const data = trimmed.slice(5).trim()
353+
if (data === '[DONE]') {
354+
logger.info(`[${requestId}] Received [DONE] signal`)
360355

361-
controller.enqueue(
362-
encoder.encode(`data: ${JSON.stringify({ done: true })}\n\n`)
363-
)
364-
controller.close()
365-
return
356+
if (finalUsage) {
357+
await updateUserStatsForWand(session.user.id, finalUsage, requestId, isBYOK)
366358
}
367359

368-
try {
369-
const parsed = JSON.parse(data)
370-
const content = parsed.choices?.[0]?.delta?.content
360+
controller.enqueue(
361+
encoder.encode(`data: ${JSON.stringify({ done: true })}\n\n`)
362+
)
363+
controller.close()
364+
return
365+
}
366+
367+
try {
368+
const parsed = JSON.parse(data)
369+
const eventType = parsed?.type ?? activeEventType
370+
371+
if (
372+
eventType === 'response.error' ||
373+
eventType === 'error' ||
374+
activeEventType === 'response.failed'
375+
) {
376+
throw new Error(parsed?.error?.message || 'Responses stream error')
377+
}
371378

379+
if (eventType === 'response.output_text.delta') {
380+
const content = parsed.delta
372381
if (content) {
373382
chunkCount++
374383
if (chunkCount === 1) {
@@ -379,18 +388,23 @@ Use this context to calculate relative dates like "yesterday", "last week", "beg
379388
encoder.encode(`data: ${JSON.stringify({ chunk: content })}\n\n`)
380389
)
381390
}
391+
}
382392

383-
if (parsed.usage) {
384-
finalUsage = parsed.usage
393+
if (eventType === 'response.completed') {
394+
const usage = parseResponsesUsage(parsed?.response?.usage ?? parsed?.usage)
395+
if (usage) {
396+
finalUsage = {
397+
prompt_tokens: usage.promptTokens,
398+
completion_tokens: usage.completionTokens,
399+
total_tokens: usage.totalTokens,
400+
}
385401
logger.info(
386-
`[${requestId}] Received usage data: ${JSON.stringify(parsed.usage)}`
402+
`[${requestId}] Received usage data: ${JSON.stringify(finalUsage)}`
387403
)
388404
}
389-
} catch (parseError) {
390-
logger.debug(
391-
`[${requestId}] Skipped non-JSON line: ${data.substring(0, 100)}`
392-
)
393405
}
406+
} catch (parseError) {
407+
logger.debug(`[${requestId}] Skipped non-JSON line: ${data.substring(0, 100)}`)
394408
}
395409
}
396410
}
@@ -424,8 +438,6 @@ Use this context to calculate relative dates like "yesterday", "last week", "beg
424438
message: error?.message || 'Unknown error',
425439
code: error?.code,
426440
status: error?.status,
427-
responseStatus: error?.response?.status,
428-
responseData: error?.response?.data ? safeStringify(error.response.data) : undefined,
429441
stack: error?.stack,
430442
useWandAzure,
431443
model: useWandAzure ? wandModelName : 'gpt-4o',
@@ -440,14 +452,43 @@ Use this context to calculate relative dates like "yesterday", "last week", "beg
440452
}
441453
}
442454

443-
const completion = await activeClient.chat.completions.create({
444-
model: useWandAzure ? wandModelName : 'gpt-4o',
445-
messages: messages,
446-
temperature: 0.3,
447-
max_tokens: 10000,
455+
const apiUrl = useWandAzure
456+
? `${azureEndpoint}/openai/v1/responses?api-version=${azureApiVersion}`
457+
: 'https://api.openai.com/v1/responses'
458+
459+
const headers: Record<string, string> = {
460+
'Content-Type': 'application/json',
461+
'OpenAI-Beta': 'responses=v1',
462+
}
463+
464+
if (useWandAzure) {
465+
headers['api-key'] = azureApiKey!
466+
} else {
467+
headers.Authorization = `Bearer ${activeOpenAIKey}`
468+
}
469+
470+
const response = await fetch(apiUrl, {
471+
method: 'POST',
472+
headers,
473+
body: JSON.stringify({
474+
model: useWandAzure ? wandModelName : 'gpt-4o',
475+
input: messages,
476+
temperature: 0.3,
477+
max_output_tokens: 10000,
478+
}),
448479
})
449480

450-
const generatedContent = completion.choices[0]?.message?.content?.trim()
481+
if (!response.ok) {
482+
const errorText = await response.text()
483+
const apiError = new Error(
484+
`API request failed: ${response.status} ${response.statusText} - ${errorText}`
485+
)
486+
;(apiError as any).status = response.status
487+
throw apiError
488+
}
489+
490+
const completion = await response.json()
491+
const generatedContent = extractResponseText(completion.output)?.trim()
451492

452493
if (!generatedContent) {
453494
logger.error(
@@ -461,8 +502,18 @@ Use this context to calculate relative dates like "yesterday", "last week", "beg
461502

462503
logger.info(`[${requestId}] Wand generation successful`)
463504

464-
if (completion.usage) {
465-
await updateUserStatsForWand(session.user.id, completion.usage, requestId, isBYOK)
505+
const usage = parseResponsesUsage(completion.usage)
506+
if (usage) {
507+
await updateUserStatsForWand(
508+
session.user.id,
509+
{
510+
prompt_tokens: usage.promptTokens,
511+
completion_tokens: usage.completionTokens,
512+
total_tokens: usage.totalTokens,
513+
},
514+
requestId,
515+
isBYOK
516+
)
466517
}
467518

468519
return NextResponse.json({ success: true, content: generatedContent })
@@ -472,10 +523,6 @@ Use this context to calculate relative dates like "yesterday", "last week", "beg
472523
message: error?.message || 'Unknown error',
473524
code: error?.code,
474525
status: error?.status,
475-
responseStatus: error instanceof OpenAI.APIError ? error.status : error?.response?.status,
476-
responseData: (error as any)?.response?.data
477-
? safeStringify((error as any).response.data)
478-
: undefined,
479526
stack: error?.stack,
480527
useWandAzure,
481528
model: useWandAzure ? wandModelName : 'gpt-4o',
@@ -484,26 +531,19 @@ Use this context to calculate relative dates like "yesterday", "last week", "beg
484531
})
485532

486533
let clientErrorMessage = 'Wand generation failed. Please try again later.'
487-
let status = 500
534+
let status = typeof (error as any)?.status === 'number' ? (error as any).status : 500
488535

489-
if (error instanceof OpenAI.APIError) {
490-
status = error.status || 500
491-
logger.error(
492-
`[${requestId}] ${useWandAzure ? 'Azure OpenAI' : 'OpenAI'} API Error: ${status} - ${error.message}`
493-
)
494-
495-
if (status === 401) {
496-
clientErrorMessage = 'Authentication failed. Please check your API key configuration.'
497-
} else if (status === 429) {
498-
clientErrorMessage = 'Rate limit exceeded. Please try again later.'
499-
} else if (status >= 500) {
500-
clientErrorMessage =
501-
'The wand generation service is currently unavailable. Please try again later.'
502-
}
503-
} else if (useWandAzure && error.message?.includes('DeploymentNotFound')) {
536+
if (useWandAzure && error?.message?.includes('DeploymentNotFound')) {
504537
clientErrorMessage =
505538
'Azure OpenAI deployment not found. Please check your model deployment configuration.'
506539
status = 404
540+
} else if (status === 401) {
541+
clientErrorMessage = 'Authentication failed. Please check your API key configuration.'
542+
} else if (status === 429) {
543+
clientErrorMessage = 'Rate limit exceeded. Please try again later.'
544+
} else if (status >= 500) {
545+
clientErrorMessage =
546+
'The wand generation service is currently unavailable. Please try again later.'
507547
}
508548

509549
return NextResponse.json(

0 commit comments

Comments
 (0)