





















@@ -90,6 +90,7 @@ const OPENAI_REALTIME_ACTIVE_RESPONSE_ERROR_PREFIX =
9090"Conversation already has an active response in progress:";
9191const OPENAI_REALTIME_NO_ACTIVE_RESPONSE_CANCEL_ERROR =
9292"Cancellation failed: no active response found";
93+const OPENAI_REALTIME_MAX_SESSION_DURATION_FRAGMENT = "maximum duration";
9394const OPENAI_REALTIME_DEFAULT_MIN_BARGE_IN_AUDIO_END_MS = 250;
9495const OPENAI_REALTIME_VOICES = [
9596"alloy",
@@ -330,6 +331,14 @@ function prefersCodexOAuthForRealtimeModel(model: string | undefined): boolean {
330331return (model ?? OPENAI_REALTIME_DEFAULT_MODEL).trim().toLowerCase().startsWith("gpt-");
331332}
332333334+function isOpenAIRealtimeMaxSessionDurationError(detail: string): boolean {
335+const normalized = detail.toLowerCase();
336+return (
337+normalized.includes("session") &&
338+normalized.includes(OPENAI_REALTIME_MAX_SESSION_DURATION_FRAGMENT)
339+);
340+}
341+333342async function resolveOpenAIRealtimeDefaultAuth(params: {
334343configuredApiKey: string | undefined;
335344cfg: RealtimeVoiceBrowserSessionCreateRequest["cfg"] | undefined;
@@ -426,6 +435,8 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
426435private deliveredToolCallKeys = new Set<string>();
427436private readonly flowId = randomUUID();
428437private sessionReadyFired = false;
438+private reconnectReason: string | undefined;
439+private activeConnectionReason: string | undefined;
429440private readonly audioFormat: RealtimeVoiceAudioFormat;
430441431442constructor(private readonly config: OpenAIRealtimeVoiceBridgeConfig) {
@@ -658,7 +669,9 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
658669settleReject(new Error("OpenAI realtime connection closed before ready"));
659670return;
660671}
661-void this.attemptReconnect();
672+const reason = this.reconnectReason ?? "websocket-close";
673+this.reconnectReason = undefined;
674+void this.attemptReconnect(reason);
662675});
663676};
664677@@ -833,26 +846,41 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
833846};
834847}
835848836-private async attemptReconnect(): Promise<void> {
849+private async attemptReconnect(reason: string): Promise<void> {
837850if (this.intentionallyClosed) {
838851return;
839852}
840853if (this.reconnectAttempts >= OpenAIRealtimeVoiceBridge.MAX_RECONNECT_ATTEMPTS) {
854+this.config.onEvent?.({
855+direction: "client",
856+type: "session.reconnect.exhausted",
857+detail: `reason=${reason} attempts=${this.reconnectAttempts}`,
858+});
841859this.config.onClose?.("error");
842860return;
843861}
844862this.reconnectAttempts += 1;
845-const delay =
846-OpenAIRealtimeVoiceBridge.BASE_RECONNECT_DELAY_MS * 2 ** (this.reconnectAttempts - 1);
863+const attempt = this.reconnectAttempts;
864+const delay = OpenAIRealtimeVoiceBridge.BASE_RECONNECT_DELAY_MS * 2 ** (attempt - 1);
865+this.config.onEvent?.({
866+direction: "client",
867+type: "session.reconnect.scheduled",
868+detail: `reason=${reason} attempt=${attempt} delayMs=${delay}`,
869+});
847870await new Promise((resolve) => setTimeout(resolve, delay));
848871if (this.intentionallyClosed) {
849872return;
850873}
851874try {
852875await this.doConnect();
876+this.config.onEvent?.({
877+direction: "client",
878+type: "session.reconnect.ready",
879+detail: `reason=${reason} attempt=${attempt}`,
880+});
853881} catch (error) {
854882this.config.onError?.(error instanceof Error ? error : new Error(String(error)));
855-await this.attemptReconnect();
883+await this.attemptReconnect(reason);
856884}
857885}
858886@@ -951,11 +979,27 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
951979}
952980953981private handleEvent(event: RealtimeEvent): void {
954-this.config.onEvent?.({
955-direction: "server",
956-type: event.type,
957-detail: this.describeServerEvent(event),
958-});
982+const emitServerEvent = () =>
983+this.config.onEvent?.({
984+direction: "server",
985+type: event.type,
986+detail: this.describeServerEvent(event),
987+});
988+if (
989+event.type === "error" &&
990+isOpenAIRealtimeMaxSessionDurationError(readRealtimeErrorDetail(event.error))
991+) {
992+this.reconnectReason = "max-duration";
993+this.activeConnectionReason = "max-duration";
994+this.config.onEvent?.({
995+direction: "server",
996+type: "session.rotation",
997+detail: "reason=max-duration",
998+});
999+this.ws?.close(1000, "max-duration rotation");
1000+return;
1001+}
1002+emitServerEvent();
9591003switch (event.type) {
9601004case "session.created":
9611005return;
@@ -965,6 +1009,14 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
9651009for (const chunk of this.pendingAudio.splice(0)) {
9661010this.sendAudio(chunk);
9671011}
1012+if (this.activeConnectionReason) {
1013+this.config.onEvent?.({
1014+direction: "server",
1015+type: "session.rotation.ready",
1016+detail: `reason=${this.activeConnectionReason}`,
1017+});
1018+this.activeConnectionReason = undefined;
1019+}
9681020if (!this.sessionReadyFired) {
9691021this.sessionReadyFired = true;
9701022this.config.onReady?.();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。