import { C as CONFIG_DIR, J as logVerbose, R as resolveUserPath } from "./subsystem-Ci61Iebe.js";
import { O as resolveDefaultModelForAgent, T as normalizeProviderId, V as requireApiKey, X as DEFAULT_CONTEXT_TOKENS, b as buildModelAliasIndex, k as resolveModelRefFromString, m as resolveOpenClawAgentDir, x as findNormalizedProviderValue, z as getApiKeyForModel } from "./auth-profiles-CfyCILIe.js";
import { n as discoverModels, t as discoverAuthStorage } from "./pi-model-discovery-C9hP1DnY.js";
import { r as normalizeChannelId } from "./plugins-6aUfx6Yt.js";
import { r as createReceiptCard } from "./schedule-cards-yaJLWvpB.js";
import { h as normalizeMimeType, u as getFileExtension } from "./image-ops-DfgorOBP.js";
import { tmpdir } from "node:os";
import path from "node:path";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
import { completeSimple } from "@mariozechner/pi-ai";
import { EdgeTTS } from "node-edge-tts";

//#region src/channels/impl/line/flex-templates/message.ts
/**
* Wrap a FlexContainer in a FlexMessage
*/
function toFlexMessage(altText, contents) {
	return {
		type: "flex",
		altText,
		contents
	};
}

//#endregion
//#region src/channels/impl/line/markdown-to-line.ts
/**
* Regex patterns for markdown detection
*/
const MARKDOWN_TABLE_REGEX = /^\|(.+)\|[\r\n]+\|[-:\s|]+\|[\r\n]+((?:\|.+\|[\r\n]*)+)/gm;
const MARKDOWN_CODE_BLOCK_REGEX = /```(\w*)\n([\s\S]*?)```/g;
const MARKDOWN_LINK_REGEX = /\[([^\]]+)\]\(([^)]+)\)/g;
/**
* Detect and extract markdown tables from text
*/
function extractMarkdownTables(text) {
	const tables = [];
	let textWithoutTables = text;
	MARKDOWN_TABLE_REGEX.lastIndex = 0;
	let match;
	const matches = [];
	while ((match = MARKDOWN_TABLE_REGEX.exec(text)) !== null) {
		const fullMatch = match[0];
		const headerLine = match[1];
		const bodyLines = match[2];
		const headers = parseTableRow(headerLine);
		const rows = bodyLines.trim().split(/[\r\n]+/).filter((line) => line.trim()).map(parseTableRow);
		if (headers.length > 0 && rows.length > 0) matches.push({
			fullMatch,
			table: {
				headers,
				rows
			}
		});
	}
	for (let i = matches.length - 1; i >= 0; i--) {
		const { fullMatch, table } = matches[i];
		tables.unshift(table);
		textWithoutTables = textWithoutTables.replace(fullMatch, "");
	}
	return {
		tables,
		textWithoutTables
	};
}
/**
* Parse a single table row (pipe-separated values)
*/
function parseTableRow(row) {
	return row.split("|").map((cell) => cell.trim()).filter((cell, index, arr) => {
		if (index === 0 && cell === "") return false;
		if (index === arr.length - 1 && cell === "") return false;
		return true;
	});
}
/**
* Convert a markdown table to a LINE Flex Message bubble
*/
function convertTableToFlexBubble(table) {
	const parseCell = (value) => {
		const raw = value?.trim() ?? "";
		if (!raw) return {
			text: "-",
			bold: false,
			hasMarkup: false
		};
		let hasMarkup = false;
		return {
			text: raw.replace(/\*\*(.+?)\*\*/g, (_, inner) => {
				hasMarkup = true;
				return String(inner);
			}).trim() || "-",
			bold: /^\*\*.+\*\*$/.test(raw),
			hasMarkup
		};
	};
	const headerCells = table.headers.map((header) => parseCell(header));
	const rowCells = table.rows.map((row) => row.map((cell) => parseCell(cell)));
	const hasInlineMarkup = headerCells.some((cell) => cell.hasMarkup) || rowCells.some((row) => row.some((cell) => cell.hasMarkup));
	if (table.headers.length === 2 && !hasInlineMarkup) {
		const items = rowCells.map((row) => ({
			name: row[0]?.text ?? "-",
			value: row[1]?.text ?? "-"
		}));
		return createReceiptCard({
			title: headerCells.map((cell) => cell.text).join(" / "),
			items
		});
	}
	return {
		type: "bubble",
		body: {
			type: "box",
			layout: "vertical",
			contents: [
				{
					type: "box",
					layout: "horizontal",
					contents: headerCells.map((cell) => ({
						type: "text",
						text: cell.text,
						weight: "bold",
						size: "sm",
						color: "#333333",
						flex: 1,
						wrap: true
					})),
					paddingBottom: "sm"
				},
				{
					type: "separator",
					margin: "sm"
				},
				...rowCells.slice(0, 10).map((row, rowIndex) => {
					return {
						type: "box",
						layout: "horizontal",
						contents: table.headers.map((_, colIndex) => {
							const cell = row[colIndex] ?? {
								text: "-",
								bold: false,
								hasMarkup: false
							};
							return {
								type: "text",
								text: cell.text,
								size: "sm",
								color: "#666666",
								flex: 1,
								wrap: true,
								weight: cell.bold ? "bold" : void 0
							};
						}),
						margin: rowIndex === 0 ? "md" : "sm"
					};
				})
			],
			paddingAll: "lg"
		}
	};
}
/**
* Detect and extract code blocks from text
*/
function extractCodeBlocks(text) {
	const codeBlocks = [];
	let textWithoutCode = text;
	MARKDOWN_CODE_BLOCK_REGEX.lastIndex = 0;
	let match;
	const matches = [];
	while ((match = MARKDOWN_CODE_BLOCK_REGEX.exec(text)) !== null) {
		const fullMatch = match[0];
		const language = match[1] || void 0;
		const code = match[2];
		matches.push({
			fullMatch,
			block: {
				language,
				code: code.trim()
			}
		});
	}
	for (let i = matches.length - 1; i >= 0; i--) {
		const { fullMatch, block } = matches[i];
		codeBlocks.unshift(block);
		textWithoutCode = textWithoutCode.replace(fullMatch, "");
	}
	return {
		codeBlocks,
		textWithoutCode
	};
}
/**
* Convert a code block to a LINE Flex Message bubble
*/
function convertCodeBlockToFlexBubble(block) {
	const titleText = block.language ? `Code (${block.language})` : "Code";
	const displayCode = block.code.length > 2e3 ? block.code.slice(0, 2e3) + "\n..." : block.code;
	return {
		type: "bubble",
		body: {
			type: "box",
			layout: "vertical",
			contents: [{
				type: "text",
				text: titleText,
				weight: "bold",
				size: "sm",
				color: "#666666"
			}, {
				type: "box",
				layout: "vertical",
				contents: [{
					type: "text",
					text: displayCode,
					size: "xs",
					color: "#333333",
					wrap: true
				}],
				backgroundColor: "#F5F5F5",
				paddingAll: "md",
				cornerRadius: "md",
				margin: "sm"
			}],
			paddingAll: "lg"
		}
	};
}
/**
* Extract markdown links from text
*/
function extractLinks(text) {
	const links = [];
	MARKDOWN_LINK_REGEX.lastIndex = 0;
	let match;
	while ((match = MARKDOWN_LINK_REGEX.exec(text)) !== null) links.push({
		text: match[1],
		url: match[2]
	});
	return {
		links,
		textWithLinks: text.replace(MARKDOWN_LINK_REGEX, "$1")
	};
}
/**
* Strip markdown formatting from text (for plain text output)
* Handles: bold, italic, strikethrough, headers, blockquotes, horizontal rules
*/
function stripMarkdown(text) {
	let result = text;
	result = result.replace(/\*\*(.+?)\*\*/g, "$1");
	result = result.replace(/__(.+?)__/g, "$1");
	result = result.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, "$1");
	result = result.replace(/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, "$1");
	result = result.replace(/~~(.+?)~~/g, "$1");
	result = result.replace(/^#{1,6}\s+(.+)$/gm, "$1");
	result = result.replace(/^>\s?(.*)$/gm, "$1");
	result = result.replace(/^[-*_]{3,}$/gm, "");
	result = result.replace(/`([^`]+)`/g, "$1");
	result = result.replace(/\n{3,}/g, "\n\n");
	result = result.trim();
	return result;
}
/**
* Main function: Process text for LINE output
* - Extracts tables → Flex Messages
* - Extracts code blocks → Flex Messages
* - Strips remaining markdown
* - Returns processed text + Flex Messages
*/
function processLineMessage(text) {
	const flexMessages = [];
	let processedText = text;
	const { tables, textWithoutTables } = extractMarkdownTables(processedText);
	processedText = textWithoutTables;
	for (const table of tables) {
		const bubble = convertTableToFlexBubble(table);
		flexMessages.push(toFlexMessage("Table", bubble));
	}
	const { codeBlocks, textWithoutCode } = extractCodeBlocks(processedText);
	processedText = textWithoutCode;
	for (const block of codeBlocks) {
		const bubble = convertCodeBlockToFlexBubble(block);
		flexMessages.push(toFlexMessage("Code", bubble));
	}
	const { textWithLinks } = extractLinks(processedText);
	processedText = textWithLinks;
	processedText = stripMarkdown(processedText);
	return {
		text: processedText,
		flexMessages
	};
}

//#endregion
//#region src/media/audio.ts
const TELEGRAM_VOICE_AUDIO_EXTENSIONS = new Set([
	".oga",
	".ogg",
	".opus",
	".mp3",
	".m4a"
]);
/**
* MIME types compatible with voice messages.
* Telegram sendVoice supports OGG/Opus, MP3, and M4A.
* https://core.telegram.org/bots/api#sendvoice
*/
const TELEGRAM_VOICE_MIME_TYPES = new Set([
	"audio/ogg",
	"audio/opus",
	"audio/mpeg",
	"audio/mp3",
	"audio/mp4",
	"audio/x-m4a",
	"audio/m4a"
]);
function isTelegramVoiceCompatibleAudio(opts) {
	const mime = normalizeMimeType(opts.contentType);
	if (mime && TELEGRAM_VOICE_MIME_TYPES.has(mime)) return true;
	const fileName = opts.fileName?.trim();
	if (!fileName) return false;
	const ext = getFileExtension(fileName);
	if (!ext) return false;
	return TELEGRAM_VOICE_AUDIO_EXTENSIONS.has(ext);
}
/**
* Backward-compatible alias used across plugin/runtime call sites.
* Keeps existing behavior while making Telegram-specific policy explicit.
*/
function isVoiceCompatibleAudio(opts) {
	return isTelegramVoiceCompatibleAudio(opts);
}

//#endregion
//#region src/agents/models/model-alias-lines.ts
function buildModelAliasLines(cfg) {
	const models = cfg?.agents?.defaults?.models ?? {};
	const entries = [];
	for (const [keyRaw, entryRaw] of Object.entries(models)) {
		const model = String(keyRaw ?? "").trim();
		if (!model) continue;
		const alias = String(entryRaw?.alias ?? "").trim();
		if (!alias) continue;
		entries.push({
			alias,
			model
		});
	}
	return entries.toSorted((a, b) => a.alias.localeCompare(b.alias)).map((entry) => `- ${entry.alias}: ${entry.model}`);
}

//#endregion
//#region src/agents/models/model-compat.ts
function isOpenAiCompletionsModel(model) {
	return model.api === "openai-completions";
}
function normalizeModelCompat(model) {
	const baseUrl = model.baseUrl ?? "";
	if (!(model.provider === "zai" || baseUrl.includes("api.z.ai")) || !isOpenAiCompletionsModel(model)) return model;
	const openaiModel = model;
	const compat = openaiModel.compat ?? void 0;
	if (compat?.supportsDeveloperRole === false) return model;
	openaiModel.compat = compat ? {
		...compat,
		supportsDeveloperRole: false
	} : { supportsDeveloperRole: false };
	return openaiModel;
}

//#endregion
//#region src/agents/models/model-forward-compat.ts
const OPENAI_CODEX_GPT_53_MODEL_ID = "gpt-5.3-codex";
const OPENAI_CODEX_TEMPLATE_MODEL_IDS = ["gpt-5.2-codex"];
const ANTHROPIC_OPUS_46_MODEL_ID = "claude-opus-4-6";
const ANTHROPIC_OPUS_46_DOT_MODEL_ID = "claude-opus-4.6";
const ANTHROPIC_OPUS_TEMPLATE_MODEL_IDS = ["claude-opus-4-5", "claude-opus-4.5"];
const ANTHROPIC_SONNET_46_MODEL_ID = "claude-sonnet-4-6";
const ANTHROPIC_SONNET_46_DOT_MODEL_ID = "claude-sonnet-4.6";
const ANTHROPIC_SONNET_TEMPLATE_MODEL_IDS = ["claude-sonnet-4-5", "claude-sonnet-4.5"];
const ZAI_GLM5_MODEL_ID = "glm-5";
const ZAI_GLM5_TEMPLATE_MODEL_IDS = ["glm-4.7"];
const ANTIGRAVITY_OPUS_46_MODEL_ID = "claude-opus-4-6";
const ANTIGRAVITY_OPUS_46_DOT_MODEL_ID = "claude-opus-4.6";
const ANTIGRAVITY_OPUS_TEMPLATE_MODEL_IDS = ["claude-opus-4-5", "claude-opus-4.5"];
const ANTIGRAVITY_OPUS_46_THINKING_MODEL_ID = "claude-opus-4-6-thinking";
const ANTIGRAVITY_OPUS_46_DOT_THINKING_MODEL_ID = "claude-opus-4.6-thinking";
const ANTIGRAVITY_OPUS_THINKING_TEMPLATE_MODEL_IDS = ["claude-opus-4-5-thinking", "claude-opus-4.5-thinking"];
function cloneFirstTemplateModel(params) {
	const { normalizedProvider, trimmedModelId, templateIds, modelRegistry } = params;
	for (const templateId of [...new Set(templateIds)].filter(Boolean)) {
		const template = modelRegistry.find(normalizedProvider, templateId);
		if (!template) continue;
		return normalizeModelCompat({
			...template,
			id: trimmedModelId,
			name: trimmedModelId,
			...params.patch
		});
	}
}
function resolveOpenAICodexGpt53FallbackModel(provider, modelId, modelRegistry) {
	const normalizedProvider = normalizeProviderId(provider);
	const trimmedModelId = modelId.trim();
	if (normalizedProvider !== "openai-codex") return;
	if (trimmedModelId.toLowerCase() !== OPENAI_CODEX_GPT_53_MODEL_ID) return;
	for (const templateId of OPENAI_CODEX_TEMPLATE_MODEL_IDS) {
		const template = modelRegistry.find(normalizedProvider, templateId);
		if (!template) continue;
		return normalizeModelCompat({
			...template,
			id: trimmedModelId,
			name: trimmedModelId
		});
	}
	return normalizeModelCompat({
		id: trimmedModelId,
		name: trimmedModelId,
		api: "openai-codex-responses",
		provider: normalizedProvider,
		baseUrl: "https://chatgpt.com/backend-api",
		reasoning: true,
		input: ["text", "image"],
		cost: {
			input: 0,
			output: 0,
			cacheRead: 0,
			cacheWrite: 0
		},
		contextWindow: DEFAULT_CONTEXT_TOKENS,
		maxTokens: DEFAULT_CONTEXT_TOKENS
	});
}
function resolveAnthropic46ForwardCompatModel(params) {
	const { provider, modelId, modelRegistry, dashModelId, dotModelId } = params;
	const normalizedProvider = normalizeProviderId(provider);
	if (normalizedProvider !== "anthropic") return;
	const trimmedModelId = modelId.trim();
	const lower = trimmedModelId.toLowerCase();
	if (!(lower === dashModelId || lower === dotModelId || lower.startsWith(`${dashModelId}-`) || lower.startsWith(`${dotModelId}-`))) return;
	const templateIds = [];
	if (lower.startsWith(dashModelId)) templateIds.push(lower.replace(dashModelId, params.dashTemplateId));
	if (lower.startsWith(dotModelId)) templateIds.push(lower.replace(dotModelId, params.dotTemplateId));
	templateIds.push(...params.fallbackTemplateIds);
	return cloneFirstTemplateModel({
		normalizedProvider,
		trimmedModelId,
		templateIds,
		modelRegistry
	});
}
function resolveAnthropicOpus46ForwardCompatModel(provider, modelId, modelRegistry) {
	return resolveAnthropic46ForwardCompatModel({
		provider,
		modelId,
		modelRegistry,
		dashModelId: ANTHROPIC_OPUS_46_MODEL_ID,
		dotModelId: ANTHROPIC_OPUS_46_DOT_MODEL_ID,
		dashTemplateId: "claude-opus-4-5",
		dotTemplateId: "claude-opus-4.5",
		fallbackTemplateIds: ANTHROPIC_OPUS_TEMPLATE_MODEL_IDS
	});
}
function resolveAnthropicSonnet46ForwardCompatModel(provider, modelId, modelRegistry) {
	return resolveAnthropic46ForwardCompatModel({
		provider,
		modelId,
		modelRegistry,
		dashModelId: ANTHROPIC_SONNET_46_MODEL_ID,
		dotModelId: ANTHROPIC_SONNET_46_DOT_MODEL_ID,
		dashTemplateId: "claude-sonnet-4-5",
		dotTemplateId: "claude-sonnet-4.5",
		fallbackTemplateIds: ANTHROPIC_SONNET_TEMPLATE_MODEL_IDS
	});
}
function resolveZaiGlm5ForwardCompatModel(provider, modelId, modelRegistry) {
	if (normalizeProviderId(provider) !== "zai") return;
	const trimmed = modelId.trim();
	const lower = trimmed.toLowerCase();
	if (lower !== ZAI_GLM5_MODEL_ID && !lower.startsWith(`${ZAI_GLM5_MODEL_ID}-`)) return;
	for (const templateId of ZAI_GLM5_TEMPLATE_MODEL_IDS) {
		const template = modelRegistry.find("zai", templateId);
		if (!template) continue;
		return normalizeModelCompat({
			...template,
			id: trimmed,
			name: trimmed,
			reasoning: true
		});
	}
	return normalizeModelCompat({
		id: trimmed,
		name: trimmed,
		api: "openai-completions",
		provider: "zai",
		reasoning: true,
		input: ["text"],
		cost: {
			input: 0,
			output: 0,
			cacheRead: 0,
			cacheWrite: 0
		},
		contextWindow: DEFAULT_CONTEXT_TOKENS,
		maxTokens: DEFAULT_CONTEXT_TOKENS
	});
}
function resolveAntigravityOpus46ForwardCompatModel(provider, modelId, modelRegistry) {
	const normalizedProvider = normalizeProviderId(provider);
	if (normalizedProvider !== "google-antigravity") return;
	const trimmedModelId = modelId.trim();
	const lower = trimmedModelId.toLowerCase();
	const isOpus46 = lower === ANTIGRAVITY_OPUS_46_MODEL_ID || lower === ANTIGRAVITY_OPUS_46_DOT_MODEL_ID || lower.startsWith(`${ANTIGRAVITY_OPUS_46_MODEL_ID}-`) || lower.startsWith(`${ANTIGRAVITY_OPUS_46_DOT_MODEL_ID}-`);
	const isOpus46Thinking = lower === ANTIGRAVITY_OPUS_46_THINKING_MODEL_ID || lower === ANTIGRAVITY_OPUS_46_DOT_THINKING_MODEL_ID || lower.startsWith(`${ANTIGRAVITY_OPUS_46_THINKING_MODEL_ID}-`) || lower.startsWith(`${ANTIGRAVITY_OPUS_46_DOT_THINKING_MODEL_ID}-`);
	if (!isOpus46 && !isOpus46Thinking) return;
	const templateIds = [];
	if (lower.startsWith(ANTIGRAVITY_OPUS_46_MODEL_ID)) templateIds.push(lower.replace(ANTIGRAVITY_OPUS_46_MODEL_ID, "claude-opus-4-5"));
	if (lower.startsWith(ANTIGRAVITY_OPUS_46_DOT_MODEL_ID)) templateIds.push(lower.replace(ANTIGRAVITY_OPUS_46_DOT_MODEL_ID, "claude-opus-4.5"));
	if (lower.startsWith(ANTIGRAVITY_OPUS_46_THINKING_MODEL_ID)) templateIds.push(lower.replace(ANTIGRAVITY_OPUS_46_THINKING_MODEL_ID, "claude-opus-4-5-thinking"));
	if (lower.startsWith(ANTIGRAVITY_OPUS_46_DOT_THINKING_MODEL_ID)) templateIds.push(lower.replace(ANTIGRAVITY_OPUS_46_DOT_THINKING_MODEL_ID, "claude-opus-4.5-thinking"));
	templateIds.push(...ANTIGRAVITY_OPUS_TEMPLATE_MODEL_IDS);
	templateIds.push(...ANTIGRAVITY_OPUS_THINKING_TEMPLATE_MODEL_IDS);
	return cloneFirstTemplateModel({
		normalizedProvider,
		trimmedModelId,
		templateIds,
		modelRegistry
	});
}
function resolveForwardCompatModel(provider, modelId, modelRegistry) {
	return resolveOpenAICodexGpt53FallbackModel(provider, modelId, modelRegistry) ?? resolveAnthropicOpus46ForwardCompatModel(provider, modelId, modelRegistry) ?? resolveAnthropicSonnet46ForwardCompatModel(provider, modelId, modelRegistry) ?? resolveZaiGlm5ForwardCompatModel(provider, modelId, modelRegistry) ?? resolveAntigravityOpus46ForwardCompatModel(provider, modelId, modelRegistry);
}

//#endregion
//#region src/agents/pi-embedded-runner/model.ts
function buildInlineProviderModels(providers) {
	return Object.entries(providers).flatMap(([providerId, entry]) => {
		const trimmed = providerId.trim();
		if (!trimmed) return [];
		return (entry?.models ?? []).map((model) => ({
			...model,
			provider: trimmed,
			baseUrl: entry?.baseUrl,
			api: model.api ?? entry?.api
		}));
	});
}
function resolveModel(provider, modelId, agentDir, cfg) {
	const resolvedAgentDir = agentDir ?? resolveOpenClawAgentDir();
	const authStorage = discoverAuthStorage(resolvedAgentDir);
	const modelRegistry = discoverModels(authStorage, resolvedAgentDir);
	const normalizedProvider = normalizeProviderId(provider);
	const model = modelRegistry.find(normalizedProvider, modelId);
	if (!model) {
		const providers = cfg?.models?.providers ?? {};
		const inlineMatch = buildInlineProviderModels(providers).find((entry) => normalizeProviderId(entry.provider) === normalizedProvider && entry.id === modelId);
		if (inlineMatch) return {
			model: normalizeModelCompat(inlineMatch),
			authStorage,
			modelRegistry
		};
		const forwardCompat = resolveForwardCompatModel(normalizedProvider, modelId, modelRegistry);
		if (forwardCompat) return {
			model: forwardCompat,
			authStorage,
			modelRegistry
		};
		const providerCfg = findNormalizedProviderValue(providers, provider);
		if (providerCfg || modelId.startsWith("mock-")) return {
			model: normalizeModelCompat({
				id: modelId,
				name: modelId,
				api: providerCfg?.api ?? "openai-responses",
				provider: normalizedProvider,
				baseUrl: providerCfg?.baseUrl,
				reasoning: false,
				input: ["text"],
				cost: {
					input: 0,
					output: 0,
					cacheRead: 0,
					cacheWrite: 0
				},
				contextWindow: providerCfg?.models?.[0]?.contextWindow ?? DEFAULT_CONTEXT_TOKENS,
				maxTokens: providerCfg?.models?.[0]?.maxTokens ?? DEFAULT_CONTEXT_TOKENS
			}),
			authStorage,
			modelRegistry
		};
		return {
			error: buildUnknownModelError(provider, modelId),
			authStorage,
			modelRegistry
		};
	}
	return {
		model: normalizeModelCompat(model),
		authStorage,
		modelRegistry
	};
}
/**
* Build a more helpful error when the model is not found.
*
* Local providers (ollama, vllm) need a dummy API key to be registered.
* Users often configure `agents.defaults.model.primary: "ollama/…"` but
* forget to set `OLLAMA_API_KEY`, resulting in a confusing "Unknown model"
* error.  This detects known providers that require opt-in auth and adds
* a hint.
*
* See: https://github.com/openclaw/openclaw/issues/17328
*/
const LOCAL_PROVIDER_HINTS = {
	ollama: "Ollama requires authentication to be registered as a provider. Set OLLAMA_API_KEY=\"ollama-local\" (any value works) or run \"openclaw configure\". See: https://docs.openclaw.ai/providers/ollama",
	vllm: "vLLM requires authentication to be registered as a provider. Set VLLM_API_KEY (any value works) or run \"openclaw configure\". See: https://docs.openclaw.ai/providers/vllm"
};
function buildUnknownModelError(provider, modelId) {
	const base = `Unknown model: ${provider}/${modelId}`;
	const hint = LOCAL_PROVIDER_HINTS[provider.toLowerCase()];
	return hint ? `${base}. ${hint}` : base;
}

//#endregion
//#region src/tts/tts-core.ts
const DEFAULT_ELEVENLABS_BASE_URL$1 = "https://api.elevenlabs.io";
const TEMP_FILE_CLEANUP_DELAY_MS = 300 * 1e3;
function isValidVoiceId(voiceId) {
	return /^[a-zA-Z0-9]{10,40}$/.test(voiceId);
}
function normalizeElevenLabsBaseUrl(baseUrl) {
	const trimmed = baseUrl.trim();
	if (!trimmed) return DEFAULT_ELEVENLABS_BASE_URL$1;
	return trimmed.replace(/\/+$/, "");
}
function requireInRange(value, min, max, label) {
	if (!Number.isFinite(value) || value < min || value > max) throw new Error(`${label} must be between ${min} and ${max}`);
}
function assertElevenLabsVoiceSettings(settings) {
	requireInRange(settings.stability, 0, 1, "stability");
	requireInRange(settings.similarityBoost, 0, 1, "similarityBoost");
	requireInRange(settings.style, 0, 1, "style");
	requireInRange(settings.speed, .5, 2, "speed");
}
function normalizeLanguageCode(code) {
	const trimmed = code?.trim();
	if (!trimmed) return;
	const normalized = trimmed.toLowerCase();
	if (!/^[a-z]{2}$/.test(normalized)) throw new Error("languageCode must be a 2-letter ISO 639-1 code (e.g. en, de, fr)");
	return normalized;
}
function normalizeApplyTextNormalization(mode) {
	const trimmed = mode?.trim();
	if (!trimmed) return;
	const normalized = trimmed.toLowerCase();
	if (normalized === "auto" || normalized === "on" || normalized === "off") return normalized;
	throw new Error("applyTextNormalization must be one of: auto, on, off");
}
function normalizeSeed(seed) {
	if (seed == null) return;
	const next = Math.floor(seed);
	if (!Number.isFinite(next) || next < 0 || next > 4294967295) throw new Error("seed must be between 0 and 4294967295");
	return next;
}
function parseBooleanValue(value) {
	const normalized = value.trim().toLowerCase();
	if ([
		"true",
		"1",
		"yes",
		"on"
	].includes(normalized)) return true;
	if ([
		"false",
		"0",
		"no",
		"off"
	].includes(normalized)) return false;
}
function parseNumberValue(value) {
	const parsed = Number.parseFloat(value);
	return Number.isFinite(parsed) ? parsed : void 0;
}
function parseTtsDirectives(text, policy) {
	if (!policy.enabled) return {
		cleanedText: text,
		overrides: {},
		warnings: [],
		hasDirective: false
	};
	const overrides = {};
	const warnings = [];
	let cleanedText = text;
	let hasDirective = false;
	cleanedText = cleanedText.replace(/\[\[tts\]\]([\s\S]*?)\[\[\/tts\]\]/gi, (_match, inner) => {
		hasDirective = true;
		if (policy.allowText && overrides.ttsText == null) overrides.ttsText = inner.trim();
		return "";
	});
	cleanedText = cleanedText.replace(/\[\[tts:text\]\]([\s\S]*?)\[\[\/tts:text\]\]/gi, (_match, inner) => {
		hasDirective = true;
		if (policy.allowText && overrides.ttsText == null) overrides.ttsText = inner.trim();
		return "";
	});
	cleanedText = cleanedText.replace(/\[\[tts\]\]/gi, () => {
		hasDirective = true;
		return "";
	});
	cleanedText = cleanedText.replace(/\[\[\/tts(?::text)?\]\]/gi, () => {
		hasDirective = true;
		return "";
	});
	cleanedText = cleanedText.replace(/\[\[tts:([^\]]+)\]\]/gi, (_match, body) => {
		hasDirective = true;
		const tokens = body.split(/\s+/).filter(Boolean);
		for (const token of tokens) {
			const eqIndex = token.indexOf("=");
			if (eqIndex === -1) continue;
			const rawKey = token.slice(0, eqIndex).trim();
			const rawValue = token.slice(eqIndex + 1).trim();
			if (!rawKey || !rawValue) continue;
			const key = rawKey.toLowerCase();
			try {
				switch (key) {
					case "provider":
						if (!policy.allowProvider) break;
						if (rawValue === "openai" || rawValue === "elevenlabs" || rawValue === "edge" || rawValue === "mistral") overrides.provider = rawValue;
						else warnings.push(`unsupported provider "${rawValue}"`);
						break;
					case "voice":
					case "openai_voice":
					case "openaivoice":
						if (!policy.allowVoice) break;
						if (isValidOpenAIVoice(rawValue)) overrides.openai = {
							...overrides.openai,
							voice: rawValue
						};
						else warnings.push(`invalid OpenAI voice "${rawValue}"`);
						break;
					case "voiceid":
					case "voice_id":
					case "elevenlabs_voice":
					case "elevenlabsvoice":
						if (!policy.allowVoice) break;
						if (isValidVoiceId(rawValue)) overrides.elevenlabs = {
							...overrides.elevenlabs,
							voiceId: rawValue
						};
						else warnings.push(`invalid ElevenLabs voiceId "${rawValue}"`);
						break;
					case "model":
					case "modelid":
					case "model_id":
					case "elevenlabs_model":
					case "elevenlabsmodel":
					case "openai_model":
					case "openaimodel":
						if (!policy.allowModelId) break;
						if (isValidOpenAIModel(rawValue)) overrides.openai = {
							...overrides.openai,
							model: rawValue
						};
						else overrides.elevenlabs = {
							...overrides.elevenlabs,
							modelId: rawValue
						};
						break;
					case "stability":
						if (!policy.allowVoiceSettings) break;
						{
							const value = parseNumberValue(rawValue);
							if (value == null) {
								warnings.push("invalid stability value");
								break;
							}
							requireInRange(value, 0, 1, "stability");
							overrides.elevenlabs = {
								...overrides.elevenlabs,
								voiceSettings: {
									...overrides.elevenlabs?.voiceSettings,
									stability: value
								}
							};
						}
						break;
					case "similarity":
					case "similarityboost":
					case "similarity_boost":
						if (!policy.allowVoiceSettings) break;
						{
							const value = parseNumberValue(rawValue);
							if (value == null) {
								warnings.push("invalid similarityBoost value");
								break;
							}
							requireInRange(value, 0, 1, "similarityBoost");
							overrides.elevenlabs = {
								...overrides.elevenlabs,
								voiceSettings: {
									...overrides.elevenlabs?.voiceSettings,
									similarityBoost: value
								}
							};
						}
						break;
					case "style":
						if (!policy.allowVoiceSettings) break;
						{
							const value = parseNumberValue(rawValue);
							if (value == null) {
								warnings.push("invalid style value");
								break;
							}
							requireInRange(value, 0, 1, "style");
							overrides.elevenlabs = {
								...overrides.elevenlabs,
								voiceSettings: {
									...overrides.elevenlabs?.voiceSettings,
									style: value
								}
							};
						}
						break;
					case "speed":
						if (!policy.allowVoiceSettings) break;
						{
							const value = parseNumberValue(rawValue);
							if (value == null) {
								warnings.push("invalid speed value");
								break;
							}
							requireInRange(value, .5, 2, "speed");
							overrides.elevenlabs = {
								...overrides.elevenlabs,
								voiceSettings: {
									...overrides.elevenlabs?.voiceSettings,
									speed: value
								}
							};
						}
						break;
					case "speakerboost":
					case "speaker_boost":
					case "usespeakerboost":
					case "use_speaker_boost":
						if (!policy.allowVoiceSettings) break;
						{
							const value = parseBooleanValue(rawValue);
							if (value == null) {
								warnings.push("invalid useSpeakerBoost value");
								break;
							}
							overrides.elevenlabs = {
								...overrides.elevenlabs,
								voiceSettings: {
									...overrides.elevenlabs?.voiceSettings,
									useSpeakerBoost: value
								}
							};
						}
						break;
					case "normalize":
					case "applytextnormalization":
					case "apply_text_normalization":
						if (!policy.allowNormalization) break;
						overrides.elevenlabs = {
							...overrides.elevenlabs,
							applyTextNormalization: normalizeApplyTextNormalization(rawValue)
						};
						break;
					case "language":
					case "languagecode":
					case "language_code":
						if (!policy.allowNormalization) break;
						overrides.elevenlabs = {
							...overrides.elevenlabs,
							languageCode: normalizeLanguageCode(rawValue)
						};
						break;
					case "seed":
						if (!policy.allowSeed) break;
						overrides.elevenlabs = {
							...overrides.elevenlabs,
							seed: normalizeSeed(Number.parseInt(rawValue, 10))
						};
						break;
					default: break;
				}
			} catch (err) {
				warnings.push(err.message);
			}
		}
		return "";
	});
	return {
		cleanedText,
		ttsText: overrides.ttsText,
		hasDirective,
		overrides,
		warnings
	};
}
const OPENAI_TTS_MODELS = [
	"gpt-4o-mini-tts",
	"tts-1",
	"tts-1-hd"
];
/**
* Custom OpenAI-compatible TTS endpoint.
* When set, model/voice validation is relaxed to allow non-OpenAI models.
* Example: OPENAI_TTS_BASE_URL=http://localhost:8880/v1
*
* Note: Read at runtime (not module load) to support config.env loading.
*/
function getOpenAITtsBaseUrl() {
	return (process.env.OPENAI_TTS_BASE_URL?.trim() || "https://api.openai.com/v1").replace(/\/+$/, "");
}
function isCustomOpenAIEndpoint() {
	return getOpenAITtsBaseUrl() !== "https://api.openai.com/v1";
}
const OPENAI_TTS_VOICES = [
	"alloy",
	"ash",
	"ballad",
	"cedar",
	"coral",
	"echo",
	"fable",
	"juniper",
	"marin",
	"onyx",
	"nova",
	"sage",
	"shimmer",
	"verse"
];
function isValidOpenAIModel(model) {
	if (isCustomOpenAIEndpoint()) return true;
	return OPENAI_TTS_MODELS.includes(model);
}
function isValidOpenAIVoice(voice) {
	if (isCustomOpenAIEndpoint()) return true;
	return OPENAI_TTS_VOICES.includes(voice);
}
function resolveSummaryModelRef(cfg, config) {
	const defaultRef = resolveDefaultModelForAgent({ cfg });
	const override = config.summaryModel?.trim();
	if (!override) return {
		ref: defaultRef,
		source: "default"
	};
	const aliasIndex = buildModelAliasIndex({
		cfg,
		defaultProvider: defaultRef.provider
	});
	const resolved = resolveModelRefFromString({
		raw: override,
		defaultProvider: defaultRef.provider,
		aliasIndex
	});
	if (!resolved) return {
		ref: defaultRef,
		source: "default"
	};
	return {
		ref: resolved.ref,
		source: "summaryModel"
	};
}
function isTextContentBlock(block) {
	return block.type === "text";
}
async function summarizeText(params) {
	const { text, targetLength, cfg, config, timeoutMs } = params;
	if (targetLength < 100 || targetLength > 1e4) throw new Error(`Invalid targetLength: ${targetLength}`);
	const startTime = Date.now();
	const { ref } = resolveSummaryModelRef(cfg, config);
	const resolved = resolveModel(ref.provider, ref.model, void 0, cfg);
	if (!resolved.model) throw new Error(resolved.error ?? `Unknown summary model: ${ref.provider}/${ref.model}`);
	const apiKey = requireApiKey(await getApiKeyForModel({
		model: resolved.model,
		cfg
	}), ref.provider);
	try {
		const controller = new AbortController();
		const timeout = setTimeout(() => controller.abort(), timeoutMs);
		try {
			const summary = (await completeSimple(resolved.model, { messages: [{
				role: "user",
				content: `You are an assistant that summarizes texts concisely while keeping the most important information. Summarize the text to approximately ${targetLength} characters. Maintain the original tone and style. Reply only with the summary, without additional explanations.\n\n<text_to_summarize>\n${text}\n</text_to_summarize>`,
				timestamp: Date.now()
			}] }, {
				apiKey,
				maxTokens: Math.ceil(targetLength / 2),
				temperature: .3,
				signal: controller.signal
			})).content.filter(isTextContentBlock).map((block) => block.text.trim()).filter(Boolean).join(" ").trim();
			if (!summary) throw new Error("No summary returned");
			return {
				summary,
				latencyMs: Date.now() - startTime,
				inputLength: text.length,
				outputLength: summary.length
			};
		} finally {
			clearTimeout(timeout);
		}
	} catch (err) {
		if (err.name === "AbortError") throw new Error("Summarization timed out", { cause: err });
		throw err;
	}
}
function scheduleCleanup(tempDir, delayMs = TEMP_FILE_CLEANUP_DELAY_MS) {
	setTimeout(() => {
		try {
			rmSync(tempDir, {
				recursive: true,
				force: true
			});
		} catch {}
	}, delayMs).unref();
}
async function elevenLabsTTS(params) {
	const { text, apiKey, baseUrl, voiceId, modelId, outputFormat, seed, applyTextNormalization, languageCode, voiceSettings, timeoutMs } = params;
	if (!isValidVoiceId(voiceId)) throw new Error("Invalid voiceId format");
	assertElevenLabsVoiceSettings(voiceSettings);
	const normalizedLanguage = normalizeLanguageCode(languageCode);
	const normalizedNormalization = normalizeApplyTextNormalization(applyTextNormalization);
	const normalizedSeed = normalizeSeed(seed);
	const controller = new AbortController();
	const timeout = setTimeout(() => controller.abort(), timeoutMs);
	try {
		const url = new URL(`${normalizeElevenLabsBaseUrl(baseUrl)}/v1/text-to-speech/${voiceId}`);
		if (outputFormat) url.searchParams.set("output_format", outputFormat);
		const response = await fetch(url.toString(), {
			method: "POST",
			headers: {
				"xi-api-key": apiKey,
				"Content-Type": "application/json",
				Accept: "audio/mpeg"
			},
			body: JSON.stringify({
				text,
				model_id: modelId,
				seed: normalizedSeed,
				apply_text_normalization: normalizedNormalization,
				language_code: normalizedLanguage,
				voice_settings: {
					stability: voiceSettings.stability,
					similarity_boost: voiceSettings.similarityBoost,
					style: voiceSettings.style,
					use_speaker_boost: voiceSettings.useSpeakerBoost,
					speed: voiceSettings.speed
				}
			}),
			signal: controller.signal
		});
		if (!response.ok) throw new Error(`ElevenLabs API error (${response.status})`);
		return Buffer.from(await response.arrayBuffer());
	} finally {
		clearTimeout(timeout);
	}
}
async function openaiTTS(params) {
	const { text, apiKey, model, voice, responseFormat, timeoutMs } = params;
	if (!isValidOpenAIModel(model)) throw new Error(`Invalid model: ${model}`);
	if (!isValidOpenAIVoice(voice)) throw new Error(`Invalid voice: ${voice}`);
	const controller = new AbortController();
	const timeout = setTimeout(() => controller.abort(), timeoutMs);
	try {
		const response = await fetch(`${getOpenAITtsBaseUrl()}/audio/speech`, {
			method: "POST",
			headers: {
				Authorization: `Bearer ${apiKey}`,
				"Content-Type": "application/json"
			},
			body: JSON.stringify({
				model,
				input: text,
				voice,
				response_format: responseFormat
			}),
			signal: controller.signal
		});
		if (!response.ok) throw new Error(`OpenAI TTS API error (${response.status})`);
		return Buffer.from(await response.arrayBuffer());
	} finally {
		clearTimeout(timeout);
	}
}
const VOXTRAL_MAX_CHARS = 4096;
const VOXTRAL_BASE_URL = "https://api.mistral.ai/v1/audio/speech";
async function voxtralTTS(params) {
	const { text, apiKey, model, voice, speed, outputFormat, timeoutMs } = params;
	if (text.length > VOXTRAL_MAX_CHARS) throw new Error(`Text exceeds Voxtral limit of ${VOXTRAL_MAX_CHARS} characters`);
	const controller = new AbortController();
	const timeout = setTimeout(() => controller.abort(), timeoutMs);
	try {
		const response = await fetch(VOXTRAL_BASE_URL, {
			method: "POST",
			headers: {
				Authorization: `Bearer ${apiKey}`,
				"Content-Type": "application/json"
			},
			body: JSON.stringify({
				model,
				input: text,
				voice,
				speed,
				output_format: outputFormat,
				stream_audio: false
			}),
			signal: controller.signal
		});
		if (!response.ok) throw new Error(`Voxtral TTS API error (${response.status})`);
		return Buffer.from(await response.arrayBuffer());
	} finally {
		clearTimeout(timeout);
	}
}
function inferEdgeExtension(outputFormat) {
	const normalized = outputFormat.toLowerCase();
	if (normalized.includes("webm")) return ".webm";
	if (normalized.includes("ogg")) return ".ogg";
	if (normalized.includes("opus")) return ".opus";
	if (normalized.includes("wav") || normalized.includes("riff") || normalized.includes("pcm")) return ".wav";
	return ".mp3";
}
async function edgeTTS(params) {
	const { text, outputPath, config, timeoutMs } = params;
	await new EdgeTTS({
		voice: config.voice,
		lang: config.lang,
		outputFormat: config.outputFormat,
		saveSubtitles: config.saveSubtitles,
		proxy: config.proxy,
		rate: config.rate,
		pitch: config.pitch,
		volume: config.volume,
		timeout: config.timeoutMs ?? timeoutMs
	}).ttsPromise(text, outputPath);
}

//#endregion
//#region src/tts/tts.ts
const DEFAULT_TIMEOUT_MS = 3e4;
const DEFAULT_TTS_MAX_LENGTH = 1500;
const DEFAULT_TTS_SUMMARIZE = true;
const DEFAULT_MAX_TEXT_LENGTH = 4096;
const DEFAULT_MISTRAL_MODEL = "voxtral-4b-tts";
const DEFAULT_MISTRAL_VOICE = "sophia";
const DEFAULT_MISTRAL_SPEED = 1;
const DEFAULT_ELEVENLABS_BASE_URL = "https://api.elevenlabs.io";
const DEFAULT_ELEVENLABS_VOICE_ID = "pMsXgVXv3BLzUgSXRplE";
const DEFAULT_ELEVENLABS_MODEL_ID = "eleven_multilingual_v2";
const DEFAULT_OPENAI_MODEL = "gpt-4o-mini-tts";
const DEFAULT_OPENAI_VOICE = "alloy";
const DEFAULT_EDGE_VOICE = "en-US-MichelleNeural";
const DEFAULT_EDGE_LANG = "en-US";
const DEFAULT_EDGE_OUTPUT_FORMAT = "audio-24khz-48kbitrate-mono-mp3";
const DEFAULT_ELEVENLABS_VOICE_SETTINGS = {
	stability: .5,
	similarityBoost: .75,
	style: 0,
	useSpeakerBoost: true,
	speed: 1
};
const TELEGRAM_OUTPUT = {
	openai: "opus",
	elevenlabs: "opus_48000_64",
	extension: ".opus",
	voiceCompatible: true
};
const DEFAULT_OUTPUT = {
	openai: "mp3",
	elevenlabs: "mp3_44100_128",
	extension: ".mp3",
	voiceCompatible: false
};
const TELEPHONY_OUTPUT = {
	openai: {
		format: "pcm",
		sampleRate: 24e3
	},
	elevenlabs: {
		format: "pcm_22050",
		sampleRate: 22050
	},
	mistral: {
		format: "pcm",
		sampleRate: 24e3
	}
};
const TTS_AUTO_MODES = new Set([
	"off",
	"always",
	"inbound",
	"tagged",
	"smart"
]);
let lastTtsAttempt;
function normalizeTtsAutoMode(value) {
	if (typeof value !== "string") return;
	const normalized = value.trim().toLowerCase();
	if (TTS_AUTO_MODES.has(normalized)) return normalized;
}
function resolveModelOverridePolicy(overrides) {
	if (!(overrides?.enabled ?? true)) return {
		enabled: false,
		allowText: false,
		allowProvider: false,
		allowVoice: false,
		allowModelId: false,
		allowVoiceSettings: false,
		allowNormalization: false,
		allowSeed: false
	};
	const allow = (value) => value ?? true;
	return {
		enabled: true,
		allowText: allow(overrides?.allowText),
		allowProvider: allow(overrides?.allowProvider),
		allowVoice: allow(overrides?.allowVoice),
		allowModelId: allow(overrides?.allowModelId),
		allowVoiceSettings: allow(overrides?.allowVoiceSettings),
		allowNormalization: allow(overrides?.allowNormalization),
		allowSeed: allow(overrides?.allowSeed)
	};
}
function resolveTtsConfig(cfg) {
	const raw = cfg.messages?.tts ?? {};
	const providerSource = raw.provider ? "config" : "default";
	const edgeOutputFormat = raw.edge?.outputFormat?.trim();
	return {
		auto: normalizeTtsAutoMode(raw.auto) ?? (raw.enabled ? "always" : "off"),
		mode: raw.mode ?? "final",
		provider: raw.provider ?? "edge",
		providerSource,
		summaryModel: raw.summaryModel?.trim() || void 0,
		modelOverrides: resolveModelOverridePolicy(raw.modelOverrides),
		elevenlabs: {
			apiKey: raw.elevenlabs?.apiKey,
			baseUrl: raw.elevenlabs?.baseUrl?.trim() || DEFAULT_ELEVENLABS_BASE_URL,
			voiceId: raw.elevenlabs?.voiceId ?? DEFAULT_ELEVENLABS_VOICE_ID,
			modelId: raw.elevenlabs?.modelId ?? DEFAULT_ELEVENLABS_MODEL_ID,
			seed: raw.elevenlabs?.seed,
			applyTextNormalization: raw.elevenlabs?.applyTextNormalization,
			languageCode: raw.elevenlabs?.languageCode,
			voiceSettings: {
				stability: raw.elevenlabs?.voiceSettings?.stability ?? DEFAULT_ELEVENLABS_VOICE_SETTINGS.stability,
				similarityBoost: raw.elevenlabs?.voiceSettings?.similarityBoost ?? DEFAULT_ELEVENLABS_VOICE_SETTINGS.similarityBoost,
				style: raw.elevenlabs?.voiceSettings?.style ?? DEFAULT_ELEVENLABS_VOICE_SETTINGS.style,
				useSpeakerBoost: raw.elevenlabs?.voiceSettings?.useSpeakerBoost ?? DEFAULT_ELEVENLABS_VOICE_SETTINGS.useSpeakerBoost,
				speed: raw.elevenlabs?.voiceSettings?.speed ?? DEFAULT_ELEVENLABS_VOICE_SETTINGS.speed
			}
		},
		openai: {
			apiKey: raw.openai?.apiKey,
			model: raw.openai?.model ?? DEFAULT_OPENAI_MODEL,
			voice: raw.openai?.voice ?? DEFAULT_OPENAI_VOICE
		},
		edge: {
			enabled: raw.edge?.enabled ?? true,
			voice: raw.edge?.voice?.trim() || DEFAULT_EDGE_VOICE,
			lang: raw.edge?.lang?.trim() || DEFAULT_EDGE_LANG,
			outputFormat: edgeOutputFormat || DEFAULT_EDGE_OUTPUT_FORMAT,
			outputFormatConfigured: Boolean(edgeOutputFormat),
			pitch: raw.edge?.pitch?.trim() || void 0,
			rate: raw.edge?.rate?.trim() || void 0,
			volume: raw.edge?.volume?.trim() || void 0,
			saveSubtitles: raw.edge?.saveSubtitles ?? false,
			proxy: raw.edge?.proxy?.trim() || void 0,
			timeoutMs: raw.edge?.timeoutMs
		},
		mistral: {
			apiKey: raw.mistral?.apiKey,
			model: raw.mistral?.model ?? DEFAULT_MISTRAL_MODEL,
			voice: raw.mistral?.voice ?? DEFAULT_MISTRAL_VOICE,
			speed: raw.mistral?.speed ?? DEFAULT_MISTRAL_SPEED
		},
		prefsPath: raw.prefsPath,
		maxTextLength: raw.maxTextLength ?? DEFAULT_MAX_TEXT_LENGTH,
		timeoutMs: raw.timeoutMs ?? DEFAULT_TIMEOUT_MS
	};
}
function resolveTtsPrefsPath(config) {
	if (config.prefsPath?.trim()) return resolveUserPath(config.prefsPath.trim());
	const envPath = process.env.MINION_TTS_PREFS?.trim();
	if (envPath) return resolveUserPath(envPath);
	return path.join(CONFIG_DIR, "settings", "tts.json");
}
function resolveTtsAutoModeFromPrefs(prefs) {
	const auto = normalizeTtsAutoMode(prefs.tts?.auto);
	if (auto) return auto;
	if (typeof prefs.tts?.enabled === "boolean") return prefs.tts.enabled ? "always" : "off";
}
function resolveTtsAutoMode(params) {
	const sessionAuto = normalizeTtsAutoMode(params.sessionAuto);
	if (sessionAuto) return sessionAuto;
	const prefsAuto = resolveTtsAutoModeFromPrefs(readPrefs(params.prefsPath));
	if (prefsAuto) return prefsAuto;
	return params.config.auto;
}
function buildTtsSystemPromptHint(cfg) {
	const config = resolveTtsConfig(cfg);
	const prefsPath = resolveTtsPrefsPath(config);
	const autoMode = resolveTtsAutoMode({
		config,
		prefsPath
	});
	if (autoMode === "off") return;
	const maxLength = getTtsMaxLength(prefsPath);
	const summarize = isSummarizationEnabled(prefsPath) ? "on" : "off";
	if (autoMode === "smart") return [
		"## Voice (TTS)",
		"",
		"You can respond with voice or text. Choose intelligently based on content.",
		"",
		"**Use voice** (`[[tts]]` or `[[tts]]...[[/tts]]`):",
		"- Greetings, casual chat, personal opinions, empathy",
		"- Short summaries or briefings (< 200 words)",
		"- Storytelling, narration, roleplay",
		"- Responding conversationally to voice notes",
		"- When the user asks for voice/audio",
		"",
		"**Use text** (no tag):",
		"- Lists, tables, structured data, code, JSON",
		"- URLs, commands, file paths, technical output",
		"- Long content (> 200 words), multi-step instructions",
		"- Content the user will copy, reference, or search",
		"- Tool results with raw data, error messages",
		"",
		"**Targeted voice** — wrap only the spoken portion:",
		"```",
		"[[tts]]Spoken summary here.[[/tts]]",
		"",
		"**Detailed data below:**",
		"- Item 1...",
		"```",
		"",
		"Most responses should be text. Voice is for warmth and personality, not data delivery.",
		`Keep spoken text ≤${maxLength} chars.`
	].join("\n");
	return [
		"Voice (TTS) is enabled.",
		autoMode === "inbound" ? "Only use TTS when the user's last message includes audio/voice." : autoMode === "tagged" ? "Only use TTS when you include [[tts]] or [[tts:text]] tags." : void 0,
		`Keep spoken text ≤${maxLength} chars to avoid auto-summary (summary ${summarize}).`,
		"Use [[tts]] to trigger voice, or [[tts:...]] / [[tts:text]]...[[/tts:text]] for voice/expressiveness control."
	].filter(Boolean).join("\n");
}
function readPrefs(prefsPath) {
	try {
		if (!existsSync(prefsPath)) return {};
		return JSON.parse(readFileSync(prefsPath, "utf8"));
	} catch {
		return {};
	}
}
function atomicWriteFileSync(filePath, content) {
	const tmpPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`;
	writeFileSync(tmpPath, content);
	try {
		renameSync(tmpPath, filePath);
	} catch (err) {
		try {
			unlinkSync(tmpPath);
		} catch {}
		throw err;
	}
}
function updatePrefs(prefsPath, update) {
	const prefs = readPrefs(prefsPath);
	update(prefs);
	mkdirSync(path.dirname(prefsPath), { recursive: true });
	atomicWriteFileSync(prefsPath, JSON.stringify(prefs, null, 2));
}
function isTtsEnabled(config, prefsPath, sessionAuto) {
	return resolveTtsAutoMode({
		config,
		prefsPath,
		sessionAuto
	}) !== "off";
}
function setTtsAutoMode(prefsPath, mode) {
	updatePrefs(prefsPath, (prefs) => {
		const next = { ...prefs.tts };
		delete next.enabled;
		next.auto = mode;
		prefs.tts = next;
	});
}
function setTtsEnabled(prefsPath, enabled) {
	setTtsAutoMode(prefsPath, enabled ? "always" : "off");
}
function getTtsProvider(config, prefsPath) {
	const prefs = readPrefs(prefsPath);
	if (prefs.tts?.provider) return prefs.tts.provider;
	if (config.providerSource === "config") return config.provider;
	if (resolveTtsApiKey(config, "openai")) return "openai";
	if (resolveTtsApiKey(config, "elevenlabs")) return "elevenlabs";
	if (resolveTtsApiKey(config, "mistral")) return "mistral";
	return "edge";
}
function setTtsProvider(prefsPath, provider) {
	updatePrefs(prefsPath, (prefs) => {
		prefs.tts = {
			...prefs.tts,
			provider
		};
	});
}
function getTtsMaxLength(prefsPath) {
	return readPrefs(prefsPath).tts?.maxLength ?? DEFAULT_TTS_MAX_LENGTH;
}
function setTtsMaxLength(prefsPath, maxLength) {
	updatePrefs(prefsPath, (prefs) => {
		prefs.tts = {
			...prefs.tts,
			maxLength
		};
	});
}
function isSummarizationEnabled(prefsPath) {
	return readPrefs(prefsPath).tts?.summarize ?? DEFAULT_TTS_SUMMARIZE;
}
function setSummarizationEnabled(prefsPath, enabled) {
	updatePrefs(prefsPath, (prefs) => {
		prefs.tts = {
			...prefs.tts,
			summarize: enabled
		};
	});
}
function getLastTtsAttempt() {
	return lastTtsAttempt;
}
function setLastTtsAttempt(entry) {
	lastTtsAttempt = entry;
}
function resolveOutputFormat(channelId) {
	if (channelId === "telegram") return TELEGRAM_OUTPUT;
	return DEFAULT_OUTPUT;
}
function resolveChannelId(channel) {
	return channel ? normalizeChannelId(channel) : null;
}
function resolveEdgeOutputFormat(config) {
	return config.edge.outputFormat;
}
function resolveTtsApiKey(config, provider) {
	if (provider === "elevenlabs") return config.elevenlabs.apiKey || process.env.ELEVENLABS_API_KEY || process.env.XI_API_KEY;
	if (provider === "openai") return config.openai.apiKey || process.env.OPENAI_API_KEY;
	if (provider === "mistral") return config.mistral.apiKey || process.env.MISTRAL_API_KEY;
}
const TTS_PROVIDERS = [
	"openai",
	"elevenlabs",
	"mistral",
	"edge"
];
function resolveTtsProviderOrder(primary) {
	return [primary, ...TTS_PROVIDERS.filter((provider) => provider !== primary)];
}
function isTtsProviderConfigured(config, provider) {
	if (provider === "edge") return config.edge.enabled;
	return Boolean(resolveTtsApiKey(config, provider));
}
function formatTtsProviderError(provider, err) {
	const error = err instanceof Error ? err : new Error(String(err));
	if (error.name === "AbortError") return `${provider}: request timed out`;
	return `${provider}: ${error.message}`;
}
async function textToSpeech(params) {
	const config = resolveTtsConfig(params.cfg);
	const prefsPath = params.prefsPath ?? resolveTtsPrefsPath(config);
	const output = resolveOutputFormat(resolveChannelId(params.channel));
	if (params.text.length > config.maxTextLength) return {
		success: false,
		error: `Text too long (${params.text.length} chars, max ${config.maxTextLength})`
	};
	const userProvider = getTtsProvider(config, prefsPath);
	const providers = resolveTtsProviderOrder(params.overrides?.provider ?? userProvider);
	const errors = [];
	for (const provider of providers) {
		const providerStart = Date.now();
		try {
			if (provider === "edge") {
				if (!config.edge.enabled) {
					errors.push("edge: disabled");
					continue;
				}
				const tempDir = mkdtempSync(path.join(tmpdir(), "tts-"));
				let edgeOutputFormat = resolveEdgeOutputFormat(config);
				const fallbackEdgeOutputFormat = edgeOutputFormat !== DEFAULT_EDGE_OUTPUT_FORMAT ? DEFAULT_EDGE_OUTPUT_FORMAT : void 0;
				const attemptEdgeTts = async (outputFormat) => {
					const extension = inferEdgeExtension(outputFormat);
					const audioPath = path.join(tempDir, `voice-${Date.now()}${extension}`);
					await edgeTTS({
						text: params.text,
						outputPath: audioPath,
						config: {
							...config.edge,
							outputFormat
						},
						timeoutMs: config.timeoutMs
					});
					return {
						audioPath,
						outputFormat
					};
				};
				let edgeResult;
				try {
					edgeResult = await attemptEdgeTts(edgeOutputFormat);
				} catch (err) {
					if (fallbackEdgeOutputFormat && fallbackEdgeOutputFormat !== edgeOutputFormat) {
						logVerbose(`TTS: Edge output ${edgeOutputFormat} failed; retrying with ${fallbackEdgeOutputFormat}.`);
						edgeOutputFormat = fallbackEdgeOutputFormat;
						try {
							edgeResult = await attemptEdgeTts(edgeOutputFormat);
						} catch (fallbackErr) {
							try {
								rmSync(tempDir, {
									recursive: true,
									force: true
								});
							} catch {}
							throw fallbackErr;
						}
					} else {
						try {
							rmSync(tempDir, {
								recursive: true,
								force: true
							});
						} catch {}
						throw err;
					}
				}
				scheduleCleanup(tempDir);
				const voiceCompatible = isVoiceCompatibleAudio({ fileName: edgeResult.audioPath });
				return {
					success: true,
					audioPath: edgeResult.audioPath,
					latencyMs: Date.now() - providerStart,
					provider,
					outputFormat: edgeResult.outputFormat,
					voiceCompatible
				};
			}
			const apiKey = resolveTtsApiKey(config, provider);
			if (!apiKey) {
				errors.push(`${provider}: no API key`);
				continue;
			}
			let audioBuffer;
			let resolvedOutputFormat;
			if (provider === "elevenlabs") {
				const voiceIdOverride = params.overrides?.elevenlabs?.voiceId;
				const modelIdOverride = params.overrides?.elevenlabs?.modelId;
				const voiceSettings = {
					...config.elevenlabs.voiceSettings,
					...params.overrides?.elevenlabs?.voiceSettings
				};
				const seedOverride = params.overrides?.elevenlabs?.seed;
				const normalizationOverride = params.overrides?.elevenlabs?.applyTextNormalization;
				const languageOverride = params.overrides?.elevenlabs?.languageCode;
				audioBuffer = await elevenLabsTTS({
					text: params.text,
					apiKey,
					baseUrl: config.elevenlabs.baseUrl,
					voiceId: voiceIdOverride ?? config.elevenlabs.voiceId,
					modelId: modelIdOverride ?? config.elevenlabs.modelId,
					outputFormat: output.elevenlabs,
					seed: seedOverride ?? config.elevenlabs.seed,
					applyTextNormalization: normalizationOverride ?? config.elevenlabs.applyTextNormalization,
					languageCode: languageOverride ?? config.elevenlabs.languageCode,
					voiceSettings,
					timeoutMs: config.timeoutMs
				});
				resolvedOutputFormat = output.elevenlabs;
			} else if (provider === "mistral") {
				audioBuffer = await voxtralTTS({
					text: params.text,
					apiKey,
					model: config.mistral.model,
					voice: config.mistral.voice,
					speed: config.mistral.speed,
					outputFormat: output.openai === "opus" ? "opus" : "mp3",
					timeoutMs: config.timeoutMs
				});
				resolvedOutputFormat = output.openai === "opus" ? "opus" : "mp3";
			} else {
				const openaiModelOverride = params.overrides?.openai?.model;
				const openaiVoiceOverride = params.overrides?.openai?.voice;
				audioBuffer = await openaiTTS({
					text: params.text,
					apiKey,
					model: openaiModelOverride ?? config.openai.model,
					voice: openaiVoiceOverride ?? config.openai.voice,
					responseFormat: output.openai,
					timeoutMs: config.timeoutMs
				});
				resolvedOutputFormat = output.openai;
			}
			const latencyMs = Date.now() - providerStart;
			const tempDir = mkdtempSync(path.join(tmpdir(), "tts-"));
			const audioPath = path.join(tempDir, `voice-${Date.now()}${output.extension}`);
			writeFileSync(audioPath, audioBuffer);
			scheduleCleanup(tempDir);
			return {
				success: true,
				audioPath,
				latencyMs,
				provider,
				outputFormat: resolvedOutputFormat,
				voiceCompatible: output.voiceCompatible
			};
		} catch (err) {
			errors.push(formatTtsProviderError(provider, err));
		}
	}
	return {
		success: false,
		error: `TTS conversion failed: ${errors.join("; ") || "no providers available"}`
	};
}
async function textToSpeechTelephony(params) {
	const config = resolveTtsConfig(params.cfg);
	const prefsPath = params.prefsPath ?? resolveTtsPrefsPath(config);
	if (params.text.length > config.maxTextLength) return {
		success: false,
		error: `Text too long (${params.text.length} chars, max ${config.maxTextLength})`
	};
	const providers = resolveTtsProviderOrder(getTtsProvider(config, prefsPath));
	const errors = [];
	for (const provider of providers) {
		const providerStart = Date.now();
		try {
			if (provider === "edge") {
				errors.push("edge: unsupported for telephony");
				continue;
			}
			const apiKey = resolveTtsApiKey(config, provider);
			if (!apiKey) {
				errors.push(`${provider}: no API key`);
				continue;
			}
			if (provider === "elevenlabs") {
				const output = TELEPHONY_OUTPUT.elevenlabs;
				return {
					success: true,
					audioBuffer: await elevenLabsTTS({
						text: params.text,
						apiKey,
						baseUrl: config.elevenlabs.baseUrl,
						voiceId: config.elevenlabs.voiceId,
						modelId: config.elevenlabs.modelId,
						outputFormat: output.format,
						seed: config.elevenlabs.seed,
						applyTextNormalization: config.elevenlabs.applyTextNormalization,
						languageCode: config.elevenlabs.languageCode,
						voiceSettings: config.elevenlabs.voiceSettings,
						timeoutMs: config.timeoutMs
					}),
					latencyMs: Date.now() - providerStart,
					provider,
					outputFormat: output.format,
					sampleRate: output.sampleRate
				};
			}
			if (provider === "mistral") {
				const output = TELEPHONY_OUTPUT.mistral;
				return {
					success: true,
					audioBuffer: await voxtralTTS({
						text: params.text,
						apiKey,
						model: config.mistral.model,
						voice: config.mistral.voice,
						speed: config.mistral.speed,
						outputFormat: output.format,
						timeoutMs: config.timeoutMs
					}),
					latencyMs: Date.now() - providerStart,
					provider,
					outputFormat: output.format,
					sampleRate: output.sampleRate
				};
			}
			const output = TELEPHONY_OUTPUT.openai;
			return {
				success: true,
				audioBuffer: await openaiTTS({
					text: params.text,
					apiKey,
					model: config.openai.model,
					voice: config.openai.voice,
					responseFormat: output.format,
					timeoutMs: config.timeoutMs
				}),
				latencyMs: Date.now() - providerStart,
				provider,
				outputFormat: output.format,
				sampleRate: output.sampleRate
			};
		} catch (err) {
			errors.push(formatTtsProviderError(provider, err));
		}
	}
	return {
		success: false,
		error: `TTS conversion failed: ${errors.join("; ") || "no providers available"}`
	};
}
async function maybeApplyTtsToPayload(params) {
	const config = resolveTtsConfig(params.cfg);
	const prefsPath = resolveTtsPrefsPath(config);
	const autoMode = resolveTtsAutoMode({
		config,
		prefsPath,
		sessionAuto: params.ttsAuto
	});
	if (autoMode === "off") {
		const rawText = params.payload.text ?? "";
		const offCleaned = parseTtsDirectives(rawText, config.modelOverrides).cleanedText.trim();
		if (offCleaned !== rawText.trim()) return {
			...params.payload,
			text: offCleaned.length > 0 ? offCleaned : void 0
		};
		return params.payload;
	}
	const text = params.payload.text ?? "";
	const directives = parseTtsDirectives(text, config.modelOverrides);
	if (directives.warnings.length > 0) logVerbose(`TTS: ignored directive overrides (${directives.warnings.join("; ")})`);
	const trimmedCleaned = directives.cleanedText.trim();
	const visibleText = trimmedCleaned.length > 0 ? trimmedCleaned : "";
	const ttsText = directives.ttsText?.trim() || visibleText;
	const nextPayload = visibleText === text.trim() ? params.payload : {
		...params.payload,
		text: visibleText.length > 0 ? visibleText : void 0
	};
	if ((autoMode === "tagged" || autoMode === "smart") && !directives.hasDirective) return nextPayload;
	if (autoMode === "inbound" && params.inboundAudio !== true) return nextPayload;
	if ((config.mode ?? "final") === "final" && params.kind && params.kind !== "final") return nextPayload;
	if (!ttsText.trim()) return nextPayload;
	if (params.payload.mediaUrl || (params.payload.mediaUrls?.length ?? 0) > 0) return nextPayload;
	if (text.includes("MEDIA:")) return nextPayload;
	if (ttsText.trim().length < 10) return nextPayload;
	const maxLength = getTtsMaxLength(prefsPath);
	let textForAudio = ttsText.trim();
	let wasSummarized = false;
	if (textForAudio.length > maxLength) if (!isSummarizationEnabled(prefsPath)) {
		logVerbose(`TTS: truncating long text (${textForAudio.length} > ${maxLength}), summarization disabled.`);
		textForAudio = `${textForAudio.slice(0, maxLength - 3)}...`;
	} else try {
		textForAudio = (await summarizeText({
			text: textForAudio,
			targetLength: maxLength,
			cfg: params.cfg,
			config,
			timeoutMs: config.timeoutMs
		})).summary;
		wasSummarized = true;
		if (textForAudio.length > config.maxTextLength) {
			logVerbose(`TTS: summary exceeded hard limit (${textForAudio.length} > ${config.maxTextLength}); truncating.`);
			textForAudio = `${textForAudio.slice(0, config.maxTextLength - 3)}...`;
		}
	} catch (err) {
		logVerbose(`TTS: summarization failed, truncating instead: ${err.message}`);
		textForAudio = `${textForAudio.slice(0, maxLength - 3)}...`;
	}
	textForAudio = stripMarkdown(textForAudio).trim();
	if (textForAudio.length < 10) return nextPayload;
	const ttsStart = Date.now();
	const result = await textToSpeech({
		text: textForAudio,
		cfg: params.cfg,
		prefsPath,
		channel: params.channel,
		overrides: directives.overrides
	});
	if (result.success && result.audioPath) {
		lastTtsAttempt = {
			timestamp: Date.now(),
			success: true,
			textLength: text.length,
			summarized: wasSummarized,
			provider: result.provider,
			latencyMs: result.latencyMs
		};
		const shouldVoice = resolveChannelId(params.channel) === "telegram" && result.voiceCompatible === true;
		return {
			...nextPayload,
			mediaUrl: result.audioPath,
			audioAsVoice: shouldVoice || params.payload.audioAsVoice
		};
	}
	lastTtsAttempt = {
		timestamp: Date.now(),
		success: false,
		textLength: text.length,
		summarized: wasSummarized,
		error: result.error
	};
	logVerbose(`TTS: conversion failed after ${Date.now() - ttsStart}ms (${result.error ?? "unknown"}).`);
	return nextPayload;
}
/**
* Strip all [[tts]] / [[/tts]] directives from text without generating audio.
* Used when TTS audio was already delivered during streaming and the final
* reply text just needs tag cleanup.
*/
function stripTtsDirectivesFromText(text, cfg) {
	return parseTtsDirectives(text, resolveTtsConfig(cfg).modelOverrides).cleanedText;
}

//#endregion
export { buildModelAliasLines as C, processLineMessage as E, resolveModel as S, isVoiceCompatibleAudio as T, setTtsMaxLength as _, isSummarizationEnabled as a, textToSpeech as b, maybeApplyTtsToPayload as c, resolveTtsAutoMode as d, resolveTtsConfig as f, setTtsEnabled as g, setSummarizationEnabled as h, getTtsProvider as i, normalizeTtsAutoMode as l, setLastTtsAttempt as m, getLastTtsAttempt as n, isTtsEnabled as o, resolveTtsPrefsPath as p, getTtsMaxLength as r, isTtsProviderConfigured as s, buildTtsSystemPromptHint as t, resolveTtsApiKey as u, setTtsProvider as v, isTelegramVoiceCompatibleAudio as w, textToSpeechTelephony as x, stripTtsDirectivesFromText as y };