mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 20:00:42 +00:00
refactor: share provider HTTP errors with google
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { assertOkOrThrowProviderError } from "openclaw/plugin-sdk/provider-http";
|
||||
import {
|
||||
assertOkOrThrowProviderError,
|
||||
normalizeApplyTextNormalization,
|
||||
normalizeLanguageCode,
|
||||
normalizeSeed,
|
||||
|
||||
66
extensions/google/google.live.test.ts
Normal file
66
extensions/google/google.live.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isLiveTestEnabled } from "../../src/agents/live-test-helpers.js";
|
||||
import {
|
||||
registerProviderPlugin,
|
||||
requireRegisteredProvider,
|
||||
} from "../../test/helpers/plugins/provider-registration.js";
|
||||
import { normalizeTranscriptForMatch } from "../../test/helpers/stt-live-audio.js";
|
||||
import plugin from "./index.js";
|
||||
|
||||
const GOOGLE_API_KEY =
|
||||
process.env.GEMINI_API_KEY?.trim() || process.env.GOOGLE_API_KEY?.trim() || "";
|
||||
const LIVE = isLiveTestEnabled() && GOOGLE_API_KEY.length > 0;
|
||||
const describeLive = LIVE ? describe : describe.skip;
|
||||
|
||||
const registerGooglePlugin = () =>
|
||||
registerProviderPlugin({
|
||||
plugin,
|
||||
id: "google",
|
||||
name: "Google Provider",
|
||||
});
|
||||
|
||||
describeLive("google plugin live", () => {
|
||||
it("synthesizes speech through the registered provider", async () => {
|
||||
const { speechProviders } = await registerGooglePlugin();
|
||||
const provider = requireRegisteredProvider(speechProviders, "google");
|
||||
|
||||
const audioFile = await provider.synthesize({
|
||||
text: "OpenClaw Google text to speech integration test OK.",
|
||||
cfg: { plugins: { enabled: true } } as never,
|
||||
providerConfig: { apiKey: GOOGLE_API_KEY },
|
||||
target: "audio-file",
|
||||
timeoutMs: 90_000,
|
||||
});
|
||||
|
||||
expect(audioFile.outputFormat).toBe("wav");
|
||||
expect(audioFile.fileExtension).toBe(".wav");
|
||||
expect(audioFile.audioBuffer.byteLength).toBeGreaterThan(512);
|
||||
}, 120_000);
|
||||
|
||||
it("transcribes synthesized speech through the media provider", async () => {
|
||||
const { mediaProviders, speechProviders } = await registerGooglePlugin();
|
||||
const speechProvider = requireRegisteredProvider(speechProviders, "google");
|
||||
const mediaProvider = requireRegisteredProvider(mediaProviders, "google");
|
||||
|
||||
const phrase = "Testing Google audio transcription with OpenClaw.";
|
||||
const audioFile = await speechProvider.synthesize({
|
||||
text: phrase,
|
||||
cfg: { plugins: { enabled: true } } as never,
|
||||
providerConfig: { apiKey: GOOGLE_API_KEY },
|
||||
target: "audio-file",
|
||||
timeoutMs: 90_000,
|
||||
});
|
||||
|
||||
const transcript = await mediaProvider.transcribeAudio?.({
|
||||
buffer: audioFile.audioBuffer,
|
||||
fileName: "google-live.wav",
|
||||
mime: "audio/wav",
|
||||
apiKey: GOOGLE_API_KEY,
|
||||
timeoutMs: 90_000,
|
||||
});
|
||||
|
||||
const normalized = normalizeTranscriptForMatch(transcript?.text ?? "");
|
||||
expect(normalized).toContain("google");
|
||||
expect(normalized).toContain("openclaw");
|
||||
}, 180_000);
|
||||
});
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
type VideoDescriptionResult,
|
||||
} from "openclaw/plugin-sdk/media-understanding";
|
||||
import {
|
||||
assertOkOrThrowHttpError,
|
||||
assertOkOrThrowProviderError,
|
||||
postJsonRequest,
|
||||
type ProviderRequestTransportOverrides,
|
||||
} from "openclaw/plugin-sdk/provider-http";
|
||||
@@ -96,7 +96,7 @@ async function generateGeminiInlineDataText(params: {
|
||||
});
|
||||
|
||||
try {
|
||||
await assertOkOrThrowHttpError(res, params.httpErrorLabel);
|
||||
await assertOkOrThrowProviderError(res, params.httpErrorLabel);
|
||||
|
||||
const payload = (await res.json()) as {
|
||||
candidates?: Array<{
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
createRequestCaptureJsonFetch,
|
||||
installPinnedHostnameTestHooks,
|
||||
} from "../../src/media-understanding/audio.test-helpers.js";
|
||||
import { describeGeminiVideo } from "./media-understanding-provider.js";
|
||||
import { describeGeminiVideo, transcribeGeminiAudio } from "./media-understanding-provider.js";
|
||||
import { resolveGoogleGenerativeAiHttpRequestConfig } from "./runtime-api.js";
|
||||
|
||||
installPinnedHostnameTestHooks();
|
||||
@@ -129,4 +129,30 @@ describe("describeGeminiVideo", () => {
|
||||
"Google Generative AI baseUrl must use https://generativelanguage.googleapis.com",
|
||||
);
|
||||
});
|
||||
|
||||
it("formats Google audio transcription HTTP errors with provider details", async () => {
|
||||
await expect(
|
||||
transcribeGeminiAudio({
|
||||
buffer: Buffer.from("audio-bytes"),
|
||||
fileName: "clip.wav",
|
||||
apiKey: "test-key",
|
||||
timeoutMs: 1500,
|
||||
fetchFn: async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: "Unsupported audio",
|
||||
status: "INVALID_ARGUMENT",
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
headers: { "x-request-id": "google_audio_req" },
|
||||
},
|
||||
),
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"Audio transcription failed (400): Unsupported audio [code=INVALID_ARGUMENT] [request_id=google_audio_req]",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -245,4 +245,37 @@ describe("Google speech provider", () => {
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("formats Google TTS HTTP errors with provider details", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: "Quota exceeded",
|
||||
status: "RESOURCE_EXHAUSTED",
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 429,
|
||||
headers: { "x-request-id": "google_req_123" },
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
const provider = buildGoogleSpeechProvider();
|
||||
|
||||
await expect(
|
||||
provider.synthesize({
|
||||
text: "Read this plainly.",
|
||||
cfg: {},
|
||||
providerConfig: { apiKey: "google-test-key" },
|
||||
target: "audio-file",
|
||||
timeoutMs: 10_000,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"Google TTS failed (429): Quota exceeded [code=RESOURCE_EXHAUSTED] [request_id=google_req_123]",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { assertOkOrThrowHttpError, postJsonRequest } from "openclaw/plugin-sdk/provider-http";
|
||||
import { assertOkOrThrowProviderError, postJsonRequest } from "openclaw/plugin-sdk/provider-http";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/provider-onboard";
|
||||
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
||||
import type {
|
||||
@@ -281,7 +281,7 @@ async function synthesizeGoogleTtsPcm(params: {
|
||||
});
|
||||
|
||||
try {
|
||||
await assertOkOrThrowHttpError(res, "Google TTS failed");
|
||||
await assertOkOrThrowProviderError(res, "Google TTS failed");
|
||||
return extractGoogleSpeechPcm((await res.json()) as GoogleGenerateSpeechResponse);
|
||||
} finally {
|
||||
await release();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { assertOkOrThrowProviderError } from "openclaw/plugin-sdk/speech";
|
||||
import { assertOkOrThrowProviderError } from "openclaw/plugin-sdk/provider-http";
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { normalizeGradiumBaseUrl } from "./shared.js";
|
||||
|
||||
|
||||
44
extensions/openai/openai-tts.live.test.ts
Normal file
44
extensions/openai/openai-tts.live.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isLiveTestEnabled } from "../../src/agents/live-test-helpers.js";
|
||||
import { buildOpenAISpeechProvider } from "./speech-provider.js";
|
||||
|
||||
const OPENAI_API_KEY = process.env.OPENAI_API_KEY?.trim() ?? "";
|
||||
const LIVE = isLiveTestEnabled() && OPENAI_API_KEY.length > 0;
|
||||
const describeLive = LIVE ? describe : describe.skip;
|
||||
|
||||
describeLive("openai tts live", () => {
|
||||
it("synthesizes audio through the speech provider", async () => {
|
||||
const speechProvider = buildOpenAISpeechProvider();
|
||||
|
||||
const voices = await speechProvider.listVoices?.({});
|
||||
expect(voices).toEqual(expect.arrayContaining([expect.objectContaining({ id: "alloy" })]));
|
||||
|
||||
const providerConfig = {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
model: "gpt-4o-mini-tts",
|
||||
voice: "alloy",
|
||||
};
|
||||
|
||||
const audioFile = await speechProvider.synthesize({
|
||||
text: "OpenClaw OpenAI text to speech integration test OK.",
|
||||
cfg: { plugins: { enabled: true } } as never,
|
||||
providerConfig,
|
||||
target: "audio-file",
|
||||
timeoutMs: 45_000,
|
||||
});
|
||||
expect(audioFile.outputFormat).toBe("mp3");
|
||||
expect(audioFile.fileExtension).toBe(".mp3");
|
||||
expect(audioFile.audioBuffer.byteLength).toBeGreaterThan(512);
|
||||
|
||||
const telephony = await speechProvider.synthesizeTelephony?.({
|
||||
text: "OpenClaw OpenAI telephony integration test OK.",
|
||||
cfg: { plugins: { enabled: true } } as never,
|
||||
providerConfig,
|
||||
timeoutMs: 45_000,
|
||||
});
|
||||
expect(telephony?.outputFormat).toBe("pcm");
|
||||
expect(telephony?.sampleRate).toBe(24_000);
|
||||
expect(telephony?.audioBuffer.byteLength).toBeGreaterThan(512);
|
||||
}, 60_000);
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import { assertOkOrThrowProviderError } from "openclaw/plugin-sdk/provider-http";
|
||||
import {
|
||||
captureHttpExchange,
|
||||
isDebugProxyGlobalFetchPatchInstalled,
|
||||
} from "openclaw/plugin-sdk/proxy-capture";
|
||||
import { assertOkOrThrowProviderError } from "openclaw/plugin-sdk/speech";
|
||||
import {
|
||||
fetchWithSsrFGuard,
|
||||
ssrfPolicyFromHttpBaseUrlAllowedHostname,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { postJsonRequest } from "openclaw/plugin-sdk/provider-http";
|
||||
import { assertOkOrThrowProviderError, trimToUndefined } from "openclaw/plugin-sdk/speech";
|
||||
import { assertOkOrThrowProviderError, postJsonRequest } from "openclaw/plugin-sdk/provider-http";
|
||||
import { trimToUndefined } from "openclaw/plugin-sdk/speech";
|
||||
import { XAI_BASE_URL } from "./api.js";
|
||||
export { XAI_BASE_URL };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user