Trim GA session.update + add deploy marker + surface upstream silence

The previous run showed session_started, then total silence from the
relay for the entire conversation — no events forwarded, no errors,
nothing. Two changes to disambiguate:

1. Drop the explicit audio.input.format / audio.output.format objects.
   24 kHz PCM16 mono is the documented GA default for both directions
   and the only thing OpenAI ever advertised; an unrecognised value
   inside the format object is the most likely reason OpenAI silently
   ignores incoming audio.

2. Forward 'upstream silent NxN250ms' debug envelopes every ~2 s when
   ReceiveJsonAsync keeps returning null, and add a 'server_version'
   field to hello_ack so the Pi journal proves which build is running.
This commit is contained in:
2026-06-12 07:04:53 +00:00
parent a42038b825
commit 12be767325
3 changed files with 22 additions and 17 deletions
+6 -1
View File
@@ -177,6 +177,10 @@ public static class DeviceHubEndpoint
}, ct); }, ct);
} }
// Bump on every backend change so we can verify a Coolify deploy landed
// by looking at the hello_ack config.server_version field in the Pi journal.
public const string ServerVersion = "plan3-debug-2026-06-12-09";
private static async Task SendHelloAckAsync(ActiveDevice active, Device device, CancellationToken ct) private static async Task SendHelloAckAsync(ActiveDevice active, Device device, CancellationToken ct)
{ {
var enabled = device.Config?.EnabledToolsJson ?? "[]"; var enabled = device.Config?.EnabledToolsJson ?? "[]";
@@ -186,7 +190,8 @@ public static class DeviceHubEndpoint
"model": "{{Escape(device.Config?.Model)}}", "model": "{{Escape(device.Config?.Model)}}",
"system_prompt": "{{Escape(device.Config?.SystemPrompt)}}", "system_prompt": "{{Escape(device.Config?.SystemPrompt)}}",
"idle_timeout_seconds": {{device.Config?.IdleTimeoutSeconds ?? 30}}, "idle_timeout_seconds": {{device.Config?.IdleTimeoutSeconds ?? 30}},
"enabled_tools": {{enabled}} "enabled_tools": {{enabled}},
"server_version": "{{ServerVersion}}"
} }
""")!.AsObject(); """)!.AsObject();
+5 -13
View File
@@ -26,6 +26,11 @@ public static class RealtimeEvents
}); });
} }
// Minimal GA session.update. We drop explicit audio.input/output.format
// objects (the GA shape accepts them but OpenAI defaults to 24 kHz PCM16
// mono on both directions, which is what we use). Removing the explicit
// format objects matches the documented minimal session config and
// eliminates a known source of OpenAI silently ignoring the audio.
return new JsonObject return new JsonObject
{ {
["type"] = "session.update", ["type"] = "session.update",
@@ -38,11 +43,6 @@ public static class RealtimeEvents
{ {
["input"] = new JsonObject ["input"] = new JsonObject
{ {
["format"] = new JsonObject
{
["type"] = "audio/pcm",
["rate"] = 24000,
},
["transcription"] = new JsonObject { ["model"] = "whisper-1" }, ["transcription"] = new JsonObject { ["model"] = "whisper-1" },
["turn_detection"] = new JsonObject ["turn_detection"] = new JsonObject
{ {
@@ -50,9 +50,6 @@ public static class RealtimeEvents
["threshold"] = 0.5, ["threshold"] = 0.5,
["prefix_padding_ms"] = 300, ["prefix_padding_ms"] = 300,
["silence_duration_ms"] = 500, ["silence_duration_ms"] = 500,
// GA requires both explicitly — without them the
// model never auto-generates a reply after VAD
// detects speech_stopped.
["create_response"] = true, ["create_response"] = true,
["interrupt_response"] = true, ["interrupt_response"] = true,
}, },
@@ -60,11 +57,6 @@ public static class RealtimeEvents
["output"] = new JsonObject ["output"] = new JsonObject
{ {
["voice"] = cfg.Voice, ["voice"] = cfg.Voice,
["format"] = new JsonObject
{
["type"] = "audio/pcm",
["rate"] = 24000,
},
}, },
}, },
["tools"] = tools, ["tools"] = tools,
+11 -3
View File
@@ -128,10 +128,18 @@ public class RealtimeSession
if (evt is null && ct.IsCancellationRequested) return; if (evt is null && ct.IsCancellationRequested) return;
if (evt is null) if (evt is null)
{ {
// Upstream has gone silent. If null persists for >2s, log so // Upstream has gone silent. Surface that to the device every
// we can spot a dead upstream that's not surfacing as a close. // ~2s so we can tell "OpenAI is up but emitting nothing" from
// "the upstream WS was closed" — Coolify logs aren't reachable.
if (++nullPolls % 8 == 0) if (++nullPolls % 8 == 0)
_logger.LogDebug("upstream silent ({Polls} x 250ms polls)", nullPolls); {
_logger.LogInformation("upstream silent ({Polls} x 250ms polls)", nullPolls);
await _output.WriteEnvelopeAsync(new JsonObject
{
["type"] = "debug",
["message"] = "upstream silent " + nullPolls + "x250ms",
}, ct);
}
continue; continue;
} }
nullPolls = 0; nullPolls = 0;