diff --git a/src/server/middleware/openai-proxy.middleware.ts b/src/server/middleware/openai-proxy.middleware.ts index d5956c9..85f7ba9 100644 --- a/src/server/middleware/openai-proxy.middleware.ts +++ b/src/server/middleware/openai-proxy.middleware.ts @@ -20,16 +20,26 @@ export const proxyOpenAI = async ( const upstreamUrl = getUpstreamURL(provider, targetPath) const headers = prepareHeaders(provider) - let modifiedBody = body as Record - if (modifiedBody?.model === 'gpt-5-high') { - modifiedBody.model = 'gpt-5' - modifiedBody.reasoning_effort = 'high' + let requestBody: BodyInit | undefined + + if (body instanceof FormData) { + headers.delete('Content-Type') + requestBody = body + } else { + let modifiedBody = body as Record + if (modifiedBody?.model === 'gpt-5-high') { + modifiedBody.model = 'gpt-5' + if (targetPath !== '/v1/responses') { + modifiedBody.reasoning_effort = 'high' + } + } + requestBody = req.method === 'POST' && modifiedBody ? JSON.stringify(modifiedBody) : undefined } const response = await fetch(upstreamUrl, { method: req.method, - headers: headers, - body: req.method === 'POST' && modifiedBody ? JSON.stringify(modifiedBody) : undefined, + headers, + body: requestBody, }) const originalHeaders = response.headers diff --git a/src/server/routes/anthropic-proxy.route.ts b/src/server/routes/anthropic-proxy.route.ts index 7c92250..8e8bded 100644 --- a/src/server/routes/anthropic-proxy.route.ts +++ b/src/server/routes/anthropic-proxy.route.ts @@ -7,7 +7,7 @@ import { proxyAnthropic } from '../middleware/anthropic-proxy.middleware' import { chatCompletionsValidator } from './proxy.route' const anthropicChatCompletions: MiddlewareHandler = async c => { - const result = await c.req.valid('json') + const result = (c.req as any).valid('json') console.log("result", result) return proxyAnthropic(c, result, '/v1/messages') } diff --git a/src/server/routes/proxy.route.ts b/src/server/routes/proxy.route.ts index ad63fe9..d6a2ae4 100644 --- a/src/server/routes/proxy.route.ts +++ b/src/server/routes/proxy.route.ts @@ -8,8 +8,8 @@ import { validateAPIToken } from '../middleware/validate-token.middleware' import { MiddlewareHandler, Context } from 'hono' import { Env } from '../types' -export const processRequest = async (c:Context) => { - const result = await c.req.valid('json') +export const processRequest = async (c: Context) => { + const result = (c.req as any).valid('json') let model = null; if ('model' in result) { // Get model from body @@ -34,6 +34,33 @@ export const processRequest = async (c:Context) => { } } +export const processRealtimeRequest = async ( + c: Context, + extractModel: (payload: any, c: Context) => string | undefined +) => { + const result = (c.req as any).valid('json') + const model = extractModel(result, c) + + if (!model) { + throw new HTTPException(400, { + message: 'Model missing', + }) + } + + const modelsController = new ModelController(c.env.redis) + const provider = await modelsController.findProviderByModel(model) + if (!provider) { + throw new HTTPException(401, { + message: 'Model unsupported', + }) + } + return { + result, + provider, + } +} + + export const completions: MiddlewareHandler = async c => { const { result, provider } = await processRequest(c) return proxyOpenAI(c, result, provider, '/v1/completions') @@ -49,6 +76,89 @@ export const embeddings: MiddlewareHandler = async c => { return proxyOpenAI(c, result, provider, '/v1/embeddings') } +export const responses: MiddlewareHandler = async c => { + const { result, provider } = await processRequest(c) + return proxyOpenAI(c, result, provider, '/v1/responses') +} + +export const realtimeClientSecrets: MiddlewareHandler = async c => { + const { result, provider } = await processRealtimeRequest( + c, + payload => (payload as any).session?.model + ) + return proxyOpenAI(c, result, provider, '/v1/realtime/client_secrets') +} + +export const realtimeCalls: MiddlewareHandler = async c => { + let model: string + const formData = new FormData() + + if (c.req.header('content-type')?.includes('multipart/form-data')) { + const incoming = await c.req.formData() + const sessionData = incoming.get('session') + const sdpData = incoming.get('sdp') + + if (!sessionData || !sdpData) { + throw new HTTPException(400, { message: 'Invalid realtime payload' }) + } + + const rawSession = + typeof sessionData === 'string' + ? sessionData + : await (sessionData as Blob).text() + + const session = JSON.parse(rawSession.split(';')[0]) + model = session.model + + const sdpText = + typeof sdpData === 'string' + ? sdpData + : await (sdpData as Blob).text() + formData.set('sdp', sdpText) + formData.set('session', rawSession.split(';')[0]) + } else { + model = c.req.query('model') as string + formData.set('sdp', await c.req.text()) + formData.set('session', JSON.stringify({ model, modalities: ['audio', 'text'] })) + } + + const modelsController = new ModelController(c.env.redis) + const provider = await modelsController.findProviderByModel(model) + + if (!provider) { + throw new HTTPException(401, { message: 'Model unsupported' }) + } + + return proxyOpenAI(c, formData, provider, '/v1/realtime/calls') +} + +export const realtimeCallAccept: MiddlewareHandler = async c => { + const { result, provider } = await processRealtimeRequest( + c, + payload => (payload as any).model + ) + const callId = (c.req.param() as Record).callId + return proxyOpenAI(c, result, provider, `/v1/realtime/calls/${callId}/accept`) +} + +export const realtimeCallReject: MiddlewareHandler = async c => { + const { result, provider } = await processRealtimeRequest( + c, + payload => (payload as any).model + ) + const callId = (c.req.param() as Record).callId + return proxyOpenAI(c, result, provider, `/v1/realtime/calls/${callId}/reject`) +} + +export const realtimeCallHangup: MiddlewareHandler = async c => { + const { result, provider } = await processRealtimeRequest( + c, + payload => (payload as any).model + ) + const callId = (c.req.param() as Record).callId + return proxyOpenAI(c, result, provider, `/v1/realtime/calls/${callId}/hangup`) +} + export const models: MiddlewareHandler = async c => { const redis = c.env.redis const modelController = new ModelController(redis) @@ -95,6 +205,18 @@ export const embeddingsValidator = z.object({ ]).transform(val => Array.isArray(val) ? val : [val]) }).passthrough() +export const responsesValidator = z.object({ + model: z.string().optional(), + input: z.union([ + z.string(), + z.array(z.any()) + ]).optional() +}).passthrough() + +export const realtimeValidator = z.object({ + model: z.string().optional() +}).passthrough() + handler // OPENAI APIs .post( @@ -115,6 +237,41 @@ handler validateAPIToken, embeddings ) + .post( + '/v1/responses', + zValidator('json', responsesValidator), + validateAPIToken, + responses + ) + .post( + '/v1/realtime/client_secrets', + zValidator('json', realtimeValidator), + validateAPIToken, + realtimeClientSecrets + ) + .post( + '/v1/realtime/calls', + validateAPIToken, + realtimeCalls + ) + .post( + '/v1/realtime/calls/:callId/accept', + zValidator('json', realtimeValidator), + validateAPIToken, + realtimeCallAccept + ) + .post( + '/v1/realtime/calls/:callId/reject', + zValidator('json', realtimeValidator), + validateAPIToken, + realtimeCallReject + ) + .post( + '/v1/realtime/calls/:callId/hangup', + zValidator('json', realtimeValidator), + validateAPIToken, + realtimeCallHangup + ) .get( '/v1/models', validateAPIToken, @@ -139,5 +296,11 @@ handler validateAPIToken, embeddings ) + .post( + '/deployments/*/responses', + zValidator('json', responsesValidator), + validateAPIToken, + responses + ) export default handler