Mints ephemeral client secrets by proxying to OpenAI’s Realtime API.Request headers:
Content-Type: application/json
Request body:
{ "model": "gpt-realtime", "voice": "marin", "instructions": "You are a helpful assistant.", "language": "Spanish", "voiceAccent": "neutral Latin American Spanish", "voiceTone": "friendly and professional", "apiKey": "sk-..."}
Client-provided API key (only if allowed by backend)
Response (200):
{ "value": "ek_...", "expires_at": 1730000000}
Error responses:
// 400: Invalid TTL{ "error": "clientSecretTtlSeconds must be between 10 and 7200. Received: 5"}// 401: Missing/invalid API key{ "error": "Missing API key. Configure openaiApiKey or send apiKey in request."}// 403: Request API key disabled{ "error": "Passing apiKey from request is disabled. Set allowApiKeyFromRequest=true to enable it."}
use App\Http\Controllers\NavaiController;Route::post('/navai/realtime/client-secret', [NavaiController::class, 'clientSecret']);Route::get('/navai/functions', [NavaiController::class, 'listFunctions']);Route::post('/navai/functions/execute', [NavaiController::class, 'executeFunction']);
3
Create controller
Create app/Http/Controllers/NavaiController.php:
<?phpnamespace App\Http\Controllers;use Illuminate\Http\Request;use Illuminate\Support\Facades\Http;class NavaiController extends Controller{ private const OPENAI_CLIENT_SECRETS_URL = 'https://api.openai.com/v1/realtime/client_secrets'; private const MIN_TTL_SECONDS = 10; private const MAX_TTL_SECONDS = 7200; public function clientSecret(Request $request) { $apiKey = $this->resolveApiKey($request); $ttl = (int) env('OPENAI_REALTIME_CLIENT_SECRET_TTL', 600); if ($ttl < self::MIN_TTL_SECONDS || $ttl > self::MAX_TTL_SECONDS) { return response()->json([ 'error' => "clientSecretTtlSeconds must be between " . self::MIN_TTL_SECONDS . " and " . self::MAX_TTL_SECONDS ], 400); } $model = $request->input('model', env('OPENAI_REALTIME_MODEL', 'gpt-realtime')); $voice = $request->input('voice', env('OPENAI_REALTIME_VOICE', 'marin')); $instructions = $this->buildInstructions($request); $response = Http::withHeaders([ 'Authorization' => 'Bearer ' . $apiKey, 'Content-Type' => 'application/json' ])->post(self::OPENAI_CLIENT_SECRETS_URL, [ 'expires_after' => [ 'anchor' => 'created_at', 'seconds' => $ttl ], 'session' => [ 'type' => 'realtime', 'model' => $model, 'instructions' => $instructions, 'audio' => [ 'output' => ['voice' => $voice] ] ] ]); if (!$response->successful()) { return response()->json([ 'error' => 'OpenAI client_secrets failed: ' . $response->body() ], $response->status()); } $data = $response->json(); return response()->json([ 'value' => $data['value'], 'expires_at' => $data['expires_at'] ]); } public function listFunctions() { // Implement your function discovery logic $functions = $this->discoverFunctions(); return response()->json([ 'items' => $functions, 'warnings' => [] ]); } public function executeFunction(Request $request) { $functionName = $request->input('function_name'); $payload = $request->input('payload', []); if (empty($functionName)) { return response()->json(['error' => 'function_name is required.'], 400); } // Implement your function execution logic $result = $this->runFunction($functionName, $payload, $request); if ($result === null) { return response()->json([ 'error' => 'Unknown or disallowed function.', 'available_functions' => array_column($this->discoverFunctions(), 'name') ], 404); } return response()->json([ 'ok' => true, 'function_name' => $functionName, 'source' => 'php', 'result' => $result ]); } private function resolveApiKey(Request $request): string { $backendKey = env('OPENAI_API_KEY'); if (!empty($backendKey)) { return $backendKey; } $requestKey = $request->input('apiKey'); $allowRequestKey = env('NAVAI_ALLOW_FRONTEND_API_KEY', false); if (!empty($requestKey) && $allowRequestKey) { return $requestKey; } abort(401, 'Missing API key.'); } private function buildInstructions(Request $request): string { $lines = [ $request->input('instructions', env('OPENAI_REALTIME_INSTRUCTIONS', 'You are a helpful assistant.')) ]; $language = $request->input('language', env('OPENAI_REALTIME_LANGUAGE')); if (!empty($language)) { $lines[] = "Always reply in {$language}."; } $accent = $request->input('voiceAccent', env('OPENAI_REALTIME_VOICE_ACCENT')); if (!empty($accent)) { $lines[] = "Use a {$accent} accent while speaking."; } $tone = $request->input('voiceTone', env('OPENAI_REALTIME_VOICE_TONE')); if (!empty($tone)) { $lines[] = "Use a {$tone} tone while speaking."; } return implode("\n", $lines); } private function discoverFunctions(): array { // Implement your function discovery return [ [ 'name' => 'get_weather', 'description' => 'Get weather for a location', 'source' => 'app/Navai/Functions/Weather.php' ] ]; } private function runFunction(string $name, array $payload, Request $request) { // Implement your function execution return match($name) { 'get_weather' => $this->getWeather($payload['args'][0] ?? null), default => null }; } private function getWeather(?string $location) { if (empty($location)) { return ['error' => 'Location required']; } return ['temperature' => 72, 'conditions' => 'sunny', 'location' => $location]; }}
4
Configure environment
Add to .env:
OPENAI_API_KEY=sk-...OPENAI_REALTIME_MODEL=gpt-realtimeOPENAI_REALTIME_VOICE=marinOPENAI_REALTIME_INSTRUCTIONS="You are a helpful assistant."OPENAI_REALTIME_CLIENT_SECRET_TTL=600NAVAI_ALLOW_FRONTEND_API_KEY=false
require 'net/http'require 'json'class NavaiController < ApplicationController skip_before_action :verify_authenticity_token OPENAI_CLIENT_SECRETS_URL = 'https://api.openai.com/v1/realtime/client_secrets'.freeze MIN_TTL_SECONDS = 10 MAX_TTL_SECONDS = 7200 def client_secret api_key = resolve_api_key ttl = (ENV['OPENAI_REALTIME_CLIENT_SECRET_TTL'] || '600').to_i if ttl < MIN_TTL_SECONDS || ttl > MAX_TTL_SECONDS return render json: { error: "clientSecretTtlSeconds must be between #{MIN_TTL_SECONDS} and #{MAX_TTL_SECONDS}" }, status: :bad_request end model = params[:model] || ENV['OPENAI_REALTIME_MODEL'] || 'gpt-realtime' voice = params[:voice] || ENV['OPENAI_REALTIME_VOICE'] || 'marin' instructions = build_instructions uri = URI(OPENAI_CLIENT_SECRETS_URL) request = Net::HTTP::Post.new(uri) request['Authorization'] = "Bearer #{api_key}" request['Content-Type'] = 'application/json' request.body = { expires_after: { anchor: 'created_at', seconds: ttl }, session: { type: 'realtime', model: model, instructions: instructions, audio: { output: { voice: voice } } } }.to_json response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end unless response.is_a?(Net::HTTPSuccess) return render json: { error: "OpenAI client_secrets failed: #{response.body}" }, status: response.code.to_i end data = JSON.parse(response.body) render json: { value: data['value'], expires_at: data['expires_at'] } rescue StandardError => e render json: { error: e.message }, status: :internal_server_error end def list_functions functions = discover_functions render json: { items: functions, warnings: [] } end def execute_function function_name = params[:function_name]&.strip unless function_name return render json: { error: 'function_name is required.' }, status: :bad_request end payload = params[:payload] || {} result = run_function(function_name, payload) if result.nil? return render json: { error: 'Unknown or disallowed function.', available_functions: discover_functions.map { |f| f[:name] } }, status: :not_found end render json: { ok: true, function_name: function_name, source: 'ruby', result: result } end private def resolve_api_key backend_key = ENV['OPENAI_API_KEY']&.strip return backend_key if backend_key.present? request_key = params[:apiKey]&.strip allow_request_key = ENV['NAVAI_ALLOW_FRONTEND_API_KEY']&.downcase == 'true' if request_key.present? && allow_request_key return request_key end raise 'Missing API key' end def build_instructions lines = [ params[:instructions] || ENV['OPENAI_REALTIME_INSTRUCTIONS'] || 'You are a helpful assistant.' ] language = params[:language] || ENV['OPENAI_REALTIME_LANGUAGE'] lines << "Always reply in #{language}." if language.present? accent = params[:voiceAccent] || ENV['OPENAI_REALTIME_VOICE_ACCENT'] lines << "Use a #{accent} accent while speaking." if accent.present? tone = params[:voiceTone] || ENV['OPENAI_REALTIME_VOICE_TONE'] lines << "Use a #{tone} tone while speaking." if tone.present? lines.join("\n") end def discover_functions [ { name: 'get_weather', description: 'Get weather for a location', source: 'app/navai/functions/weather.rb' } ] end def run_function(name, payload) case name when 'get_weather' location = payload.dig('args', 0) { temperature: 72, conditions: 'sunny', location: location } else nil end endend
2
Add routes
Add to config/routes.rb:
Rails.application.routes.draw do post 'navai/realtime/client-secret', to: 'navai#client_secret' get 'navai/functions', to: 'navai#list_functions' post 'navai/functions/execute', to: 'navai#execute_function'end