mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-27 16:07:13 +00:00
219 lines
6.7 KiB
TypeScript
219 lines
6.7 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { buildOpenRouterSpeechProvider } from "./speech-provider.js";
|
|
|
|
const {
|
|
assertOkOrThrowHttpErrorMock,
|
|
postJsonRequestMock,
|
|
readProviderBinaryResponseMock,
|
|
resolveProviderHttpRequestConfigMock,
|
|
} = vi.hoisted(() => ({
|
|
assertOkOrThrowHttpErrorMock: vi.fn(async () => {}),
|
|
postJsonRequestMock: vi.fn(),
|
|
readProviderBinaryResponseMock: vi.fn(async (response: Response) => {
|
|
return new Uint8Array(await response.arrayBuffer());
|
|
}),
|
|
resolveProviderHttpRequestConfigMock: vi.fn((params: Record<string, unknown>) => ({
|
|
baseUrl: params.baseUrl ?? params.defaultBaseUrl ?? "https://openrouter.ai/api/v1",
|
|
allowPrivateNetwork: false,
|
|
headers: new Headers(params.defaultHeaders as HeadersInit | undefined),
|
|
dispatcherPolicy: undefined,
|
|
})),
|
|
}));
|
|
|
|
vi.mock("openclaw/plugin-sdk/provider-http", () => ({
|
|
assertOkOrThrowHttpError: assertOkOrThrowHttpErrorMock,
|
|
postJsonRequest: postJsonRequestMock,
|
|
readProviderBinaryResponse: readProviderBinaryResponseMock,
|
|
resolveProviderHttpRequestConfig: resolveProviderHttpRequestConfigMock,
|
|
}));
|
|
|
|
function requireOpenRouterConfigRequest(): Record<string, unknown> {
|
|
const [call] = resolveProviderHttpRequestConfigMock.mock.calls;
|
|
if (!call) {
|
|
throw new Error("expected OpenRouter speech config request");
|
|
}
|
|
const [request] = call;
|
|
if (!request || typeof request !== "object" || Array.isArray(request)) {
|
|
throw new Error("expected OpenRouter speech config request");
|
|
}
|
|
return request;
|
|
}
|
|
|
|
function requireOpenRouterPostRequest(): Record<string, unknown> {
|
|
const [call] = postJsonRequestMock.mock.calls;
|
|
if (!call) {
|
|
throw new Error("expected OpenRouter speech request");
|
|
}
|
|
const [request] = call;
|
|
if (!request || typeof request !== "object" || Array.isArray(request)) {
|
|
throw new Error("expected OpenRouter speech request");
|
|
}
|
|
return request as Record<string, unknown>;
|
|
}
|
|
|
|
function requireHeaders(value: unknown): Headers {
|
|
if (!(value instanceof Headers)) {
|
|
throw new Error("expected OpenRouter speech request headers");
|
|
}
|
|
return value;
|
|
}
|
|
|
|
describe("openrouter speech provider", () => {
|
|
afterEach(() => {
|
|
assertOkOrThrowHttpErrorMock.mockClear();
|
|
postJsonRequestMock.mockReset();
|
|
readProviderBinaryResponseMock.mockClear();
|
|
resolveProviderHttpRequestConfigMock.mockClear();
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
it("normalizes provider-owned speech config", () => {
|
|
const provider = buildOpenRouterSpeechProvider();
|
|
const resolved = provider.resolveConfig?.({
|
|
cfg: {} as never,
|
|
timeoutMs: 30_000,
|
|
rawConfig: {
|
|
providers: {
|
|
openrouter: {
|
|
apiKey: "sk-test",
|
|
baseUrl: "https://openrouter.ai/v1/",
|
|
modelId: "google/gemini-3.1-flash-tts-preview",
|
|
voiceId: "Kore",
|
|
speed: 1.1,
|
|
responseFormat: " MP3 ",
|
|
provider: {
|
|
options: {
|
|
openai: {
|
|
instructions: "Speak warmly.",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(resolved).toEqual({
|
|
apiKey: "sk-test",
|
|
baseUrl: "https://openrouter.ai/api/v1",
|
|
model: "google/gemini-3.1-flash-tts-preview",
|
|
voice: "Kore",
|
|
speed: 1.1,
|
|
responseFormat: "mp3",
|
|
provider: {
|
|
options: {
|
|
openai: {
|
|
instructions: "Speak warmly.",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("synthesizes OpenAI-compatible speech through OpenRouter", async () => {
|
|
const release = vi.fn(async () => {});
|
|
postJsonRequestMock.mockResolvedValue({
|
|
response: new Response(new Uint8Array([1, 2, 3]), { status: 200 }),
|
|
release,
|
|
});
|
|
|
|
const provider = buildOpenRouterSpeechProvider();
|
|
const result = await provider.synthesize({
|
|
text: "hello",
|
|
cfg: {
|
|
models: {
|
|
providers: {
|
|
openrouter: {
|
|
apiKey: "sk-openrouter",
|
|
baseUrl: "https://openrouter.ai/v1/",
|
|
},
|
|
},
|
|
},
|
|
} as never,
|
|
providerConfig: {
|
|
model: "openai/gpt-4o-mini-tts-2025-12-15",
|
|
voice: "nova",
|
|
speed: 1.2,
|
|
},
|
|
target: "voice-note",
|
|
timeoutMs: 12_345,
|
|
});
|
|
|
|
expect(resolveProviderHttpRequestConfigMock).toHaveBeenCalledOnce();
|
|
expect(requireOpenRouterConfigRequest()).toEqual({
|
|
baseUrl: "https://openrouter.ai/api/v1",
|
|
defaultBaseUrl: "https://openrouter.ai/api/v1",
|
|
allowPrivateNetwork: false,
|
|
defaultHeaders: {
|
|
Authorization: "Bearer sk-openrouter",
|
|
"Content-Type": "application/json",
|
|
"HTTP-Referer": "https://openclaw.ai",
|
|
"X-OpenRouter-Title": "OpenClaw",
|
|
},
|
|
provider: "openrouter",
|
|
capability: "audio",
|
|
transport: "http",
|
|
});
|
|
expect(postJsonRequestMock).toHaveBeenCalledOnce();
|
|
const request = requireOpenRouterPostRequest();
|
|
const headers = requireHeaders(request.headers);
|
|
expect(Object.fromEntries(headers.entries())).toEqual({
|
|
authorization: "Bearer sk-openrouter",
|
|
"content-type": "application/json",
|
|
"http-referer": "https://openclaw.ai",
|
|
"x-openrouter-title": "OpenClaw",
|
|
});
|
|
expect(request).toEqual({
|
|
url: "https://openrouter.ai/api/v1/audio/speech",
|
|
headers,
|
|
body: {
|
|
model: "openai/gpt-4o-mini-tts-2025-12-15",
|
|
input: "hello",
|
|
voice: "nova",
|
|
response_format: "mp3",
|
|
speed: 1.2,
|
|
},
|
|
timeoutMs: 12_345,
|
|
fetchFn: fetch,
|
|
allowPrivateNetwork: false,
|
|
dispatcherPolicy: undefined,
|
|
});
|
|
expect(result.audioBuffer).toEqual(Buffer.from([1, 2, 3]));
|
|
expect(readProviderBinaryResponseMock).toHaveBeenCalledWith(
|
|
expect.any(Response),
|
|
"OpenRouter TTS API error",
|
|
"audio",
|
|
);
|
|
expect(result.outputFormat).toBe("mp3");
|
|
expect(result.fileExtension).toBe(".mp3");
|
|
expect(result.voiceCompatible).toBe(true);
|
|
expect(release).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("defaults to a live-proven OpenRouter TTS model", () => {
|
|
const provider = buildOpenRouterSpeechProvider();
|
|
|
|
expect(
|
|
provider.resolveConfig?.({ cfg: {} as never, rawConfig: {}, timeoutMs: 30_000 }),
|
|
).toEqual({
|
|
model: "hexgrad/kokoro-82m",
|
|
voice: "af_alloy",
|
|
responseFormat: undefined,
|
|
provider: undefined,
|
|
});
|
|
});
|
|
|
|
it("uses OPENROUTER_API_KEY when provider config omits apiKey", () => {
|
|
vi.stubEnv("OPENROUTER_API_KEY", "sk-env");
|
|
const provider = buildOpenRouterSpeechProvider();
|
|
|
|
expect(
|
|
provider.isConfigured({
|
|
cfg: {} as never,
|
|
providerConfig: {},
|
|
timeoutMs: 30_000,
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
});
|