mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
fix(openai): preserve GPT-Live startup after recoverable provider errors (#155534)
Related: #155190 (provider-startup portion only; telephone-call teardown remains separate).
## What Problem This Solves
Fixes GPT-Live sessions failing to start when a recoverable provider error arrives before `session.started`.
## User Impact
Recoverable warnings no longer prevent a subsequent readiness event from starting the voice session. Fatal authentication errors still reject startup immediately. No configuration or migration changes are needed.
## Why This Change Was Made
The bridge now checks the existing fatal-auth classification before rejecting startup, logs a redacted warning for recoverable startup errors, and continues waiting within the existing readiness timeout. Established-session error behavior and redaction remain unchanged. The classifier is unchanged: status 401 (number or string), or codes `authentication_error`, `invalid_api_key`, `invalid_token`, and `token_expired` are fatal; other error events, including `missing_scope` without status 401, are nonfatal.
Provider-error dispatch stays in a small private sibling module because the bridge is already at its enforced line cap. The regression checks readiness and terminal behavior rather than exact warning wording.
## Evidence
- Real bridge `connect()` replay using a real `ws` client and a TCP loopback WebSocket server, production event parsing/lifecycle, and the existing media-runtime test adapter. The server sent `error` followed by `session.started` after receiving `session.start`.
- Baseline `4d50b52bce`: `missing_scope` rejected startup; connected=false, ready callbacks=0. Candidate: startup resolved; connected=true, ready callbacks=1, warning=1, error callbacks=0, close callbacks=0.
- `authentication_error` rejected startup on both baseline and candidate, with connected=false and ready callbacks=0 despite the subsequent readiness frame.
- `node scripts/run-vitest.mjs extensions/openai/realtime-quicksilver-bridge.test.ts --maxWorkers=1`: 37 passed; measured command wall time 38.71 seconds (Vitest 34.73 seconds). Earlier exact contributor-head CI changed-extension test-shard step took 133 seconds.
- Replay ran under Bun 1.4.2; this is synthetic provider-stream transport proof, not live OpenAI availability, full Gateway, telephone-call, or media-worker packaging proof. No real provider credentials were used.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
parent
dca548e48d
commit
837a17c1fb
3 changed files with 72 additions and 11 deletions
|
|
@ -478,6 +478,29 @@ describe("OpenAIQuicksilverVoiceBridge", () => {
|
|||
expect(harness.onReady).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("keeps recoverable provider errors non-terminal before session readiness", async () => {
|
||||
const harness = createHarness({ autoStart: false });
|
||||
const connecting = harness.bridge.connect();
|
||||
await vi.waitFor(() => expect(harness.socket.readyState).toBe(1));
|
||||
|
||||
harness.socket.serverEvent({
|
||||
type: "error",
|
||||
error: { code: "missing_scope", message: "temporary provider rejection" },
|
||||
});
|
||||
|
||||
expect(harness.bridge.isConnected()).toBe(false);
|
||||
expect(harness.onError).not.toHaveBeenCalled();
|
||||
expect(harness.onClose).not.toHaveBeenCalled();
|
||||
|
||||
harness.socket.serverEvent({
|
||||
type: "session.started",
|
||||
session: { id: "live-1", expires_at: Math.floor(Date.now() / 1000) + 60 },
|
||||
});
|
||||
await connecting;
|
||||
expect(harness.bridge.isConnected()).toBe(true);
|
||||
expect(harness.onReady).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[1000, "completed"],
|
||||
[1006, "error"],
|
||||
|
|
@ -595,7 +618,7 @@ describe("OpenAIQuicksilverVoiceBridge", () => {
|
|||
|
||||
harness.socket.serverEvent({
|
||||
type: "error",
|
||||
error: { message: "invalid live session" },
|
||||
error: { code: "authentication_error", message: "invalid live session" },
|
||||
});
|
||||
|
||||
await expect(connecting).rejects.toThrow("OpenAI GPT-Live transport failed");
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
closeOpenAILiveSocket,
|
||||
openAIQuicksilverToolResultText,
|
||||
} from "./realtime-quicksilver-protocol.js";
|
||||
import { handleOpenAIQuicksilverProviderError } from "./realtime-quicksilver-provider-error.js";
|
||||
import { projectOpenAIQuicksilverErrorMessage } from "./realtime-quicksilver-redaction.js";
|
||||
import {
|
||||
connectOpenAIQuicksilverSideband,
|
||||
|
|
@ -562,18 +563,17 @@ export class OpenAIQuicksilverVoiceBridge implements RealtimeVoiceBridge {
|
|||
}
|
||||
const message = projectOpenAIQuicksilverErrorMessage("provider");
|
||||
const error = new Error(message);
|
||||
if (!this.lifecycle.isReady()) {
|
||||
failStartup(error, "session start failed");
|
||||
return;
|
||||
}
|
||||
const reportEvent = () =>
|
||||
this.config.onEvent?.({ direction: "server", type: "error", detail: message });
|
||||
if (event.fatalAuth) {
|
||||
this.fail(connection, error, "authentication failed", reportEvent);
|
||||
} else {
|
||||
reportEvent();
|
||||
this.config.onError?.(error);
|
||||
}
|
||||
handleOpenAIQuicksilverProviderError({
|
||||
fatalAuth: event.fatalAuth,
|
||||
ready: this.lifecycle.isReady(),
|
||||
failStartup: () => failStartup(error, "session start failed"),
|
||||
failAuthentication: () => this.fail(connection, error, "authentication failed", reportEvent),
|
||||
reportEvent,
|
||||
reportError: () => this.config.onError?.(error),
|
||||
logger: this.config.logger,
|
||||
});
|
||||
}
|
||||
|
||||
private startDelegation(
|
||||
|
|
|
|||
38
extensions/openai/realtime-quicksilver-provider-error.ts
Normal file
38
extensions/openai/realtime-quicksilver-provider-error.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import type { PluginLogger } from "openclaw/plugin-sdk/plugin-entry";
|
||||
|
||||
type OpenAIQuicksilverProviderErrorHandling = {
|
||||
fatalAuth: boolean;
|
||||
ready: boolean;
|
||||
failStartup: () => void;
|
||||
failAuthentication: () => void;
|
||||
reportEvent: () => void;
|
||||
reportError: () => void;
|
||||
logger?: Pick<PluginLogger, "warn">;
|
||||
};
|
||||
|
||||
export function handleOpenAIQuicksilverProviderError({
|
||||
fatalAuth,
|
||||
ready,
|
||||
failStartup,
|
||||
failAuthentication,
|
||||
reportEvent,
|
||||
reportError,
|
||||
logger,
|
||||
}: OpenAIQuicksilverProviderErrorHandling): void {
|
||||
if (fatalAuth) {
|
||||
if (!ready) {
|
||||
failStartup();
|
||||
} else {
|
||||
failAuthentication();
|
||||
}
|
||||
return;
|
||||
}
|
||||
reportEvent();
|
||||
if (!ready) {
|
||||
(logger?.warn ?? console.warn)(
|
||||
"OpenAI GPT-Live provider error before session startup; continuing readiness",
|
||||
);
|
||||
return;
|
||||
}
|
||||
reportError();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue