diff --git a/extensions/google/google.live.test.ts b/extensions/google/google.live.test.ts index df3fec0b436f..5e96036b38c9 100644 --- a/extensions/google/google.live.test.ts +++ b/extensions/google/google.live.test.ts @@ -6,6 +6,7 @@ import { import { normalizeTranscriptForMatch } from "openclaw/plugin-sdk/provider-test-contracts"; import { isLiveTestEnabled } from "openclaw/plugin-sdk/test-env"; import { describe, expect, it } from "vitest"; +import { hasTrustedFfmpegForLiveVoiceNote } from "../../test/helpers/live-voice-note.js"; import plugin from "./index.js"; import { createGeminiWebSearchProvider } from "./src/gemini-web-search-provider.js"; @@ -75,6 +76,10 @@ describeLive("google plugin live", () => { }, 120_000); it("transcodes speech to Opus for voice-note targets", async () => { + if (!hasTrustedFfmpegForLiveVoiceNote("google")) { + return; + } + const { speechProviders } = await registerGooglePlugin(); const provider = requireRegisteredProvider(speechProviders, "google"); diff --git a/extensions/minimax/minimax.live.test.ts b/extensions/minimax/minimax.live.test.ts index 9f71d19e516d..b8b2bd92c222 100644 --- a/extensions/minimax/minimax.live.test.ts +++ b/extensions/minimax/minimax.live.test.ts @@ -5,6 +5,7 @@ import { } from "openclaw/plugin-sdk/plugin-test-runtime"; import { isLiveTestEnabled } from "openclaw/plugin-sdk/test-env"; import { describe, expect, it } from "vitest"; +import { hasTrustedFfmpegForLiveVoiceNote } from "../../test/helpers/live-voice-note.js"; import plugin from "./index.js"; import { buildMinimaxSpeechProvider } from "./speech-provider.js"; import { createMiniMaxWebSearchProvider } from "./src/minimax-web-search-provider.js"; @@ -70,6 +71,10 @@ describeTtsLive("minimax tts live", () => { }, 120_000); it("synthesizes MiniMax TTS as an Opus voice note", async () => { + if (!hasTrustedFfmpegForLiveVoiceNote("minimax")) { + return; + } + const provider = buildMinimaxSpeechProvider(); const voiceNote = await provider.synthesize({ diff --git a/extensions/moonshot/moonshot.live.test.ts b/extensions/moonshot/moonshot.live.test.ts index 26479df9ed75..410815a02d18 100644 --- a/extensions/moonshot/moonshot.live.test.ts +++ b/extensions/moonshot/moonshot.live.test.ts @@ -19,6 +19,17 @@ function isTransientKimiSearchError(error: unknown): boolean { return message.includes("timeout") || message.includes("aborted"); } +function isKimiAuthDrift(error: unknown): boolean { + if (!(error instanceof Error)) { + return false; + } + const message = error.message.toLowerCase(); + return ( + message.includes("kimi api error (401)") && + (message.includes("incorrect api key") || message.includes("incorrect_api_key")) + ); +} + describeLive("moonshot plugin live", () => { it("runs Kimi web search through the provider tool", async () => { const provider = createKimiWebSearchProvider(); @@ -40,6 +51,10 @@ describeLive("moonshot plugin live", () => { break; } catch (error) { lastError = error; + if (isKimiAuthDrift(error)) { + console.warn("[moonshot:live] skip Kimi web search: auth drift"); + return; + } if (!isTransientKimiSearchError(error) || attempt === 1) { throw error; } diff --git a/src/agents/embedded-agent-runner-extraparams.live.test.ts b/src/agents/embedded-agent-runner-extraparams.live.test.ts index 2b8bd137828f..e07b410cac0a 100644 --- a/src/agents/embedded-agent-runner-extraparams.live.test.ts +++ b/src/agents/embedded-agent-runner-extraparams.live.test.ts @@ -5,7 +5,7 @@ import { describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { applyExtraParamsToAgent } from "./embedded-agent-runner.js"; import { isLiveTestEnabled } from "./live-test-helpers.js"; -import { isLiveBillingDrift } from "./live-test-provider-drift.js"; +import { isLiveAuthDrift, isLiveBillingDrift } from "./live-test-provider-drift.js"; const OPENAI_KEY = process.env.OPENAI_API_KEY ?? ""; const ANTHROPIC_KEY = process.env.ANTHROPIC_API_KEY ?? ""; @@ -143,9 +143,15 @@ describeAnthropicLive("embedded agent extra params (anthropic live)", () => { usage?: { service_tier?: string }; }; const errorMessage = json.error?.message ?? `HTTP ${res.status}`; - if (!res.ok && isLiveBillingDrift(errorMessage)) { - console.warn(`[anthropic:live] skip service_tier ${serviceTier}: billing drift`); - return null; + if (!res.ok) { + if (isLiveBillingDrift(errorMessage)) { + console.warn(`[anthropic:live] skip service_tier ${serviceTier}: billing drift`); + return null; + } + if (isLiveAuthDrift(errorMessage)) { + console.warn(`[anthropic:live] skip service_tier ${serviceTier}: auth drift`); + return null; + } } expect(res.ok, errorMessage).toBe(true); return json; diff --git a/src/agents/live-test-provider-drift.test.ts b/src/agents/live-test-provider-drift.test.ts index a5bbbdb6f6fc..eacea7c9b734 100644 --- a/src/agents/live-test-provider-drift.test.ts +++ b/src/agents/live-test-provider-drift.test.ts @@ -18,6 +18,7 @@ describe("live test provider drift", () => { expect( isLiveAuthDrift('401 {"error":{"message":"The API key you provided is invalid."}}'), ).toBe(true); + expect(isLiveAuthDrift("invalid x-api-key")).toBe(true); }); it("classifies API-key rate-limit drift", () => { diff --git a/src/agents/live-test-provider-drift.ts b/src/agents/live-test-provider-drift.ts index f4a4ea82306e..f9011e14ecac 100644 --- a/src/agents/live-test-provider-drift.ts +++ b/src/agents/live-test-provider-drift.ts @@ -47,7 +47,13 @@ export function liveProviderErrorText(error: unknown): string { /** Returns whether an error is expected live auth/account drift. */ export function isLiveAuthDrift(error: unknown): boolean { - return isAuthErrorMessage(liveProviderErrorText(error)); + const raw = liveProviderErrorText(error); + const message = normalizeLowercaseStringOrEmpty(raw); + return ( + isAuthErrorMessage(raw) || + message.includes("invalid x-api-key") || + message.includes("incorrect x-api-key") + ); } /** Returns whether an error is expected live billing/quota drift. */ diff --git a/src/agents/tools/image-tool.providers.live.test.ts b/src/agents/tools/image-tool.providers.live.test.ts index d4437d290c45..33272c9bda13 100644 --- a/src/agents/tools/image-tool.providers.live.test.ts +++ b/src/agents/tools/image-tool.providers.live.test.ts @@ -19,6 +19,7 @@ import { isServerErrorMessage, } from "../../plugin-sdk/test-env.js"; import { isLiveTestEnabled } from "../live-test-helpers.js"; +import { isLiveAuthDrift } from "../live-test-provider-drift.js"; import { createImageTool, testing } from "./image-tool.js"; const OPENAI_API_KEY = process.env.OPENAI_API_KEY?.trim() ?? ""; @@ -115,6 +116,7 @@ function isSkippableLiveError(error: unknown): boolean { const message = formatLiveError(error); return ( isBillingErrorMessage(message) || + isLiveAuthDrift(message) || isOverloadedErrorMessage(message) || isServerErrorMessage(message) || /timed out|operation was aborted/i.test(message) diff --git a/test/helpers/live-voice-note.ts b/test/helpers/live-voice-note.ts new file mode 100644 index 000000000000..3bb08186e826 --- /dev/null +++ b/test/helpers/live-voice-note.ts @@ -0,0 +1,15 @@ +import { resolveFfmpegBin } from "openclaw/plugin-sdk/media-runtime"; + +export function hasTrustedFfmpegForLiveVoiceNote(label: string): boolean { + try { + resolveFfmpegBin(); + return true; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (message.includes("ffmpeg not found in trusted system directories")) { + console.warn(`[${label}:live] skip voice-note transcode: ffmpeg unavailable`); + return false; + } + throw error; + } +}