mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(openai): correct Realtime auth and transcription secrets (#102518)
* fix(openai): correct Realtime auth and transcription secrets
* chore: leave release notes to release automation
(cherry picked from commit 9566aded5c)
This commit is contained in:
committed by
Dallin Romney
parent
be2f69eff6
commit
fb988756c0
@@ -153,12 +153,13 @@ export async function createOpenAIRealtimeTranscriptionClientSecret(params: {
|
||||
authToken: string;
|
||||
auditContext: string;
|
||||
session: Record<string, unknown>;
|
||||
authRejectedMessage?: string;
|
||||
}): Promise<OpenAIRealtimeClientSecretResult> {
|
||||
const url = "https://api.openai.com/v1/realtime/transcription_sessions";
|
||||
return createOpenAIRealtimeSecret({
|
||||
...params,
|
||||
url,
|
||||
body: params.session,
|
||||
body: { session: params.session },
|
||||
errorMessage: "OpenAI Realtime transcription client secret failed",
|
||||
missingValueMessage:
|
||||
"OpenAI Realtime transcription client secret response did not include a value",
|
||||
|
||||
@@ -203,7 +203,7 @@ describe("buildOpenAIRealtimeTranscriptionProvider", () => {
|
||||
expect(provider.aliases).toContain("openai-realtime");
|
||||
});
|
||||
|
||||
it("treats a Codex OAuth profile as configured when no API key is present", () => {
|
||||
it("treats an OpenAI API-key profile as configured", () => {
|
||||
const provider = buildOpenAIRealtimeTranscriptionProvider();
|
||||
const cfg = { auth: { order: { openai: ["openai:default"] } } };
|
||||
providerAuthMocks.isProviderAuthProfileConfigured.mockReturnValue(true);
|
||||
@@ -212,13 +212,14 @@ describe("buildOpenAIRealtimeTranscriptionProvider", () => {
|
||||
expect(providerAuthMocks.isProviderAuthProfileConfigured).toHaveBeenCalledWith({
|
||||
provider: "openai",
|
||||
cfg,
|
||||
profileTypes: ["api_key"],
|
||||
});
|
||||
});
|
||||
|
||||
it("mints a Codex OAuth client secret for realtime transcription sockets", async () => {
|
||||
it("mints an API-key client secret for realtime transcription sockets", async () => {
|
||||
const provider = buildOpenAIRealtimeTranscriptionProvider();
|
||||
const release = vi.fn();
|
||||
providerAuthMocks.resolveProviderAuthProfileApiKey.mockResolvedValue("oauth-token");
|
||||
providerAuthMocks.resolveProviderAuthProfileApiKey.mockResolvedValue("sk-profile"); // pragma: allowlist secret
|
||||
ssrfMocks.fetchWithSsrFGuard.mockResolvedValue({
|
||||
response: new Response(JSON.stringify({ value: "ek-test" }), { status: 200 }),
|
||||
release,
|
||||
@@ -236,6 +237,7 @@ describe("buildOpenAIRealtimeTranscriptionProvider", () => {
|
||||
expect(providerAuthMocks.resolveProviderAuthProfileApiKey).toHaveBeenCalledWith({
|
||||
provider: "openai",
|
||||
cfg,
|
||||
profileTypes: ["api_key"],
|
||||
});
|
||||
const request = mockCallArg(ssrfMocks.fetchWithSsrFGuard);
|
||||
expect(request.auditContext).toBe("openai-realtime-transcription-session");
|
||||
@@ -246,20 +248,22 @@ describe("buildOpenAIRealtimeTranscriptionProvider", () => {
|
||||
body?: unknown;
|
||||
};
|
||||
expect(init.method).toBe("POST");
|
||||
expect(init.headers?.Authorization).toBe("Bearer oauth-token");
|
||||
expect(init.headers?.Authorization).toBe("Bearer sk-profile");
|
||||
expect(init.headers?.["Content-Type"]).toBe("application/json");
|
||||
expect(typeof init.body).toBe("string");
|
||||
expect(JSON.parse(init.body as string)).toEqual({
|
||||
type: "transcription",
|
||||
audio: {
|
||||
input: {
|
||||
format: { type: "audio/pcmu" },
|
||||
transcription: { model: "gpt-4o-transcribe" },
|
||||
turn_detection: {
|
||||
type: "server_vad",
|
||||
threshold: 0.5,
|
||||
prefix_padding_ms: 300,
|
||||
silence_duration_ms: 800,
|
||||
session: {
|
||||
type: "transcription",
|
||||
audio: {
|
||||
input: {
|
||||
format: { type: "audio/pcmu" },
|
||||
transcription: { model: "gpt-4o-transcribe" },
|
||||
turn_detection: {
|
||||
type: "server_vad",
|
||||
threshold: 0.5,
|
||||
prefix_padding_ms: 300,
|
||||
silence_duration_ms: 800,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -292,6 +296,48 @@ describe("buildOpenAIRealtimeTranscriptionProvider", () => {
|
||||
session.close();
|
||||
});
|
||||
|
||||
it("does not use Codex OAuth for realtime transcription", async () => {
|
||||
const provider = buildOpenAIRealtimeTranscriptionProvider();
|
||||
const cfg = { auth: { order: { openai: ["openai:default"] } } };
|
||||
const session = provider.createSession({ cfg: cfg as never, providerConfig: {} });
|
||||
|
||||
await expect(session.connect()).rejects.toThrow(
|
||||
"OpenAI Realtime transcription requires an OpenAI Platform API key",
|
||||
);
|
||||
expect(providerAuthMocks.resolveProviderAuthProfileApiKey).toHaveBeenCalledWith({
|
||||
provider: "openai",
|
||||
cfg,
|
||||
profileTypes: ["api_key"],
|
||||
});
|
||||
expect(ssrfMocks.fetchWithSsrFGuard).not.toHaveBeenCalled();
|
||||
expect(FakeWebSocket.instances).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("prefers an API-key profile over OPENAI_API_KEY", async () => {
|
||||
vi.stubEnv("OPENAI_API_KEY", "sk-env"); // pragma: allowlist secret
|
||||
providerAuthMocks.resolveProviderAuthProfileApiKey.mockResolvedValue("sk-profile"); // pragma: allowlist secret
|
||||
ssrfMocks.fetchWithSsrFGuard.mockResolvedValue({
|
||||
response: new Response(JSON.stringify({ value: "ek-test" }), { status: 200 }),
|
||||
release: vi.fn(),
|
||||
});
|
||||
const provider = buildOpenAIRealtimeTranscriptionProvider();
|
||||
const session = provider.createSession({ providerConfig: {} });
|
||||
|
||||
const connecting = session.connect();
|
||||
const socket = await waitForFakeSocket();
|
||||
|
||||
expect(socket.headers?.Authorization).toBe("Bearer ek-test");
|
||||
const request = mockCallArg(ssrfMocks.fetchWithSsrFGuard);
|
||||
const init = request.init as { headers?: Record<string, string> };
|
||||
expect(init.headers?.Authorization).toBe("Bearer sk-profile");
|
||||
|
||||
socket.readyState = FakeWebSocket.OPEN;
|
||||
socket.emit("open");
|
||||
socket.emit("message", Buffer.from(JSON.stringify({ type: "transcription_session.updated" })));
|
||||
await connecting;
|
||||
session.close();
|
||||
});
|
||||
|
||||
it("waits for the OpenAI session update before draining audio", async () => {
|
||||
const provider = buildOpenAIRealtimeTranscriptionProvider();
|
||||
const session = provider.createSession({
|
||||
|
||||
@@ -73,6 +73,10 @@ const OPENAI_REALTIME_TRANSCRIPTION_CONNECT_TIMEOUT_MS = 10_000;
|
||||
const OPENAI_REALTIME_TRANSCRIPTION_MAX_RECONNECT_ATTEMPTS = 5;
|
||||
const OPENAI_REALTIME_TRANSCRIPTION_RECONNECT_DELAY_MS = 1000;
|
||||
const OPENAI_REALTIME_TRANSCRIPTION_DEFAULT_MODEL = "gpt-4o-transcribe";
|
||||
const OPENAI_REALTIME_TRANSCRIPTION_API_KEY_REQUIRED =
|
||||
"OpenAI Realtime transcription requires an OpenAI Platform API key";
|
||||
const OPENAI_REALTIME_TRANSCRIPTION_API_KEY_REJECTED =
|
||||
"OpenAI Realtime transcription rejected the selected API key. Update or remove the active OpenAI API-key source";
|
||||
|
||||
function normalizeProviderConfig(
|
||||
config: RealtimeTranscriptionProviderConfig,
|
||||
@@ -139,23 +143,28 @@ function buildOpenAIRealtimeTranscriptionSessionPayload(
|
||||
async function resolveOpenAIRealtimeTranscriptionAuthorization(
|
||||
config: OpenAIRealtimeTranscriptionSessionConfig,
|
||||
): Promise<string> {
|
||||
const apiKey = config.apiKey || process.env.OPENAI_API_KEY;
|
||||
if (apiKey) {
|
||||
return apiKey;
|
||||
if (config.apiKey) {
|
||||
return config.apiKey;
|
||||
}
|
||||
const authToken = await resolveProviderAuthProfileApiKey({
|
||||
provider: "openai",
|
||||
cfg: config.cfg,
|
||||
profileTypes: ["api_key"],
|
||||
});
|
||||
if (!authToken) {
|
||||
throw new Error("OpenAI API key or Codex OAuth missing");
|
||||
if (authToken) {
|
||||
const clientSecret = await createOpenAIRealtimeTranscriptionClientSecret({
|
||||
authToken,
|
||||
auditContext: "openai-realtime-transcription-session",
|
||||
session: buildOpenAIRealtimeTranscriptionSessionPayload(config),
|
||||
authRejectedMessage: OPENAI_REALTIME_TRANSCRIPTION_API_KEY_REJECTED,
|
||||
});
|
||||
return clientSecret.value;
|
||||
}
|
||||
const clientSecret = await createOpenAIRealtimeTranscriptionClientSecret({
|
||||
authToken,
|
||||
auditContext: "openai-realtime-transcription-session",
|
||||
session: buildOpenAIRealtimeTranscriptionSessionPayload(config),
|
||||
});
|
||||
return clientSecret.value;
|
||||
const envApiKey = process.env.OPENAI_API_KEY?.trim();
|
||||
if (envApiKey) {
|
||||
return envApiKey;
|
||||
}
|
||||
throw new Error(OPENAI_REALTIME_TRANSCRIPTION_API_KEY_REQUIRED);
|
||||
}
|
||||
|
||||
function createOpenAIRealtimeTranscriptionSession(
|
||||
@@ -260,7 +269,7 @@ export function buildOpenAIRealtimeTranscriptionProvider(): RealtimeTranscriptio
|
||||
Boolean(
|
||||
normalizeProviderConfig(providerConfig).apiKey ||
|
||||
process.env.OPENAI_API_KEY ||
|
||||
isProviderAuthProfileConfigured({ provider: "openai", cfg }),
|
||||
isProviderAuthProfileConfigured({ provider: "openai", cfg, profileTypes: ["api_key"] }),
|
||||
),
|
||||
createSession: (req) => {
|
||||
const config = normalizeProviderConfig(req.providerConfig);
|
||||
|
||||
Reference in New Issue
Block a user