


























@@ -0,0 +1,155 @@
1+import { afterEach, describe, expect, it, vi } from "vitest";
2+import { buildOpenRouterSpeechProvider } from "./speech-provider.js";
3+4+const { assertOkOrThrowHttpErrorMock, postJsonRequestMock, resolveProviderHttpRequestConfigMock } =
5+vi.hoisted(() => ({
6+assertOkOrThrowHttpErrorMock: vi.fn(async () => {}),
7+postJsonRequestMock: vi.fn(),
8+resolveProviderHttpRequestConfigMock: vi.fn((params: Record<string, unknown>) => ({
9+baseUrl: params.baseUrl ?? params.defaultBaseUrl ?? "https://openrouter.ai/api/v1",
10+allowPrivateNetwork: false,
11+headers: new Headers(params.defaultHeaders as HeadersInit | undefined),
12+dispatcherPolicy: undefined,
13+})),
14+}));
15+16+vi.mock("openclaw/plugin-sdk/provider-http", () => ({
17+assertOkOrThrowHttpError: assertOkOrThrowHttpErrorMock,
18+postJsonRequest: postJsonRequestMock,
19+resolveProviderHttpRequestConfig: resolveProviderHttpRequestConfigMock,
20+}));
21+22+describe("openrouter speech provider", () => {
23+afterEach(() => {
24+assertOkOrThrowHttpErrorMock.mockClear();
25+postJsonRequestMock.mockReset();
26+resolveProviderHttpRequestConfigMock.mockClear();
27+vi.unstubAllEnvs();
28+});
29+30+it("normalizes provider-owned speech config", () => {
31+const provider = buildOpenRouterSpeechProvider();
32+const resolved = provider.resolveConfig?.({
33+cfg: {} as never,
34+timeoutMs: 30_000,
35+rawConfig: {
36+providers: {
37+openrouter: {
38+apiKey: "sk-test",
39+baseUrl: "https://openrouter.ai/v1/",
40+modelId: "google/gemini-3.1-flash-tts-preview",
41+voiceId: "Kore",
42+speed: 1.1,
43+responseFormat: " MP3 ",
44+provider: {
45+options: {
46+openai: {
47+instructions: "Speak warmly.",
48+},
49+},
50+},
51+},
52+},
53+},
54+});
55+56+expect(resolved).toEqual({
57+apiKey: "sk-test",
58+baseUrl: "https://openrouter.ai/api/v1",
59+model: "google/gemini-3.1-flash-tts-preview",
60+voice: "Kore",
61+speed: 1.1,
62+responseFormat: "mp3",
63+provider: {
64+options: {
65+openai: {
66+instructions: "Speak warmly.",
67+},
68+},
69+},
70+});
71+});
72+73+it("synthesizes OpenAI-compatible speech through OpenRouter", async () => {
74+const release = vi.fn(async () => {});
75+postJsonRequestMock.mockResolvedValue({
76+response: new Response(new Uint8Array([1, 2, 3]), { status: 200 }),
77+ release,
78+});
79+80+const provider = buildOpenRouterSpeechProvider();
81+const result = await provider.synthesize({
82+text: "hello",
83+cfg: {
84+models: {
85+providers: {
86+openrouter: {
87+apiKey: "sk-openrouter",
88+baseUrl: "https://openrouter.ai/v1/",
89+},
90+},
91+},
92+} as never,
93+providerConfig: {
94+model: "openai/gpt-4o-mini-tts-2025-12-15",
95+voice: "nova",
96+speed: 1.2,
97+},
98+target: "voice-note",
99+timeoutMs: 12_345,
100+});
101+102+expect(resolveProviderHttpRequestConfigMock).toHaveBeenCalledWith(
103+expect.objectContaining({
104+provider: "openrouter",
105+capability: "audio",
106+baseUrl: "https://openrouter.ai/api/v1",
107+defaultHeaders: expect.objectContaining({
108+"Content-Type": "application/json",
109+}),
110+}),
111+);
112+expect(postJsonRequestMock).toHaveBeenCalledWith(
113+expect.objectContaining({
114+url: "https://openrouter.ai/api/v1/audio/speech",
115+timeoutMs: 12_345,
116+body: {
117+model: "openai/gpt-4o-mini-tts-2025-12-15",
118+input: "hello",
119+voice: "nova",
120+response_format: "mp3",
121+speed: 1.2,
122+},
123+}),
124+);
125+expect(result.audioBuffer).toEqual(Buffer.from([1, 2, 3]));
126+expect(result.outputFormat).toBe("mp3");
127+expect(result.fileExtension).toBe(".mp3");
128+expect(result.voiceCompatible).toBe(true);
129+expect(release).toHaveBeenCalledOnce();
130+});
131+132+it("defaults to a live-proven OpenRouter TTS model", () => {
133+const provider = buildOpenRouterSpeechProvider();
134+135+expect(
136+provider.resolveConfig?.({ cfg: {} as never, rawConfig: {}, timeoutMs: 30_000 }),
137+).toMatchObject({
138+model: "hexgrad/kokoro-82m",
139+voice: "af_alloy",
140+});
141+});
142+143+it("uses OPENROUTER_API_KEY when provider config omits apiKey", () => {
144+vi.stubEnv("OPENROUTER_API_KEY", "sk-env");
145+const provider = buildOpenRouterSpeechProvider();
146+147+expect(
148+provider.isConfigured({
149+cfg: {} as never,
150+providerConfig: {},
151+timeoutMs: 30_000,
152+}),
153+).toBe(true);
154+});
155+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。