import { afterEach, describe, expect, it, vi } from
"vitest" ;
import { buildOpenRouterSpeechProvider } from
"./speech-provider.js" ;
const { assertOkOrThrowHttpErrorMock, postJsonRequestMock, resolveProviderHttpReques
tConfigMock } =
vi.hoisted(() => ({
assertOkOrThrowHttpErrorMock: vi.fn(async () => {}),
postJsonRequestMock: vi.fn(),
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,
resolveProviderHttpRequestConfig: resolveProviderHttpRequestConfigMock,
}));
describe("openrouter speech provider" , () => {
afterEach(() => {
assertOkOrThrowHttpErrorMock.mockClear();
postJsonRequestMock.mockReset();
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).toHaveBeenCalledWith(
expect.objectContaining({
provider: "openrouter" ,
capability: "audio" ,
baseUrl: "https://openrouter.ai/api/v1 ",
defaultHeaders: expect.objectContaining({
"Content-Type" : "application/json" ,
}),
}),
);
expect(postJsonRequestMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://openrouter.ai/api/v1/audio/speech ",
timeoutMs: 12 _345 ,
body: {
model: "openai/gpt-4o-mini-tts-2025-12-15" ,
input: "hello" ,
voice: "nova" ,
response_format: "mp3" ,
speed: 1 .2 ,
},
}),
);
expect(result.audioBuffer).toEqual(Buffer.from([1 , 2 , 3 ]));
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 }),
).toMatchObject({
model: "hexgrad/kokoro-82m" ,
voice: "af_alloy" ,
});
});
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 );
});
});
Messung V0.5 in Prozent C=97 H=100 G=98
¤ Dauer der Verarbeitung: 0.11 Sekunden
(vorverarbeitet am 2026-06-05)
¤
*© Formatika GbR, Deutschland