




















@@ -332,6 +332,59 @@ describe("buildOpenAIRealtimeVoiceProvider", () => {
332332expect(options?.headers).not.toHaveProperty("OpenAI-Beta");
333333});
334334335+it("prefers Codex OAuth over OPENAI_API_KEY for default GPT realtime bridges", async () => {
336+vi.stubEnv("OPENAI_API_KEY", "sk-env"); // pragma: allowlist secret
337+isProviderAuthProfileConfiguredMock.mockReturnValue(true);
338+resolveProviderAuthProfileApiKeyMock.mockResolvedValueOnce("oauth-token");
339+fetchWithSsrFGuardMock.mockResolvedValueOnce({
340+response: createJsonResponse({
341+client_secret: { value: "ephemeral-realtime-secret" },
342+}),
343+release: vi.fn(async () => undefined),
344+});
345+const provider = buildOpenAIRealtimeVoiceProvider();
346+const bridge = provider.createBridge({
347+cfg: {} as never,
348+providerConfig: { model: "gpt-realtime-2" },
349+onAudio: vi.fn(),
350+onClearAudio: vi.fn(),
351+});
352+353+void bridge.connect();
354+await vi.waitFor(() => expect(FakeWebSocket.instances.length).toBe(1));
355+bridge.close();
356+357+expectRecordFields(requireFetchHeaders(), "fetch headers", {
358+Authorization: "Bearer oauth-token", // pragma: allowlist secret
359+});
360+const socket = FakeWebSocket.instances[0];
361+const options = socket?.args[1] as { headers?: Record<string, string> } | undefined;
362+expect(options?.headers?.Authorization).toBe("Bearer ephemeral-realtime-secret");
363+});
364+365+it("keeps explicit OpenAI realtime API keys as the advanced override", () => {
366+vi.stubEnv("OPENAI_API_KEY", "sk-env"); // pragma: allowlist secret
367+resolveProviderAuthProfileApiKeyMock.mockResolvedValueOnce("oauth-token");
368+const provider = buildOpenAIRealtimeVoiceProvider();
369+const bridge = provider.createBridge({
370+cfg: {} as never,
371+providerConfig: {
372+apiKey: "sk-configured", // pragma: allowlist secret
373+model: "gpt-realtime-2",
374+},
375+onAudio: vi.fn(),
376+onClearAudio: vi.fn(),
377+});
378+379+void bridge.connect();
380+bridge.close();
381+382+expect(resolveProviderAuthProfileApiKeyMock).not.toHaveBeenCalled();
383+const socket = FakeWebSocket.instances[0];
384+const options = socket?.args[1] as { headers?: Record<string, string> } | undefined;
385+expect(options?.headers?.Authorization).toBe("Bearer sk-configured");
386+});
387+335388it("does not fall back to Codex OAuth for custom realtime endpoints", async () => {
336389resolveProviderAuthProfileApiKeyMock.mockResolvedValueOnce("oauth-token");
337390const provider = buildOpenAIRealtimeVoiceProvider();
@@ -585,6 +638,37 @@ describe("buildOpenAIRealtimeVoiceProvider", () => {
585638});
586639});
587640641+it("prefers Codex OAuth over OPENAI_API_KEY for default GPT browser sessions", async () => {
642+vi.stubEnv("OPENAI_API_KEY", "sk-env"); // pragma: allowlist secret
643+resolveProviderAuthProfileApiKeyMock.mockResolvedValueOnce("oauth-realtime-token");
644+fetchWithSsrFGuardMock.mockResolvedValueOnce({
645+response: createJsonResponse({
646+client_secret: { value: "client-secret-123" },
647+}),
648+release: vi.fn(async () => undefined),
649+});
650+const provider = buildOpenAIRealtimeVoiceProvider();
651+if (!provider.createBrowserSession) {
652+throw new Error("expected OpenAI realtime provider to support browser sessions");
653+}
654+const cfg = { agents: { defaults: {} } } as never;
655+656+await provider.createBrowserSession({
657+ cfg,
658+providerConfig: {},
659+model: "gpt-realtime-2",
660+instructions: "Be concise.",
661+});
662+663+expect(resolveProviderAuthProfileApiKeyMock).toHaveBeenCalledWith({
664+provider: "openai-codex",
665+ cfg,
666+});
667+expectRecordFields(requireFetchHeaders(), "fetch headers", {
668+Authorization: "Bearer oauth-realtime-token", // pragma: allowlist secret
669+});
670+});
671+588672it("fails closed when keychain refs cannot be resolved", async () => {
589673vi.stubEnv("OPENAI_API_KEY", "keychain:openclaw:OPENAI_REALTIME_MISSING_TEST");
590674execFileSyncMock.mockImplementationOnce(() => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。