openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
298 lines (297 loc) • 11.8 kB
JavaScript
import { P as resolvePositiveTimerTimeoutMs } from "./number-coercion-CLj0HTDM.js";
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js";
import { n as assertOkOrThrowHttpError } from "./provider-http-errors-U-nhuk_f.js";
import { n as estimateBase64DecodedBytes, t as canonicalizeBase64 } from "./base64-Vw7DZYSc.js";
import { t as isProviderApiKeyConfigured } from "./provider-auth-availability-BUrlw84-.js";
import { n as resolveGeneratedMediaMaxBytes } from "./configured-max-bytes-CTSUbFDK.js";
import "./provider-auth-BeZ7NZUU.js";
import { h as resolveProviderOperationTimeoutMs, l as postJsonRequest, r as createProviderOperationDeadline } from "./shared-DCDpYrmy.js";
import "./number-runtime-Cy4drVnh.js";
import "./string-coerce-runtime-GQa0ehRA.js";
import "./media-runtime--XhyE_WP.js";
import "./media-generation-runtime-Dd81xPqM.js";
import "./provider-http-k9RMI7iG.js";
import { d as toImageDataUrl } from "./image-generation-B61kkjvL.js";
import { t as resolveOpenRouterGenerationRequestContext } from "./generation-request-context-Uty27CDI.js";
//#region extensions/openrouter/music-generation-provider.ts
const DEFAULT_OPENROUTER_MUSIC_MODEL = "google/lyria-3-pro-preview";
const OPENROUTER_CLIP_MUSIC_MODEL = "google/lyria-3-clip-preview";
const DEFAULT_TIMEOUT_MS = 18e4;
const OPENROUTER_SSE_ENVELOPE_OVERHEAD_BYTES = 65536;
const OPENROUTER_MUSIC_MODELS = [DEFAULT_OPENROUTER_MUSIC_MODEL, OPENROUTER_CLIP_MUSIC_MODEL];
function resolveOpenRouterMusicModel(model) {
return normalizeOptionalString(model) ?? DEFAULT_OPENROUTER_MUSIC_MODEL;
}
function outputFormatToMimeType(format) {
return format === "mp3" ? "audio/mpeg" : "audio/wav";
}
function imageToContentPart(image) {
const url = normalizeOptionalString(image.url) ?? (image.buffer ? toImageDataUrl({
...image,
buffer: image.buffer,
defaultMimeType: "image/png"
}) : void 0);
if (!url) throw new Error("OpenRouter music generation reference image is missing data.");
return {
type: "image_url",
image_url: { url }
};
}
function buildOpenRouterMusicPrompt(req) {
const parts = [req.prompt.trim()];
const lyrics = normalizeOptionalString(req.lyrics);
if (req.instrumental === true) parts.push("Instrumental only. No vocals, no sung lyrics, no spoken word.");
if (lyrics) parts.push(`Lyrics:\n${lyrics}`);
if (typeof req.durationSeconds === "number") parts.push(`Target duration: about ${Math.round(req.durationSeconds)} seconds.`);
return parts.join("\n\n");
}
function buildOpenRouterMessageContent(req) {
const prompt = buildOpenRouterMusicPrompt(req);
const images = req.inputImages ?? [];
if (images.length === 0) return prompt;
return [{
type: "text",
text: prompt
}, ...images.map((image) => imageToContentPart(image))];
}
function readDeltaAudio(part) {
if (!isRecord(part)) return;
const choices = part.choices;
if (!Array.isArray(choices)) return;
const first = choices[0];
if (!isRecord(first)) return;
const delta = first.delta;
if (!isRecord(delta)) return;
const audio = delta.audio;
if (!isRecord(audio)) return;
return {
data: normalizeOptionalString(audio.data),
transcript: typeof audio.transcript === "string" ? audio.transcript : void 0
};
}
function resolveOpenRouterSseEventMaxBytes(maxBytes) {
return Math.ceil(maxBytes / 3) * 4 + maxBytes + OPENROUTER_SSE_ENVELOPE_OVERHEAD_BYTES;
}
function createOpenRouterMusicTooLargeError(kind, maxBytes) {
return /* @__PURE__ */ new Error(`OpenRouter music generation ${kind} exceeded ${maxBytes} bytes`);
}
function appendDecodedOpenRouterMusicAudio(result, base64) {
if (!base64) return;
const remainingBytes = result.maxBytes - result.audioBytes;
if (estimateBase64DecodedBytes(base64) > remainingBytes) throw createOpenRouterMusicTooLargeError("audio", result.maxBytes);
const canonicalAudio = canonicalizeBase64(base64);
if (!canonicalAudio) throw new Error("OpenRouter music generation returned malformed base64 audio data");
if (Buffer.byteLength(canonicalAudio, "base64") > remainingBytes) throw createOpenRouterMusicTooLargeError("audio", result.maxBytes);
const buffer = Buffer.from(canonicalAudio, "base64");
const nextBytes = result.audioBytes + buffer.byteLength;
if (nextBytes > result.maxBytes) throw createOpenRouterMusicTooLargeError("audio", result.maxBytes);
result.audioBytes = nextBytes;
result.audioBuffers.push(buffer);
}
function appendOpenRouterMusicAudio(result, data) {
const combined = result.audioBase64Remainder + data;
const completeLength = combined.length - combined.length % 4;
result.audioBase64Remainder = combined.slice(completeLength);
appendDecodedOpenRouterMusicAudio(result, combined.slice(0, completeLength));
}
function flushOpenRouterMusicAudio(result) {
appendDecodedOpenRouterMusicAudio(result, result.audioBase64Remainder);
result.audioBase64Remainder = "";
}
function appendOpenRouterMusicTranscript(result, transcript) {
const nextBytes = result.transcriptBytes + Buffer.byteLength(transcript, "utf8");
if (nextBytes > result.maxBytes) throw createOpenRouterMusicTooLargeError("transcript", result.maxBytes);
result.transcriptBytes = nextBytes;
result.transcriptChunks.push(transcript);
}
function readOpenRouterStreamError(part) {
if (!isRecord(part) || !isRecord(part.error)) return;
return normalizeOptionalString(part.error.message) ?? "unknown provider stream error";
}
function processOpenRouterSseLine(line, result) {
if (!line.startsWith("data:")) return false;
const data = line.slice(5).trim();
if (!data) return false;
if (data === "[DONE]") return true;
const payload = JSON.parse(data);
const streamError = readOpenRouterStreamError(payload);
if (streamError) throw new Error(`OpenRouter music generation failed: ${streamError}`);
const audio = readDeltaAudio(payload);
if (audio?.data) appendOpenRouterMusicAudio(result, audio.data);
if (audio?.transcript) appendOpenRouterMusicTranscript(result, audio.transcript);
return false;
}
function resolveOpenRouterStreamRemainingMs(deadline) {
return resolveProviderOperationTimeoutMs({
deadline,
defaultTimeoutMs: deadline.timeoutMs ?? DEFAULT_TIMEOUT_MS
});
}
async function readOpenRouterStreamChunk(reader, deadline) {
const timeoutMs = resolveOpenRouterStreamRemainingMs(deadline);
let timeoutId;
try {
return await Promise.race([reader.read(), new Promise((_, reject) => {
timeoutId = setTimeout(() => {
reject(/* @__PURE__ */ new Error(`${deadline.label} timed out after ${deadline.timeoutMs}ms`));
}, timeoutMs);
})]);
} catch (error) {
await reader.cancel().catch(() => {});
throw error;
} finally {
if (timeoutId) clearTimeout(timeoutId);
}
}
async function readOpenRouterAudioStream(response, deadline, maxBytes) {
if (!response.body) throw new Error("OpenRouter music generation response missing stream body");
const reader = response.body.getReader();
const decoder = new TextDecoder();
const result = {
audioBuffers: [],
audioBytes: 0,
audioBase64Remainder: "",
transcriptChunks: [],
transcriptBytes: 0,
maxBytes
};
const maxEventBytes = resolveOpenRouterSseEventMaxBytes(maxBytes);
let buffer = "";
let pendingBytes = 0;
let doneSeen = false;
try {
for (;;) {
const { value, done } = await readOpenRouterStreamChunk(reader, deadline);
if (done) break;
for (const byte of value) {
pendingBytes = byte === 10 ? 0 : pendingBytes + 1;
if (pendingBytes > maxEventBytes) throw new Error(`OpenRouter music generation SSE event exceeded ${maxEventBytes} bytes for a ${maxBytes}-byte media limit`);
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split(/\r?\n/u);
buffer = lines.pop() ?? "";
for (const line of lines) if (processOpenRouterSseLine(line.trim(), result)) {
flushOpenRouterMusicAudio(result);
await reader.cancel().catch(() => {});
return {
audioBuffer: Buffer.concat(result.audioBuffers, result.audioBytes),
transcript: result.transcriptChunks.join("")
};
}
}
resolveOpenRouterStreamRemainingMs(deadline);
buffer += decoder.decode();
pendingBytes = Buffer.byteLength(buffer, "utf8");
if (pendingBytes > maxEventBytes) throw new Error(`OpenRouter music generation SSE event exceeded ${maxEventBytes} bytes for a ${maxBytes}-byte media limit`);
if (buffer.trim()) {
for (const line of buffer.split(/\r?\n/u)) if (processOpenRouterSseLine(line.trim(), result)) doneSeen = true;
}
if (!doneSeen) throw new Error("OpenRouter music generation stream ended before completion");
flushOpenRouterMusicAudio(result);
return {
audioBuffer: Buffer.concat(result.audioBuffers, result.audioBytes),
transcript: result.transcriptChunks.join("")
};
} catch (error) {
await reader.cancel().catch(() => {});
throw error;
} finally {
try {
reader.releaseLock();
} catch {}
}
}
function buildOpenRouterMusicGenerationProvider() {
return {
id: "openrouter",
label: "OpenRouter",
defaultModel: DEFAULT_OPENROUTER_MUSIC_MODEL,
models: [...OPENROUTER_MUSIC_MODELS],
isConfigured: (ctx) => isProviderApiKeyConfigured({
provider: "openrouter",
...ctx
}),
capabilities: {
generate: {
maxTracks: 1,
maxDurationSeconds: 180,
supportsLyrics: true,
supportsInstrumental: true,
supportsDuration: true,
supportsFormat: true,
supportedFormats: ["mp3", "wav"]
},
edit: {
enabled: true,
maxTracks: 1,
maxInputImages: 1,
maxDurationSeconds: 180,
supportsLyrics: true,
supportsInstrumental: true,
supportsDuration: true,
supportsFormat: true,
supportedFormats: ["mp3", "wav"]
}
},
async generateMusic(req) {
if ((req.inputImages?.length ?? 0) > 1) throw new Error("OpenRouter music generation supports at most one reference image.");
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } = await resolveOpenRouterGenerationRequestContext({
cfg: req.cfg,
agentDir: req.agentDir,
authStore: req.authStore,
capability: "audio",
jsonContentType: true
});
const model = resolveOpenRouterMusicModel(req.model);
const format = req.format ?? "wav";
const requestedTimeoutMs = resolvePositiveTimerTimeoutMs(req.timeoutMs, DEFAULT_TIMEOUT_MS);
const streamDeadline = createProviderOperationDeadline({
timeoutMs: requestedTimeoutMs,
label: "OpenRouter music generation"
});
const timeoutMs = resolveOpenRouterStreamRemainingMs(streamDeadline);
const { response, release } = await postJsonRequest({
url: `${baseUrl}/chat/completions`,
headers,
body: {
model,
messages: [{
role: "user",
content: buildOpenRouterMessageContent(req)
}],
modalities: ["text", "audio"],
audio: { format },
stream: true
},
timeoutMs,
fetchFn: fetch,
allowPrivateNetwork,
dispatcherPolicy
});
try {
await assertOkOrThrowHttpError(response, "OpenRouter music generation failed");
const streamResult = await readOpenRouterAudioStream(response, streamDeadline, resolveGeneratedMediaMaxBytes(req.cfg, "audio"));
if (streamResult.audioBuffer.byteLength === 0) throw new Error("OpenRouter music generation response missing audio data");
return {
tracks: [{
buffer: streamResult.audioBuffer,
mimeType: outputFormatToMimeType(format),
fileName: `track-1.${format}`
}],
model,
...streamResult.transcript ? { lyrics: [streamResult.transcript] } : {},
metadata: {
inputImageCount: req.inputImages?.length ?? 0,
instrumental: req.instrumental === true,
requestedFormat: format
}
};
} finally {
await release();
}
}
};
}
//#endregion
export { buildOpenRouterMusicGenerationProvider as t };