Realtime: migrate to OpenAI GA shape (beta API was retired)
Upstream returned 'beta_api_shape_disabled' on every wake. Changes:
- Drop OpenAI-Beta: realtime=v1 header from the upgrade.
- session.update payload nests audio under session.audio.{input,output}
and adds session.type='realtime'; audio.{input,output}.format is now
an object {type:audio/pcm, rate:24000} instead of 'pcm16' string.
- Default model bumped to gpt-realtime (the GA name); the relay also
promotes any stored 'gpt-4o-realtime-preview*' to gpt-realtime so
pre-Plan-3 DeviceConfig rows keep working without a manual DB edit.
This commit is contained in:
@@ -130,8 +130,14 @@ public static class DeviceHubEndpoint
|
|||||||
{
|
{
|
||||||
var enabledTools = JsonSerializer.Deserialize<string[]>(device.Config?.EnabledToolsJson ?? "[]")
|
var enabledTools = JsonSerializer.Deserialize<string[]>(device.Config?.EnabledToolsJson ?? "[]")
|
||||||
?? Array.Empty<string>();
|
?? Array.Empty<string>();
|
||||||
|
// Promote any deprecated Beta-era model name to the GA name. Plan 4's
|
||||||
|
// admin UI will let users set a specific value; until then, anything
|
||||||
|
// that mentions the old preview slug routes to gpt-realtime.
|
||||||
|
var model = device.Config?.Model ?? "gpt-realtime";
|
||||||
|
if (model.StartsWith("gpt-4o-realtime-preview", StringComparison.Ordinal))
|
||||||
|
model = "gpt-realtime";
|
||||||
var cfg = new RealtimeSettings(
|
var cfg = new RealtimeSettings(
|
||||||
Model: device.Config?.Model ?? "gpt-4o-realtime-preview",
|
Model: model,
|
||||||
Voice: device.Config?.Voice ?? "alloy",
|
Voice: device.Config?.Voice ?? "alloy",
|
||||||
SystemPrompt: device.Config?.SystemPrompt ?? "",
|
SystemPrompt: device.Config?.SystemPrompt ?? "",
|
||||||
IdleTimeoutSeconds: device.Config?.IdleTimeoutSeconds ?? 30,
|
IdleTimeoutSeconds: device.Config?.IdleTimeoutSeconds ?? 30,
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ public class OpenAIRealtimeUpstream : IRealtimeUpstream
|
|||||||
public async Task ConnectAsync(string apiKey, string model, CancellationToken ct)
|
public async Task ConnectAsync(string apiKey, string model, CancellationToken ct)
|
||||||
{
|
{
|
||||||
_ws.Options.SetRequestHeader("Authorization", $"Bearer {apiKey}");
|
_ws.Options.SetRequestHeader("Authorization", $"Bearer {apiKey}");
|
||||||
_ws.Options.SetRequestHeader("OpenAI-Beta", "realtime=v1");
|
// The GA Realtime API does NOT take the OpenAI-Beta: realtime=v1 header.
|
||||||
|
// Sending it returns upstream error code=beta_api_shape_disabled.
|
||||||
var uri = new Uri($"wss://api.openai.com/v1/realtime?model={Uri.EscapeDataString(model)}");
|
var uri = new Uri($"wss://api.openai.com/v1/realtime?model={Uri.EscapeDataString(model)}");
|
||||||
await _ws.ConnectAsync(uri, ct);
|
await _ws.ConnectAsync(uri, ct);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ namespace backend.Realtime;
|
|||||||
|
|
||||||
public static class RealtimeEvents
|
public static class RealtimeEvents
|
||||||
{
|
{
|
||||||
|
// GA Realtime API shape (the Beta API shape was retired in 2026 with
|
||||||
|
// upstream error "beta_api_shape_disabled"). Key differences:
|
||||||
|
// - session.type = "realtime" is required.
|
||||||
|
// - voice / audio formats / transcription / turn_detection now nested
|
||||||
|
// under session.audio.{input,output}.
|
||||||
|
// - No OpenAI-Beta header on the upgrade (see OpenAIRealtimeUpstream).
|
||||||
public static JsonObject SessionUpdate(RealtimeSettings cfg, IEnumerable<ITool> enabledTools)
|
public static JsonObject SessionUpdate(RealtimeSettings cfg, IEnumerable<ITool> enabledTools)
|
||||||
{
|
{
|
||||||
var tools = new JsonArray();
|
var tools = new JsonArray();
|
||||||
@@ -25,12 +31,19 @@ public static class RealtimeEvents
|
|||||||
["type"] = "session.update",
|
["type"] = "session.update",
|
||||||
["session"] = new JsonObject
|
["session"] = new JsonObject
|
||||||
{
|
{
|
||||||
["voice"] = cfg.Voice,
|
["type"] = "realtime",
|
||||||
|
["model"] = cfg.Model,
|
||||||
["instructions"] = cfg.SystemPrompt,
|
["instructions"] = cfg.SystemPrompt,
|
||||||
["modalities"] = new JsonArray("text", "audio"),
|
["audio"] = new JsonObject
|
||||||
["input_audio_format"] = "pcm16",
|
{
|
||||||
["output_audio_format"] = "pcm16",
|
["input"] = new JsonObject
|
||||||
["input_audio_transcription"] = new JsonObject { ["model"] = "whisper-1" },
|
{
|
||||||
|
["format"] = new JsonObject
|
||||||
|
{
|
||||||
|
["type"] = "audio/pcm",
|
||||||
|
["rate"] = 24000,
|
||||||
|
},
|
||||||
|
["transcription"] = new JsonObject { ["model"] = "whisper-1" },
|
||||||
["turn_detection"] = new JsonObject
|
["turn_detection"] = new JsonObject
|
||||||
{
|
{
|
||||||
["type"] = "server_vad",
|
["type"] = "server_vad",
|
||||||
@@ -38,6 +51,17 @@ public static class RealtimeEvents
|
|||||||
["prefix_padding_ms"] = 300,
|
["prefix_padding_ms"] = 300,
|
||||||
["silence_duration_ms"] = 500,
|
["silence_duration_ms"] = 500,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
["output"] = new JsonObject
|
||||||
|
{
|
||||||
|
["voice"] = cfg.Voice,
|
||||||
|
["format"] = new JsonObject
|
||||||
|
{
|
||||||
|
["type"] = "audio/pcm",
|
||||||
|
["rate"] = 24000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
["tools"] = tools,
|
["tools"] = tools,
|
||||||
["tool_choice"] = "auto",
|
["tool_choice"] = "auto",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ public class SystemSettings
|
|||||||
public string DefaultSystemPrompt { get; set; } =
|
public string DefaultSystemPrompt { get; set; } =
|
||||||
"You are a helpful voice assistant. Keep responses concise.";
|
"You are a helpful voice assistant. Keep responses concise.";
|
||||||
public string DefaultVoice { get; set; } = "alloy";
|
public string DefaultVoice { get; set; } = "alloy";
|
||||||
public string DefaultModel { get; set; } = "gpt-4o-realtime-preview";
|
public string DefaultModel { get; set; } = "gpt-realtime";
|
||||||
public int DefaultIdleTimeoutSeconds { get; set; } = 30;
|
public int DefaultIdleTimeoutSeconds { get; set; } = 30;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user