mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-14 19:05:49 +08:00
重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。
同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。 Made-with: Cursor
This commit is contained in:
parent
4a23b715a2
commit
dbbe3add6a
14438 changed files with 2693620 additions and 2546 deletions
195
openclaw/extensions/openai/speech-provider.test.ts
Normal file
195
openclaw/extensions/openai/speech-provider.test.ts
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { buildOpenAISpeechProvider } from "./speech-provider.js";
|
||||
|
||||
function isSpeechRequestBody(value: unknown): value is { response_format?: string } {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function parseRequestBody(init: RequestInit | undefined): { response_format?: string } {
|
||||
if (typeof init?.body !== "string") {
|
||||
throw new Error("expected string request body");
|
||||
}
|
||||
const body: unknown = JSON.parse(init.body);
|
||||
if (!isSpeechRequestBody(body)) {
|
||||
throw new Error("expected OpenAI speech request body");
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
describe("buildOpenAISpeechProvider", () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("normalizes provider-owned speech config from raw provider config", () => {
|
||||
const provider = buildOpenAISpeechProvider();
|
||||
const resolved = provider.resolveConfig?.({
|
||||
cfg: {} as never,
|
||||
timeoutMs: 30_000,
|
||||
rawConfig: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: "sk-test",
|
||||
baseUrl: "https://example.com/v1/",
|
||||
model: "tts-1",
|
||||
voice: "alloy",
|
||||
speed: 1.25,
|
||||
instructions: " Speak warmly ",
|
||||
responseFormat: " WAV ",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolved).toEqual({
|
||||
apiKey: "sk-test",
|
||||
baseUrl: "https://example.com/v1",
|
||||
model: "tts-1",
|
||||
voice: "alloy",
|
||||
speed: 1.25,
|
||||
instructions: "Speak warmly",
|
||||
responseFormat: "wav",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses OpenAI directive tokens against the resolved base url", () => {
|
||||
const provider = buildOpenAISpeechProvider();
|
||||
|
||||
expect(
|
||||
provider.parseDirectiveToken?.({
|
||||
key: "voice",
|
||||
value: "alloy",
|
||||
policy: {
|
||||
allowVoice: true,
|
||||
allowModelId: true,
|
||||
},
|
||||
providerConfig: {
|
||||
baseUrl: "https://api.openai.com/v1/",
|
||||
},
|
||||
} as never),
|
||||
).toEqual({
|
||||
handled: true,
|
||||
overrides: { voice: "alloy" },
|
||||
});
|
||||
|
||||
expect(
|
||||
provider.parseDirectiveToken?.({
|
||||
key: "model",
|
||||
value: "kokoro-custom-model",
|
||||
policy: {
|
||||
allowVoice: true,
|
||||
allowModelId: true,
|
||||
},
|
||||
providerConfig: {
|
||||
baseUrl: "https://api.openai.com/v1/",
|
||||
},
|
||||
} as never),
|
||||
).toEqual({
|
||||
handled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves talk responseFormat overrides", () => {
|
||||
const provider = buildOpenAISpeechProvider();
|
||||
|
||||
expect(
|
||||
provider.resolveTalkConfig?.({
|
||||
cfg: {} as never,
|
||||
timeoutMs: 30_000,
|
||||
baseTtsConfig: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: "sk-base",
|
||||
responseFormat: "mp3",
|
||||
},
|
||||
},
|
||||
},
|
||||
talkProviderConfig: {
|
||||
apiKey: "sk-talk",
|
||||
responseFormat: " WAV ",
|
||||
},
|
||||
}),
|
||||
).toMatchObject({
|
||||
apiKey: "sk-talk",
|
||||
responseFormat: "wav",
|
||||
});
|
||||
});
|
||||
|
||||
it("maps Talk speak params onto OpenAI speech overrides", () => {
|
||||
const provider = buildOpenAISpeechProvider();
|
||||
|
||||
expect(
|
||||
provider.resolveTalkOverrides?.({
|
||||
talkProviderConfig: {},
|
||||
params: {
|
||||
text: "Hello from talk mode.",
|
||||
voiceId: "nova",
|
||||
modelId: "tts-1",
|
||||
speed: 218 / 175,
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
voice: "nova",
|
||||
model: "tts-1",
|
||||
speed: 218 / 175,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses wav for Groq-compatible OpenAI TTS endpoints", async () => {
|
||||
const provider = buildOpenAISpeechProvider();
|
||||
const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => {
|
||||
const body = parseRequestBody(init);
|
||||
expect(body.response_format).toBe("wav");
|
||||
return new Response(new Uint8Array([1, 2, 3]), { status: 200 });
|
||||
});
|
||||
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
||||
|
||||
const result = await provider.synthesize({
|
||||
text: "hello",
|
||||
cfg: {} as never,
|
||||
providerConfig: {
|
||||
apiKey: "sk-test",
|
||||
baseUrl: "https://api.groq.com/openai/v1",
|
||||
model: "canopylabs/orpheus-v1-english",
|
||||
voice: "daniel",
|
||||
},
|
||||
target: "audio-file",
|
||||
timeoutMs: 1_000,
|
||||
});
|
||||
|
||||
expect(result.outputFormat).toBe("wav");
|
||||
expect(result.fileExtension).toBe(".wav");
|
||||
expect(result.voiceCompatible).toBe(false);
|
||||
});
|
||||
|
||||
it("honors explicit responseFormat overrides and clears voice-note compatibility when not opus", async () => {
|
||||
const provider = buildOpenAISpeechProvider();
|
||||
const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => {
|
||||
const body = parseRequestBody(init);
|
||||
expect(body.response_format).toBe("wav");
|
||||
return new Response(new Uint8Array([1, 2, 3]), { status: 200 });
|
||||
});
|
||||
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
||||
|
||||
const result = await provider.synthesize({
|
||||
text: "hello",
|
||||
cfg: {} as never,
|
||||
providerConfig: {
|
||||
apiKey: "sk-test",
|
||||
baseUrl: "https://proxy.example.com/openai/v1",
|
||||
model: "canopylabs/orpheus-v1-english",
|
||||
voice: "daniel",
|
||||
responseFormat: "wav",
|
||||
},
|
||||
target: "voice-note",
|
||||
timeoutMs: 1_000,
|
||||
});
|
||||
|
||||
expect(result.outputFormat).toBe("wav");
|
||||
expect(result.fileExtension).toBe(".wav");
|
||||
expect(result.voiceCompatible).toBe(false);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue