mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:10:44 +00:00
test: move more runtime specs to fast lane
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { SetSessionModeRequest } from "@agentclientprotocol/sdk";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { GatewayClient } from "../gateway/client.js";
|
||||
import { createInMemorySessionStore } from "./session.js";
|
||||
import { AcpGatewayAgent } from "./translator.js";
|
||||
@@ -24,38 +24,55 @@ function createAgentWithSession(request: GatewayClient["request"]) {
|
||||
});
|
||||
}
|
||||
|
||||
function createRequestRecorder(
|
||||
handler: (...args: Parameters<GatewayClient["request"]>) => Promise<unknown>,
|
||||
) {
|
||||
const calls: Parameters<GatewayClient["request"]>[] = [];
|
||||
const request = (async (...args: Parameters<GatewayClient["request"]>) => {
|
||||
calls.push(args);
|
||||
return handler(...args);
|
||||
}) as GatewayClient["request"];
|
||||
return { calls, request };
|
||||
}
|
||||
|
||||
describe("acp setSessionMode", () => {
|
||||
it("setSessionMode propagates gateway error", async () => {
|
||||
const request = vi.fn(async () => {
|
||||
const { calls, request } = createRequestRecorder(async () => {
|
||||
throw new Error("gateway rejected mode change");
|
||||
}) as GatewayClient["request"];
|
||||
});
|
||||
const agent = createAgentWithSession(request);
|
||||
|
||||
await expect(agent.setSessionMode(createSetSessionModeRequest("high"))).rejects.toThrow(
|
||||
"gateway rejected mode change",
|
||||
);
|
||||
expect(request).toHaveBeenCalledWith("sessions.patch", {
|
||||
key: "agent:main:main",
|
||||
thinkingLevel: "high",
|
||||
});
|
||||
expect(calls).toContainEqual([
|
||||
"sessions.patch",
|
||||
{
|
||||
key: "agent:main:main",
|
||||
thinkingLevel: "high",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("setSessionMode succeeds when gateway accepts", async () => {
|
||||
const request = vi.fn(async () => ({ ok: true })) as GatewayClient["request"];
|
||||
const { calls, request } = createRequestRecorder(async () => ({ ok: true }));
|
||||
const agent = createAgentWithSession(request);
|
||||
|
||||
await expect(agent.setSessionMode(createSetSessionModeRequest("low"))).resolves.toEqual({});
|
||||
expect(request).toHaveBeenCalledWith("sessions.patch", {
|
||||
key: "agent:main:main",
|
||||
thinkingLevel: "low",
|
||||
});
|
||||
expect(calls).toContainEqual([
|
||||
"sessions.patch",
|
||||
{
|
||||
key: "agent:main:main",
|
||||
thinkingLevel: "low",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("setSessionMode returns early for empty modeId", async () => {
|
||||
const request = vi.fn(async () => ({ ok: true })) as GatewayClient["request"];
|
||||
const { calls, request } = createRequestRecorder(async () => ({ ok: true }));
|
||||
const agent = createAgentWithSession(request);
|
||||
|
||||
await expect(agent.setSessionMode(createSetSessionModeRequest(""))).resolves.toEqual({});
|
||||
expect(request).not.toHaveBeenCalled();
|
||||
expect(calls).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
resolveCapabilityModelCandidates,
|
||||
throwCapabilityGenerationFailure,
|
||||
} from "../media-generation/runtime-shared.js";
|
||||
import { getProviderEnvVars } from "../secrets/provider-env-vars.js";
|
||||
import { parseImageGenerationModelRef } from "./model-ref.js";
|
||||
import { resolveImageGenerationOverrides } from "./normalization.js";
|
||||
import { getImageGenerationProvider, listImageGenerationProviders } from "./provider-registry.js";
|
||||
@@ -18,23 +19,42 @@ import type { ImageGenerationResult } from "./types.js";
|
||||
|
||||
const log = createSubsystemLogger("image-generation");
|
||||
|
||||
export type ImageGenerationRuntimeDeps = {
|
||||
getProvider?: typeof getImageGenerationProvider;
|
||||
listProviders?: typeof listImageGenerationProviders;
|
||||
getProviderEnvVars?: typeof getProviderEnvVars;
|
||||
log?: Pick<typeof log, "warn">;
|
||||
};
|
||||
|
||||
export type { GenerateImageParams, GenerateImageRuntimeResult } from "./runtime-types.js";
|
||||
|
||||
function buildNoImageGenerationModelConfiguredMessage(cfg: OpenClawConfig): string {
|
||||
function buildNoImageGenerationModelConfiguredMessage(
|
||||
cfg: OpenClawConfig,
|
||||
deps: ImageGenerationRuntimeDeps,
|
||||
): string {
|
||||
const listProviders = deps.listProviders ?? listImageGenerationProviders;
|
||||
return buildNoCapabilityModelConfiguredMessage({
|
||||
capabilityLabel: "image-generation",
|
||||
modelConfigKey: "imageGenerationModel",
|
||||
providers: listImageGenerationProviders(cfg),
|
||||
providers: listProviders(cfg),
|
||||
getProviderEnvVars: deps.getProviderEnvVars,
|
||||
});
|
||||
}
|
||||
|
||||
export function listRuntimeImageGenerationProviders(params?: { config?: OpenClawConfig }) {
|
||||
return listImageGenerationProviders(params?.config);
|
||||
export function listRuntimeImageGenerationProviders(
|
||||
params?: { config?: OpenClawConfig },
|
||||
deps: ImageGenerationRuntimeDeps = {},
|
||||
) {
|
||||
return (deps.listProviders ?? listImageGenerationProviders)(params?.config);
|
||||
}
|
||||
|
||||
export async function generateImage(
|
||||
params: GenerateImageParams,
|
||||
deps: ImageGenerationRuntimeDeps = {},
|
||||
): Promise<GenerateImageRuntimeResult> {
|
||||
const getProvider = deps.getProvider ?? getImageGenerationProvider;
|
||||
const listProviders = deps.listProviders ?? listImageGenerationProviders;
|
||||
const logger = deps.log ?? log;
|
||||
const timeoutMs =
|
||||
params.timeoutMs ??
|
||||
resolveAgentModelTimeoutMsValue(params.cfg.agents?.defaults?.imageGenerationModel);
|
||||
@@ -44,17 +64,17 @@ export async function generateImage(
|
||||
modelOverride: params.modelOverride,
|
||||
parseModelRef: parseImageGenerationModelRef,
|
||||
agentDir: params.agentDir,
|
||||
listProviders: listImageGenerationProviders,
|
||||
listProviders,
|
||||
});
|
||||
if (candidates.length === 0) {
|
||||
throw new Error(buildNoImageGenerationModelConfiguredMessage(params.cfg));
|
||||
throw new Error(buildNoImageGenerationModelConfiguredMessage(params.cfg, deps));
|
||||
}
|
||||
|
||||
const attempts: FallbackAttempt[] = [];
|
||||
let lastError: unknown;
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const provider = getImageGenerationProvider(candidate.provider, params.cfg);
|
||||
const provider = getProvider(candidate.provider, params.cfg);
|
||||
if (!provider) {
|
||||
const error = `No image-generation provider registered for ${candidate.provider}`;
|
||||
attempts.push({
|
||||
@@ -63,7 +83,7 @@ export async function generateImage(
|
||||
error,
|
||||
});
|
||||
lastError = new Error(error);
|
||||
log.warn(
|
||||
logger.warn(
|
||||
`image-generation candidate failed: ${candidate.provider}/${candidate.model}: ${error}`,
|
||||
);
|
||||
continue;
|
||||
@@ -127,7 +147,7 @@ export async function generateImage(
|
||||
status: described?.status,
|
||||
code: described?.code,
|
||||
});
|
||||
log.warn(
|
||||
logger.warn(
|
||||
`image-generation candidate failed: ${candidate.provider}/${candidate.model}: ${
|
||||
described?.message ?? formatErrorMessage(err)
|
||||
}`,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import type { AgentModelConfig } from "../config/types.agents-shared.js";
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { getProviderEnvVars } from "../secrets/provider-env-vars.js";
|
||||
import { getProviderEnvVars as getDefaultProviderEnvVars } from "../secrets/provider-env-vars.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import type {
|
||||
MediaGenerationNormalizationMetadataInput,
|
||||
@@ -504,7 +504,9 @@ export function buildNoCapabilityModelConfiguredMessage(params: {
|
||||
modelConfigKey: string;
|
||||
providers: Array<{ id: string; defaultModel?: string | null }>;
|
||||
fallbackSampleRef?: string;
|
||||
getProviderEnvVars?: typeof getDefaultProviderEnvVars;
|
||||
}): string {
|
||||
const getProviderEnvVars = params.getProviderEnvVars ?? getDefaultProviderEnvVars;
|
||||
const sampleModel = params.providers.find(
|
||||
(provider) =>
|
||||
normalizeOptionalString(provider.id) && normalizeOptionalString(provider.defaultModel),
|
||||
|
||||
@@ -1,24 +1,41 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
getMediaGenerationRuntimeMocks,
|
||||
resetMusicGenerationRuntimeMocks,
|
||||
} from "../../test/helpers/media-generation/runtime-module-mocks.js";
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import { generateMusic, listRuntimeMusicGenerationProviders } from "./runtime.js";
|
||||
import {
|
||||
generateMusic,
|
||||
listRuntimeMusicGenerationProviders,
|
||||
type GenerateMusicParams,
|
||||
type MusicGenerationRuntimeDeps,
|
||||
} from "./runtime.js";
|
||||
import type { MusicGenerationProvider } from "./types.js";
|
||||
|
||||
const mocks = getMediaGenerationRuntimeMocks();
|
||||
let providers: MusicGenerationProvider[] = [];
|
||||
let listedConfigs: Array<OpenClawConfig | undefined> = [];
|
||||
|
||||
const runtimeDeps: MusicGenerationRuntimeDeps = {
|
||||
getProvider: (providerId) => providers.find((provider) => provider.id === providerId),
|
||||
listProviders: (config) => {
|
||||
listedConfigs.push(config);
|
||||
return providers;
|
||||
},
|
||||
log: {
|
||||
debug: () => {},
|
||||
},
|
||||
};
|
||||
|
||||
function runGenerateMusic(params: GenerateMusicParams) {
|
||||
return generateMusic(params, runtimeDeps);
|
||||
}
|
||||
|
||||
describe("music-generation runtime", () => {
|
||||
beforeEach(() => {
|
||||
resetMusicGenerationRuntimeMocks();
|
||||
providers = [];
|
||||
listedConfigs = [];
|
||||
});
|
||||
|
||||
it("generates tracks through the active music-generation provider", async () => {
|
||||
const authStore = { version: 1, profiles: {} } as const;
|
||||
let seenAuthStore: unknown;
|
||||
let seenTimeoutMs: number | undefined;
|
||||
mocks.resolveAgentModelPrimaryValue.mockReturnValue("music-plugin/track-v1");
|
||||
const provider: MusicGenerationProvider = {
|
||||
id: "music-plugin",
|
||||
capabilities: {},
|
||||
@@ -37,9 +54,9 @@ describe("music-generation runtime", () => {
|
||||
};
|
||||
},
|
||||
};
|
||||
mocks.getMusicGenerationProvider.mockReturnValue(provider);
|
||||
providers = [provider];
|
||||
|
||||
const result = await generateMusic({
|
||||
const result = await runGenerateMusic({
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: {
|
||||
@@ -69,52 +86,31 @@ describe("music-generation runtime", () => {
|
||||
});
|
||||
|
||||
it("auto-detects and falls through to another configured music-generation provider by default", async () => {
|
||||
mocks.getMusicGenerationProvider.mockImplementation((providerId: string) => {
|
||||
if (providerId === "google") {
|
||||
return {
|
||||
id: "google",
|
||||
defaultModel: "lyria-3-clip-preview",
|
||||
capabilities: {},
|
||||
isConfigured: () => true,
|
||||
async generateMusic() {
|
||||
throw new Error("Google music generation response missing audio data");
|
||||
},
|
||||
};
|
||||
}
|
||||
if (providerId === "minimax") {
|
||||
return {
|
||||
id: "minimax",
|
||||
defaultModel: "music-2.6",
|
||||
capabilities: {},
|
||||
isConfigured: () => true,
|
||||
async generateMusic() {
|
||||
return {
|
||||
tracks: [{ buffer: Buffer.from("mp3-bytes"), mimeType: "audio/mpeg" }],
|
||||
model: "music-2.6",
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
mocks.listMusicGenerationProviders.mockReturnValue([
|
||||
providers = [
|
||||
{
|
||||
id: "google",
|
||||
defaultModel: "lyria-3-clip-preview",
|
||||
capabilities: {},
|
||||
isConfigured: () => true,
|
||||
generateMusic: async () => ({ tracks: [] }),
|
||||
async generateMusic() {
|
||||
throw new Error("Google music generation response missing audio data");
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "minimax",
|
||||
defaultModel: "music-2.6",
|
||||
capabilities: {},
|
||||
isConfigured: () => true,
|
||||
generateMusic: async () => ({ tracks: [] }),
|
||||
async generateMusic() {
|
||||
return {
|
||||
tracks: [{ buffer: Buffer.from("mp3-bytes"), mimeType: "audio/mpeg" }],
|
||||
model: "music-2.6",
|
||||
};
|
||||
},
|
||||
},
|
||||
]);
|
||||
];
|
||||
|
||||
const result = await generateMusic({
|
||||
const result = await runGenerateMusic({
|
||||
cfg: {} as OpenClawConfig,
|
||||
prompt: "play a synth line",
|
||||
});
|
||||
@@ -131,7 +127,7 @@ describe("music-generation runtime", () => {
|
||||
});
|
||||
|
||||
it("lists runtime music-generation providers through the provider registry", () => {
|
||||
const providers: MusicGenerationProvider[] = [
|
||||
const registryProviders: MusicGenerationProvider[] = [
|
||||
{
|
||||
id: "music-plugin",
|
||||
defaultModel: "track-v1",
|
||||
@@ -146,12 +142,12 @@ describe("music-generation runtime", () => {
|
||||
}),
|
||||
},
|
||||
];
|
||||
mocks.listMusicGenerationProviders.mockReturnValue(providers);
|
||||
providers = registryProviders;
|
||||
|
||||
expect(listRuntimeMusicGenerationProviders({ config: {} as OpenClawConfig })).toEqual(
|
||||
providers,
|
||||
);
|
||||
expect(mocks.listMusicGenerationProviders).toHaveBeenCalledWith({} as OpenClawConfig);
|
||||
expect(
|
||||
listRuntimeMusicGenerationProviders({ config: {} as OpenClawConfig }, runtimeDeps),
|
||||
).toEqual(registryProviders);
|
||||
expect(listedConfigs).toEqual([{} as OpenClawConfig]);
|
||||
});
|
||||
|
||||
it("ignores unsupported optional overrides per provider and model", async () => {
|
||||
@@ -163,34 +159,35 @@ describe("music-generation runtime", () => {
|
||||
format?: string;
|
||||
}
|
||||
| undefined;
|
||||
mocks.resolveAgentModelPrimaryValue.mockReturnValue("google/lyria-3-clip-preview");
|
||||
mocks.getMusicGenerationProvider.mockReturnValue({
|
||||
id: "google",
|
||||
capabilities: {
|
||||
generate: {
|
||||
supportsLyrics: true,
|
||||
supportsInstrumental: true,
|
||||
supportsFormat: true,
|
||||
supportedFormatsByModel: {
|
||||
"lyria-3-clip-preview": ["mp3"],
|
||||
providers = [
|
||||
{
|
||||
id: "google",
|
||||
capabilities: {
|
||||
generate: {
|
||||
supportsLyrics: true,
|
||||
supportsInstrumental: true,
|
||||
supportsFormat: true,
|
||||
supportedFormatsByModel: {
|
||||
"lyria-3-clip-preview": ["mp3"],
|
||||
},
|
||||
},
|
||||
},
|
||||
generateMusic: async (req) => {
|
||||
seenRequest = {
|
||||
lyrics: req.lyrics,
|
||||
instrumental: req.instrumental,
|
||||
durationSeconds: req.durationSeconds,
|
||||
format: req.format,
|
||||
};
|
||||
return {
|
||||
tracks: [{ buffer: Buffer.from("mp3-bytes"), mimeType: "audio/mpeg" }],
|
||||
model: "lyria-3-clip-preview",
|
||||
};
|
||||
},
|
||||
},
|
||||
generateMusic: async (req) => {
|
||||
seenRequest = {
|
||||
lyrics: req.lyrics,
|
||||
instrumental: req.instrumental,
|
||||
durationSeconds: req.durationSeconds,
|
||||
format: req.format,
|
||||
};
|
||||
return {
|
||||
tracks: [{ buffer: Buffer.from("mp3-bytes"), mimeType: "audio/mpeg" }],
|
||||
model: "lyria-3-clip-preview",
|
||||
};
|
||||
},
|
||||
});
|
||||
];
|
||||
|
||||
const result = await generateMusic({
|
||||
const result = await runGenerateMusic({
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: {
|
||||
@@ -226,40 +223,41 @@ describe("music-generation runtime", () => {
|
||||
format?: string;
|
||||
}
|
||||
| undefined;
|
||||
mocks.resolveAgentModelPrimaryValue.mockReturnValue("google/lyria-3-pro-preview");
|
||||
mocks.getMusicGenerationProvider.mockReturnValue({
|
||||
id: "google",
|
||||
capabilities: {
|
||||
generate: {
|
||||
supportsLyrics: false,
|
||||
supportsInstrumental: false,
|
||||
supportsFormat: true,
|
||||
supportedFormats: ["mp3"],
|
||||
providers = [
|
||||
{
|
||||
id: "google",
|
||||
capabilities: {
|
||||
generate: {
|
||||
supportsLyrics: false,
|
||||
supportsInstrumental: false,
|
||||
supportsFormat: true,
|
||||
supportedFormats: ["mp3"],
|
||||
},
|
||||
edit: {
|
||||
enabled: true,
|
||||
maxInputImages: 1,
|
||||
supportsLyrics: true,
|
||||
supportsInstrumental: true,
|
||||
supportsDuration: false,
|
||||
supportsFormat: false,
|
||||
},
|
||||
},
|
||||
edit: {
|
||||
enabled: true,
|
||||
maxInputImages: 1,
|
||||
supportsLyrics: true,
|
||||
supportsInstrumental: true,
|
||||
supportsDuration: false,
|
||||
supportsFormat: false,
|
||||
generateMusic: async (req) => {
|
||||
seenRequest = {
|
||||
lyrics: req.lyrics,
|
||||
instrumental: req.instrumental,
|
||||
durationSeconds: req.durationSeconds,
|
||||
format: req.format,
|
||||
};
|
||||
return {
|
||||
tracks: [{ buffer: Buffer.from("mp3-bytes"), mimeType: "audio/mpeg" }],
|
||||
model: "lyria-3-pro-preview",
|
||||
};
|
||||
},
|
||||
},
|
||||
generateMusic: async (req) => {
|
||||
seenRequest = {
|
||||
lyrics: req.lyrics,
|
||||
instrumental: req.instrumental,
|
||||
durationSeconds: req.durationSeconds,
|
||||
format: req.format,
|
||||
};
|
||||
return {
|
||||
tracks: [{ buffer: Buffer.from("mp3-bytes"), mimeType: "audio/mpeg" }],
|
||||
model: "lyria-3-pro-preview",
|
||||
};
|
||||
},
|
||||
});
|
||||
];
|
||||
|
||||
const result = await generateMusic({
|
||||
const result = await runGenerateMusic({
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: {
|
||||
@@ -293,27 +291,28 @@ describe("music-generation runtime", () => {
|
||||
durationSeconds?: number;
|
||||
}
|
||||
| undefined;
|
||||
mocks.resolveAgentModelPrimaryValue.mockReturnValue("minimax/music-2.6");
|
||||
mocks.getMusicGenerationProvider.mockReturnValue({
|
||||
id: "minimax",
|
||||
capabilities: {
|
||||
generate: {
|
||||
supportsDuration: true,
|
||||
maxDurationSeconds: 30,
|
||||
providers = [
|
||||
{
|
||||
id: "minimax",
|
||||
capabilities: {
|
||||
generate: {
|
||||
supportsDuration: true,
|
||||
maxDurationSeconds: 30,
|
||||
},
|
||||
},
|
||||
generateMusic: async (req) => {
|
||||
seenRequest = {
|
||||
durationSeconds: req.durationSeconds,
|
||||
};
|
||||
return {
|
||||
tracks: [{ buffer: Buffer.from("mp3-bytes"), mimeType: "audio/mpeg" }],
|
||||
model: "music-2.6",
|
||||
};
|
||||
},
|
||||
},
|
||||
generateMusic: async (req) => {
|
||||
seenRequest = {
|
||||
durationSeconds: req.durationSeconds,
|
||||
};
|
||||
return {
|
||||
tracks: [{ buffer: Buffer.from("mp3-bytes"), mimeType: "audio/mpeg" }],
|
||||
model: "music-2.6",
|
||||
};
|
||||
},
|
||||
});
|
||||
];
|
||||
|
||||
const result = await generateMusic({
|
||||
const result = await runGenerateMusic({
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
resolveCapabilityModelCandidates,
|
||||
throwCapabilityGenerationFailure,
|
||||
} from "../media-generation/runtime-shared.js";
|
||||
import { getProviderEnvVars } from "../secrets/provider-env-vars.js";
|
||||
import { parseMusicGenerationModelRef } from "./model-ref.js";
|
||||
import { resolveMusicGenerationOverrides } from "./normalization.js";
|
||||
import { getMusicGenerationProvider, listMusicGenerationProviders } from "./provider-registry.js";
|
||||
@@ -16,30 +17,45 @@ import type { MusicGenerationResult } from "./types.js";
|
||||
|
||||
const log = createSubsystemLogger("music-generation");
|
||||
|
||||
export type MusicGenerationRuntimeDeps = {
|
||||
getProvider?: typeof getMusicGenerationProvider;
|
||||
listProviders?: typeof listMusicGenerationProviders;
|
||||
getProviderEnvVars?: typeof getProviderEnvVars;
|
||||
log?: Pick<typeof log, "debug">;
|
||||
};
|
||||
|
||||
export type { GenerateMusicParams, GenerateMusicRuntimeResult } from "./runtime-types.js";
|
||||
|
||||
export function listRuntimeMusicGenerationProviders(params?: { config?: OpenClawConfig }) {
|
||||
return listMusicGenerationProviders(params?.config);
|
||||
export function listRuntimeMusicGenerationProviders(
|
||||
params?: { config?: OpenClawConfig },
|
||||
deps: MusicGenerationRuntimeDeps = {},
|
||||
) {
|
||||
return (deps.listProviders ?? listMusicGenerationProviders)(params?.config);
|
||||
}
|
||||
|
||||
export async function generateMusic(
|
||||
params: GenerateMusicParams,
|
||||
deps: MusicGenerationRuntimeDeps = {},
|
||||
): Promise<GenerateMusicRuntimeResult> {
|
||||
const getProvider = deps.getProvider ?? getMusicGenerationProvider;
|
||||
const listProviders = deps.listProviders ?? listMusicGenerationProviders;
|
||||
const logger = deps.log ?? log;
|
||||
const candidates = resolveCapabilityModelCandidates({
|
||||
cfg: params.cfg,
|
||||
modelConfig: params.cfg.agents?.defaults?.musicGenerationModel,
|
||||
modelOverride: params.modelOverride,
|
||||
parseModelRef: parseMusicGenerationModelRef,
|
||||
agentDir: params.agentDir,
|
||||
listProviders: listMusicGenerationProviders,
|
||||
listProviders,
|
||||
});
|
||||
if (candidates.length === 0) {
|
||||
throw new Error(
|
||||
buildNoCapabilityModelConfiguredMessage({
|
||||
capabilityLabel: "music-generation",
|
||||
modelConfigKey: "musicGenerationModel",
|
||||
providers: listMusicGenerationProviders(params.cfg),
|
||||
providers: listProviders(params.cfg),
|
||||
fallbackSampleRef: "google/lyria-3-clip-preview",
|
||||
getProviderEnvVars: deps.getProviderEnvVars,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -48,7 +64,7 @@ export async function generateMusic(
|
||||
let lastError: unknown;
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const provider = getMusicGenerationProvider(candidate.provider, params.cfg);
|
||||
const provider = getProvider(candidate.provider, params.cfg);
|
||||
if (!provider) {
|
||||
const error = `No music-generation provider registered for ${candidate.provider}`;
|
||||
attempts.push({
|
||||
@@ -110,7 +126,7 @@ export async function generateMusic(
|
||||
model: candidate.model,
|
||||
error: err,
|
||||
});
|
||||
log.debug(`music-generation candidate failed: ${candidate.provider}/${candidate.model}`);
|
||||
logger.debug(`music-generation candidate failed: ${candidate.provider}/${candidate.model}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +1,49 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import type { DebugProxySettings } from "./env.js";
|
||||
import {
|
||||
captureHttpExchange,
|
||||
finalizeDebugProxyCapture,
|
||||
initializeDebugProxyCapture,
|
||||
type DebugProxyCaptureRuntimeDeps,
|
||||
} from "./runtime.js";
|
||||
|
||||
const storeState = vi.hoisted(() => {
|
||||
const events: Record<string, unknown>[] = [];
|
||||
const store = {
|
||||
upsertSession: vi.fn(),
|
||||
endSession: vi.fn(),
|
||||
recordEvent: vi.fn((event: Record<string, unknown>) => {
|
||||
events.push(event);
|
||||
}),
|
||||
};
|
||||
return {
|
||||
events,
|
||||
store,
|
||||
closeDebugProxyCaptureStore: vi.fn(),
|
||||
};
|
||||
});
|
||||
type StoreCall = { name: string; args: unknown[] };
|
||||
|
||||
vi.mock("./store.sqlite.js", () => ({
|
||||
closeDebugProxyCaptureStore: storeState.closeDebugProxyCaptureStore,
|
||||
getDebugProxyCaptureStore: () => storeState.store,
|
||||
const settings: DebugProxySettings = {
|
||||
enabled: true,
|
||||
required: false,
|
||||
dbPath: "/tmp/openclaw-proxy-runtime-test.sqlite",
|
||||
blobDir: "/tmp/openclaw-proxy-runtime-test-blobs",
|
||||
certDir: "/tmp/openclaw-proxy-runtime-test-certs",
|
||||
sessionId: "runtime-test-session",
|
||||
sourceProcess: "runtime-test",
|
||||
};
|
||||
|
||||
const fetchTarget: typeof globalThis = {
|
||||
...globalThis,
|
||||
fetch: async () => new Response("{}", { status: 200 }),
|
||||
};
|
||||
|
||||
const events: Record<string, unknown>[] = [];
|
||||
const calls: StoreCall[] = [];
|
||||
const store = {
|
||||
upsertSession: (...args: unknown[]) => {
|
||||
calls.push({ name: "upsertSession", args });
|
||||
},
|
||||
endSession: (...args: unknown[]) => {
|
||||
calls.push({ name: "endSession", args });
|
||||
},
|
||||
recordEvent: (event: Record<string, unknown>) => {
|
||||
events.push(event);
|
||||
},
|
||||
};
|
||||
|
||||
const deps: DebugProxyCaptureRuntimeDeps = {
|
||||
fetchTarget,
|
||||
getStore: () => store,
|
||||
closeStore: () => {
|
||||
calls.push({ name: "closeStore", args: [] });
|
||||
},
|
||||
persistEventPayload: (
|
||||
_store: unknown,
|
||||
payload: { data?: Buffer | string | null; contentType?: string },
|
||||
@@ -32,87 +52,60 @@ vi.mock("./store.sqlite.js", () => ({
|
||||
...(typeof payload.data === "string" ? { dataText: payload.data } : {}),
|
||||
}),
|
||||
safeJsonString: (value: unknown) => (value == null ? undefined : JSON.stringify(value)),
|
||||
}));
|
||||
};
|
||||
|
||||
describe("debug proxy runtime", () => {
|
||||
const envKeys = [
|
||||
"OPENCLAW_DEBUG_PROXY_ENABLED",
|
||||
"OPENCLAW_DEBUG_PROXY_DB_PATH",
|
||||
"OPENCLAW_DEBUG_PROXY_BLOB_DIR",
|
||||
"OPENCLAW_DEBUG_PROXY_SESSION_ID",
|
||||
"OPENCLAW_DEBUG_PROXY_SOURCE_PROCESS",
|
||||
] as const;
|
||||
const savedEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]]));
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
storeState.events.length = 0;
|
||||
storeState.store.upsertSession.mockClear();
|
||||
storeState.store.endSession.mockClear();
|
||||
storeState.store.recordEvent.mockClear();
|
||||
storeState.closeDebugProxyCaptureStore.mockClear();
|
||||
process.env.OPENCLAW_DEBUG_PROXY_ENABLED = "1";
|
||||
process.env.OPENCLAW_DEBUG_PROXY_DB_PATH = "/tmp/openclaw-proxy-runtime-test.sqlite";
|
||||
process.env.OPENCLAW_DEBUG_PROXY_BLOB_DIR = "/tmp/openclaw-proxy-runtime-test-blobs";
|
||||
process.env.OPENCLAW_DEBUG_PROXY_SESSION_ID = "runtime-test-session";
|
||||
process.env.OPENCLAW_DEBUG_PROXY_SOURCE_PROCESS = "runtime-test";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
finalizeDebugProxyCapture();
|
||||
globalThis.fetch = originalFetch;
|
||||
for (const key of envKeys) {
|
||||
const value = savedEnv[key];
|
||||
if (value == null) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
finalizeDebugProxyCapture(settings, deps);
|
||||
events.length = 0;
|
||||
calls.length = 0;
|
||||
fetchTarget.fetch = async () => new Response("{}", { status: 200 });
|
||||
});
|
||||
|
||||
it("captures ambient global fetch calls when debug proxy mode is enabled", async () => {
|
||||
globalThis.fetch = vi.fn(async () => new Response("{}", { status: 200 })) as typeof fetch;
|
||||
|
||||
initializeDebugProxyCapture("test");
|
||||
await globalThis.fetch("https://api.minimax.io/anthropic/messages", {
|
||||
initializeDebugProxyCapture("test", settings, deps);
|
||||
await fetchTarget.fetch("https://api.minimax.io/anthropic/messages", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: '{"input":"hello"}',
|
||||
});
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
finalizeDebugProxyCapture();
|
||||
finalizeDebugProxyCapture(settings, deps);
|
||||
|
||||
const events = storeState.events.filter((event) => event.sessionId === "runtime-test-session");
|
||||
expect(events.some((event) => event.host === "api.minimax.io")).toBe(true);
|
||||
expect(events.some((event) => event.kind === "request")).toBe(true);
|
||||
expect(events.some((event) => event.kind === "response")).toBe(true);
|
||||
const sessionEvents = events.filter((event) => event.sessionId === "runtime-test-session");
|
||||
expect(sessionEvents.some((event) => event.host === "api.minimax.io")).toBe(true);
|
||||
expect(sessionEvents.some((event) => event.kind === "request")).toBe(true);
|
||||
expect(sessionEvents.some((event) => event.kind === "response")).toBe(true);
|
||||
});
|
||||
|
||||
it("redacts sensitive request and response headers before persistence", async () => {
|
||||
initializeDebugProxyCapture("test");
|
||||
captureHttpExchange({
|
||||
url: "https://discord.com/api/v10/gateway/bot",
|
||||
method: "GET",
|
||||
requestHeaders: {
|
||||
Authorization: "Bot discord-token",
|
||||
Cookie: "sid=session-token",
|
||||
"x-api-key": "provider-key",
|
||||
"content-type": "application/json",
|
||||
"x-safe": "visible",
|
||||
},
|
||||
response: new Response("{}", {
|
||||
status: 200,
|
||||
headers: {
|
||||
initializeDebugProxyCapture("test", settings, deps);
|
||||
captureHttpExchange(
|
||||
{
|
||||
url: "https://discord.com/api/v10/gateway/bot",
|
||||
method: "GET",
|
||||
requestHeaders: {
|
||||
Authorization: "Bot discord-token",
|
||||
Cookie: "sid=session-token",
|
||||
"x-api-key": "provider-key",
|
||||
"content-type": "application/json",
|
||||
"set-cookie": "sid=response-token",
|
||||
"x-safe": "visible",
|
||||
},
|
||||
}),
|
||||
});
|
||||
response: new Response("{}", {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"set-cookie": "sid=response-token",
|
||||
},
|
||||
}),
|
||||
},
|
||||
settings,
|
||||
deps,
|
||||
);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
finalizeDebugProxyCapture();
|
||||
finalizeDebugProxyCapture(settings, deps);
|
||||
|
||||
const request = storeState.events.find((event) => event.kind === "request");
|
||||
const request = events.find((event) => event.kind === "request");
|
||||
expect(JSON.parse(String(request?.headersJson))).toMatchObject({
|
||||
Authorization: "[REDACTED]",
|
||||
Cookie: "[REDACTED]",
|
||||
@@ -120,7 +113,7 @@ describe("debug proxy runtime", () => {
|
||||
"content-type": "application/json",
|
||||
"x-safe": "visible",
|
||||
});
|
||||
const response = storeState.events.find((event) => event.kind === "response");
|
||||
const response = events.find((event) => event.kind === "response");
|
||||
expect(JSON.parse(String(response?.headersJson))).toMatchObject({
|
||||
"content-type": "application/json",
|
||||
"set-cookie": "[REDACTED]",
|
||||
|
||||
@@ -47,6 +47,35 @@ type GlobalFetchPatchTarget = typeof globalThis & {
|
||||
[DEBUG_PROXY_FETCH_PATCH_KEY]?: GlobalFetchPatchedState;
|
||||
};
|
||||
|
||||
type DebugProxyCaptureStoreLike = Pick<
|
||||
ReturnType<typeof getDebugProxyCaptureStore>,
|
||||
"upsertSession" | "endSession" | "recordEvent"
|
||||
>;
|
||||
|
||||
export type DebugProxyCaptureRuntimeDeps = {
|
||||
getStore?: (dbPath: string, blobDir: string) => DebugProxyCaptureStoreLike;
|
||||
closeStore?: typeof closeDebugProxyCaptureStore;
|
||||
persistEventPayload?: (
|
||||
store: DebugProxyCaptureStoreLike,
|
||||
payload: Parameters<typeof persistEventPayload>[1],
|
||||
) => ReturnType<typeof persistEventPayload>;
|
||||
safeJsonString?: typeof safeJsonString;
|
||||
fetchTarget?: typeof globalThis;
|
||||
};
|
||||
|
||||
function resolveRuntimeDeps(deps: DebugProxyCaptureRuntimeDeps = {}) {
|
||||
return {
|
||||
getStore: deps.getStore ?? getDebugProxyCaptureStore,
|
||||
closeStore: deps.closeStore ?? closeDebugProxyCaptureStore,
|
||||
persistEventPayload:
|
||||
deps.persistEventPayload ??
|
||||
((store, payload) =>
|
||||
persistEventPayload(store as ReturnType<typeof getDebugProxyCaptureStore>, payload)),
|
||||
safeJsonString: deps.safeJsonString ?? safeJsonString,
|
||||
fetchTarget: deps.fetchTarget ?? globalThis,
|
||||
};
|
||||
}
|
||||
|
||||
function protocolFromUrl(rawUrl: string): CaptureProtocol {
|
||||
try {
|
||||
const url = new URL(rawUrl);
|
||||
@@ -129,51 +158,59 @@ function createHttpCaptureEventBase(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function installDebugProxyGlobalFetchPatch(settings: DebugProxySettings): void {
|
||||
if (typeof globalThis.fetch !== "function") {
|
||||
function installDebugProxyGlobalFetchPatch(
|
||||
settings: DebugProxySettings,
|
||||
deps: DebugProxyCaptureRuntimeDeps = {},
|
||||
): void {
|
||||
const runtime = resolveRuntimeDeps(deps);
|
||||
const fetchTarget = runtime.fetchTarget as GlobalFetchPatchTarget;
|
||||
if (typeof fetchTarget.fetch !== "function") {
|
||||
return;
|
||||
}
|
||||
const patched = globalThis as GlobalFetchPatchTarget;
|
||||
if (patched[DEBUG_PROXY_FETCH_PATCH_KEY]) {
|
||||
if (fetchTarget[DEBUG_PROXY_FETCH_PATCH_KEY]) {
|
||||
return;
|
||||
}
|
||||
const originalFetch = globalThis.fetch.bind(globalThis);
|
||||
patched[DEBUG_PROXY_FETCH_PATCH_KEY] = { originalFetch };
|
||||
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const originalFetch = fetchTarget.fetch.bind(fetchTarget);
|
||||
fetchTarget[DEBUG_PROXY_FETCH_PATCH_KEY] = { originalFetch };
|
||||
fetchTarget.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = resolveUrlString(input);
|
||||
try {
|
||||
const response = await originalFetch(input, init);
|
||||
if (url && /^https?:/i.test(url)) {
|
||||
captureHttpExchange({
|
||||
url,
|
||||
method:
|
||||
(typeof Request !== "undefined" && input instanceof Request
|
||||
? input.method
|
||||
: undefined) ??
|
||||
init?.method ??
|
||||
"GET",
|
||||
requestHeaders:
|
||||
(typeof Request !== "undefined" && input instanceof Request
|
||||
? input.headers
|
||||
: undefined) ?? (init?.headers as Headers | Record<string, string> | undefined),
|
||||
requestBody:
|
||||
(typeof Request !== "undefined" && input instanceof Request
|
||||
? (input as Request & { body?: BodyInit | null }).body
|
||||
: undefined) ??
|
||||
(init as (RequestInit & { body?: BodyInit | null }) | undefined)?.body ??
|
||||
null,
|
||||
response,
|
||||
transport: "http",
|
||||
meta: {
|
||||
captureOrigin: "global-fetch",
|
||||
source: settings.sourceProcess,
|
||||
captureHttpExchange(
|
||||
{
|
||||
url,
|
||||
method:
|
||||
(typeof Request !== "undefined" && input instanceof Request
|
||||
? input.method
|
||||
: undefined) ??
|
||||
init?.method ??
|
||||
"GET",
|
||||
requestHeaders:
|
||||
(typeof Request !== "undefined" && input instanceof Request
|
||||
? input.headers
|
||||
: undefined) ?? (init?.headers as Headers | Record<string, string> | undefined),
|
||||
requestBody:
|
||||
(typeof Request !== "undefined" && input instanceof Request
|
||||
? (input as Request & { body?: BodyInit | null }).body
|
||||
: undefined) ??
|
||||
(init as (RequestInit & { body?: BodyInit | null }) | undefined)?.body ??
|
||||
null,
|
||||
response,
|
||||
transport: "http",
|
||||
meta: {
|
||||
captureOrigin: "global-fetch",
|
||||
source: settings.sourceProcess,
|
||||
},
|
||||
},
|
||||
});
|
||||
settings,
|
||||
deps,
|
||||
);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (url && /^https?:/i.test(url)) {
|
||||
const store = getDebugProxyCaptureStore(settings.dbPath, settings.blobDir);
|
||||
const store = runtime.getStore(settings.dbPath, settings.blobDir);
|
||||
const parsed = new URL(url);
|
||||
store.recordEvent({
|
||||
sessionId: settings.sessionId,
|
||||
@@ -193,7 +230,7 @@ function installDebugProxyGlobalFetchPatch(settings: DebugProxySettings): void {
|
||||
host: parsed.host,
|
||||
path: `${parsed.pathname}${parsed.search}`,
|
||||
errorText: error instanceof Error ? error.message : String(error),
|
||||
metaJson: safeJsonString({ captureOrigin: "global-fetch" }),
|
||||
metaJson: runtime.safeJsonString({ captureOrigin: "global-fetch" }),
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
@@ -201,26 +238,30 @@ function installDebugProxyGlobalFetchPatch(settings: DebugProxySettings): void {
|
||||
}) as typeof globalThis.fetch;
|
||||
}
|
||||
|
||||
function uninstallDebugProxyGlobalFetchPatch(): void {
|
||||
const patched = globalThis as GlobalFetchPatchTarget;
|
||||
const state = patched[DEBUG_PROXY_FETCH_PATCH_KEY];
|
||||
function uninstallDebugProxyGlobalFetchPatch(deps: DebugProxyCaptureRuntimeDeps = {}): void {
|
||||
const fetchTarget = resolveRuntimeDeps(deps).fetchTarget as GlobalFetchPatchTarget;
|
||||
const state = fetchTarget[DEBUG_PROXY_FETCH_PATCH_KEY];
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
globalThis.fetch = state.originalFetch;
|
||||
delete patched[DEBUG_PROXY_FETCH_PATCH_KEY];
|
||||
fetchTarget.fetch = state.originalFetch;
|
||||
delete fetchTarget[DEBUG_PROXY_FETCH_PATCH_KEY];
|
||||
}
|
||||
|
||||
export function isDebugProxyGlobalFetchPatchInstalled(): boolean {
|
||||
return Boolean((globalThis as GlobalFetchPatchTarget)[DEBUG_PROXY_FETCH_PATCH_KEY]);
|
||||
}
|
||||
|
||||
export function initializeDebugProxyCapture(mode: string, resolved?: DebugProxySettings): void {
|
||||
export function initializeDebugProxyCapture(
|
||||
mode: string,
|
||||
resolved?: DebugProxySettings,
|
||||
deps: DebugProxyCaptureRuntimeDeps = {},
|
||||
): void {
|
||||
const settings = resolved ?? resolveDebugProxySettings();
|
||||
if (!settings.enabled) {
|
||||
return;
|
||||
}
|
||||
getDebugProxyCaptureStore(settings.dbPath, settings.blobDir).upsertSession({
|
||||
resolveRuntimeDeps(deps).getStore(settings.dbPath, settings.blobDir).upsertSession({
|
||||
id: settings.sessionId,
|
||||
startedAt: Date.now(),
|
||||
mode,
|
||||
@@ -230,41 +271,50 @@ export function initializeDebugProxyCapture(mode: string, resolved?: DebugProxyS
|
||||
dbPath: settings.dbPath,
|
||||
blobDir: settings.blobDir,
|
||||
});
|
||||
installDebugProxyGlobalFetchPatch(settings);
|
||||
installDebugProxyGlobalFetchPatch(settings, deps);
|
||||
}
|
||||
|
||||
export function finalizeDebugProxyCapture(resolved?: DebugProxySettings): void {
|
||||
export function finalizeDebugProxyCapture(
|
||||
resolved?: DebugProxySettings,
|
||||
deps: DebugProxyCaptureRuntimeDeps = {},
|
||||
): void {
|
||||
const settings = resolved ?? resolveDebugProxySettings();
|
||||
if (!settings.enabled) {
|
||||
return;
|
||||
}
|
||||
getDebugProxyCaptureStore(settings.dbPath, settings.blobDir).endSession(settings.sessionId);
|
||||
uninstallDebugProxyGlobalFetchPatch();
|
||||
closeDebugProxyCaptureStore();
|
||||
const runtime = resolveRuntimeDeps(deps);
|
||||
runtime.getStore(settings.dbPath, settings.blobDir).endSession(settings.sessionId);
|
||||
uninstallDebugProxyGlobalFetchPatch(deps);
|
||||
runtime.closeStore();
|
||||
}
|
||||
|
||||
export function captureHttpExchange(params: {
|
||||
url: string;
|
||||
method: string;
|
||||
requestHeaders?: Headers | Record<string, string> | undefined;
|
||||
requestBody?: BodyInit | Buffer | string | null;
|
||||
response: Response;
|
||||
transport?: "http" | "sse";
|
||||
flowId?: string;
|
||||
meta?: Record<string, unknown>;
|
||||
}): void {
|
||||
const settings = resolveDebugProxySettings();
|
||||
export function captureHttpExchange(
|
||||
params: {
|
||||
url: string;
|
||||
method: string;
|
||||
requestHeaders?: Headers | Record<string, string> | undefined;
|
||||
requestBody?: BodyInit | Buffer | string | null;
|
||||
response: Response;
|
||||
transport?: "http" | "sse";
|
||||
flowId?: string;
|
||||
meta?: Record<string, unknown>;
|
||||
},
|
||||
resolved?: DebugProxySettings,
|
||||
deps: DebugProxyCaptureRuntimeDeps = {},
|
||||
): void {
|
||||
const settings = resolved ?? resolveDebugProxySettings();
|
||||
if (!settings.enabled) {
|
||||
return;
|
||||
}
|
||||
const store = getDebugProxyCaptureStore(settings.dbPath, settings.blobDir);
|
||||
const runtime = resolveRuntimeDeps(deps);
|
||||
const store = runtime.getStore(settings.dbPath, settings.blobDir);
|
||||
const flowId = params.flowId ?? randomUUID();
|
||||
const url = new URL(params.url);
|
||||
const requestBody =
|
||||
typeof params.requestBody === "string" || Buffer.isBuffer(params.requestBody)
|
||||
? params.requestBody
|
||||
: null;
|
||||
const requestPayload = persistEventPayload(store, {
|
||||
const requestPayload = runtime.persistEventPayload(store, {
|
||||
data: requestBody,
|
||||
contentType:
|
||||
params.requestHeaders instanceof Headers
|
||||
@@ -286,8 +336,8 @@ export function captureHttpExchange(params: {
|
||||
params.requestHeaders instanceof Headers
|
||||
? (params.requestHeaders.get("content-type") ?? undefined)
|
||||
: params.requestHeaders?.["content-type"],
|
||||
headersJson: safeJsonString(redactedCaptureHeaders(params.requestHeaders)),
|
||||
metaJson: safeJsonString(params.meta),
|
||||
headersJson: runtime.safeJsonString(redactedCaptureHeaders(params.requestHeaders)),
|
||||
metaJson: runtime.safeJsonString(params.meta),
|
||||
...requestPayload,
|
||||
});
|
||||
const cloneable =
|
||||
@@ -313,9 +363,9 @@ export function captureHttpExchange(params: {
|
||||
: undefined,
|
||||
headersJson:
|
||||
params.response.headers && typeof params.response.headers.entries === "function"
|
||||
? safeJsonString(redactedCaptureHeaders(params.response.headers))
|
||||
? runtime.safeJsonString(redactedCaptureHeaders(params.response.headers))
|
||||
: undefined,
|
||||
metaJson: safeJsonString({ ...params.meta, bodyCapture: "unavailable" }),
|
||||
metaJson: runtime.safeJsonString({ ...params.meta, bodyCapture: "unavailable" }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -323,7 +373,7 @@ export function captureHttpExchange(params: {
|
||||
.clone()
|
||||
.arrayBuffer()
|
||||
.then((buffer) => {
|
||||
const responsePayload = persistEventPayload(store, {
|
||||
const responsePayload = runtime.persistEventPayload(store, {
|
||||
data: Buffer.from(buffer),
|
||||
contentType: params.response.headers.get("content-type") ?? undefined,
|
||||
});
|
||||
@@ -340,8 +390,8 @@ export function captureHttpExchange(params: {
|
||||
}),
|
||||
status: params.response.status,
|
||||
contentType: params.response.headers.get("content-type") ?? undefined,
|
||||
headersJson: safeJsonString(redactedCaptureHeaders(params.response.headers)),
|
||||
metaJson: safeJsonString(params.meta),
|
||||
headersJson: runtime.safeJsonString(redactedCaptureHeaders(params.response.headers)),
|
||||
metaJson: runtime.safeJsonString(params.meta),
|
||||
...responsePayload,
|
||||
});
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
resolveCapabilityModelCandidates,
|
||||
throwCapabilityGenerationFailure,
|
||||
} from "../media-generation/runtime-shared.js";
|
||||
import { getProviderEnvVars } from "../secrets/provider-env-vars.js";
|
||||
import { resolveVideoGenerationModeCapabilities } from "./capabilities.js";
|
||||
import { resolveVideoGenerationSupportedDurations } from "./duration-support.js";
|
||||
import { parseVideoGenerationModelRef } from "./model-ref.js";
|
||||
@@ -17,6 +18,14 @@ import type { GenerateVideoParams, GenerateVideoRuntimeResult } from "./runtime-
|
||||
import type { VideoGenerationProviderOptionType, VideoGenerationResult } from "./types.js";
|
||||
|
||||
const log = createSubsystemLogger("video-generation");
|
||||
|
||||
export type VideoGenerationRuntimeDeps = {
|
||||
getProvider?: typeof getVideoGenerationProvider;
|
||||
listProviders?: typeof listVideoGenerationProviders;
|
||||
getProviderEnvVars?: typeof getProviderEnvVars;
|
||||
log?: Pick<typeof log, "debug" | "warn">;
|
||||
};
|
||||
|
||||
export type { GenerateVideoParams, GenerateVideoRuntimeResult } from "./runtime-types.js";
|
||||
|
||||
/**
|
||||
@@ -73,31 +82,43 @@ function validateProviderOptionsAgainstDeclaration(params: {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function buildNoVideoGenerationModelConfiguredMessage(cfg: OpenClawConfig): string {
|
||||
function buildNoVideoGenerationModelConfiguredMessage(
|
||||
cfg: OpenClawConfig,
|
||||
deps: VideoGenerationRuntimeDeps,
|
||||
): string {
|
||||
const listProviders = deps.listProviders ?? listVideoGenerationProviders;
|
||||
return buildNoCapabilityModelConfiguredMessage({
|
||||
capabilityLabel: "video-generation",
|
||||
modelConfigKey: "videoGenerationModel",
|
||||
providers: listVideoGenerationProviders(cfg),
|
||||
providers: listProviders(cfg),
|
||||
getProviderEnvVars: deps.getProviderEnvVars,
|
||||
});
|
||||
}
|
||||
|
||||
export function listRuntimeVideoGenerationProviders(params?: { config?: OpenClawConfig }) {
|
||||
return listVideoGenerationProviders(params?.config);
|
||||
export function listRuntimeVideoGenerationProviders(
|
||||
params?: { config?: OpenClawConfig },
|
||||
deps: VideoGenerationRuntimeDeps = {},
|
||||
) {
|
||||
return (deps.listProviders ?? listVideoGenerationProviders)(params?.config);
|
||||
}
|
||||
|
||||
export async function generateVideo(
|
||||
params: GenerateVideoParams,
|
||||
deps: VideoGenerationRuntimeDeps = {},
|
||||
): Promise<GenerateVideoRuntimeResult> {
|
||||
const getProvider = deps.getProvider ?? getVideoGenerationProvider;
|
||||
const listProviders = deps.listProviders ?? listVideoGenerationProviders;
|
||||
const logger = deps.log ?? log;
|
||||
const candidates = resolveCapabilityModelCandidates({
|
||||
cfg: params.cfg,
|
||||
modelConfig: params.cfg.agents?.defaults?.videoGenerationModel,
|
||||
modelOverride: params.modelOverride,
|
||||
parseModelRef: parseVideoGenerationModelRef,
|
||||
agentDir: params.agentDir,
|
||||
listProviders: listVideoGenerationProviders,
|
||||
listProviders,
|
||||
});
|
||||
if (candidates.length === 0) {
|
||||
throw new Error(buildNoVideoGenerationModelConfiguredMessage(params.cfg));
|
||||
throw new Error(buildNoVideoGenerationModelConfiguredMessage(params.cfg, deps));
|
||||
}
|
||||
|
||||
const attempts: FallbackAttempt[] = [];
|
||||
@@ -110,12 +131,12 @@ export async function generateVideo(
|
||||
// passed over without flooding logs on long fallback chains.
|
||||
if (!skipWarnEmitted) {
|
||||
skipWarnEmitted = true;
|
||||
log.warn(`video-generation candidate skipped: ${reason}`);
|
||||
logger.warn(`video-generation candidate skipped: ${reason}`);
|
||||
}
|
||||
};
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const provider = getVideoGenerationProvider(candidate.provider, params.cfg);
|
||||
const provider = getProvider(candidate.provider, params.cfg);
|
||||
if (!provider) {
|
||||
const error = `No video-generation provider registered for ${candidate.provider}`;
|
||||
attempts.push({
|
||||
@@ -151,7 +172,7 @@ export async function generateVideo(
|
||||
attempts.push({ provider: candidate.provider, model: candidate.model, error });
|
||||
lastError = new Error(error);
|
||||
warnOnFirstSkip(error);
|
||||
log.debug(
|
||||
logger.debug(
|
||||
`video-generation candidate skipped (audio capability): ${candidate.provider}/${candidate.model}`,
|
||||
);
|
||||
continue;
|
||||
@@ -188,7 +209,7 @@ export async function generateVideo(
|
||||
attempts.push({ provider: candidate.provider, model: candidate.model, error: mismatch });
|
||||
lastError = new Error(mismatch);
|
||||
warnOnFirstSkip(mismatch);
|
||||
log.debug(
|
||||
logger.debug(
|
||||
`video-generation candidate skipped (providerOptions): ${candidate.provider}/${candidate.model}`,
|
||||
);
|
||||
continue;
|
||||
@@ -226,7 +247,7 @@ export async function generateVideo(
|
||||
attempts.push({ provider: candidate.provider, model: candidate.model, error });
|
||||
lastError = new Error(error);
|
||||
warnOnFirstSkip(error);
|
||||
log.debug(
|
||||
logger.debug(
|
||||
`video-generation candidate skipped (duration capability): ${candidate.provider}/${candidate.model}`,
|
||||
);
|
||||
continue;
|
||||
@@ -299,7 +320,7 @@ export async function generateVideo(
|
||||
model: candidate.model,
|
||||
error: err,
|
||||
});
|
||||
log.debug(`video-generation candidate failed: ${candidate.provider}/${candidate.model}`);
|
||||
logger.debug(`video-generation candidate failed: ${candidate.provider}/${candidate.model}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ export const forcedUnitFastTestFiles = [
|
||||
"src/acp/persistent-bindings.test.ts",
|
||||
"src/acp/server.startup.test.ts",
|
||||
"src/acp/translator.session-rate-limit.test.ts",
|
||||
"src/acp/translator.set-session-mode.test.ts",
|
||||
"src/browser-lifecycle-cleanup.test.ts",
|
||||
"src/canvas-host/server.test.ts",
|
||||
"src/crestodian/audit.test.ts",
|
||||
@@ -96,6 +97,7 @@ export const forcedUnitFastTestFiles = [
|
||||
"src/memory-host-sdk/host/embeddings-remote-fetch.test.ts",
|
||||
"src/memory-host-sdk/host/post-json.test.ts",
|
||||
"src/memory-host-sdk/host/session-files.test.ts",
|
||||
"src/music-generation/runtime.test.ts",
|
||||
"src/mcp/channel-server.shutdown-unhandled-rejection.test.ts",
|
||||
"src/node-host/invoke-system-run-plan.test.ts",
|
||||
"src/node-host/invoke-system-run.test.ts",
|
||||
@@ -104,6 +106,7 @@ export const forcedUnitFastTestFiles = [
|
||||
"src/pairing/setup-code.test.ts",
|
||||
"src/plugin-activation-boundary.test.ts",
|
||||
"src/plugin-sdk/memory-host-events.test.ts",
|
||||
"src/proxy-capture/runtime.test.ts",
|
||||
"src/proxy-capture/store.sqlite.test.ts",
|
||||
"src/security/audit-exec-surface.test.ts",
|
||||
"src/security/audit-extra.async.test.ts",
|
||||
|
||||
Reference in New Issue
Block a user