@moureau/pipeflow 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +39 -17
  2. package/dist/cjs/client/client.js +2 -0
  3. package/dist/cjs/client/client.js.map +7 -0
  4. package/dist/cjs/client/index.js +2 -0
  5. package/dist/cjs/client/index.js.map +7 -0
  6. package/dist/cjs/client/json.js +2 -0
  7. package/dist/cjs/client/json.js.map +7 -0
  8. package/dist/cjs/client/protocol/websocket.js +2 -0
  9. package/dist/cjs/client/protocol/websocket.js.map +7 -0
  10. package/dist/cjs/client/protocol.js +2 -0
  11. package/dist/cjs/client/protocol.js.map +7 -0
  12. package/dist/cjs/conversations/conversation/conversation.js +1 -1
  13. package/dist/cjs/conversations/conversation/conversation.js.map +3 -3
  14. package/dist/cjs/conversations/conversations.js +1 -1
  15. package/dist/cjs/conversations/conversations.js.map +2 -2
  16. package/dist/cjs/conversations/orchestration/orchestrator/generation/generation.js +1 -1
  17. package/dist/cjs/conversations/orchestration/orchestrator/generation/generation.js.map +3 -3
  18. package/dist/cjs/conversations/orchestration/orchestrator/orchestrator.js +1 -1
  19. package/dist/cjs/conversations/orchestration/orchestrator/orchestrator.js.map +3 -3
  20. package/dist/cjs/conversations/orchestration/orchestrator/speech/speech.js +1 -1
  21. package/dist/cjs/conversations/orchestration/orchestrator/speech/speech.js.map +3 -3
  22. package/dist/cjs/conversations/orchestration/orchestrator/tools/tools.js +1 -1
  23. package/dist/cjs/conversations/orchestration/orchestrator/tools/tools.js.map +3 -3
  24. package/dist/cjs/conversations/orchestration/test-harness.js +1 -1
  25. package/dist/cjs/conversations/orchestration/test-harness.js.map +2 -2
  26. package/dist/cjs/pipeflow/pipeflow.js +1 -1
  27. package/dist/cjs/pipeflow/pipeflow.js.map +2 -2
  28. package/dist/cjs/providers/llm/adapters/openai-compatible.js +6 -6
  29. package/dist/cjs/providers/llm/adapters/openai-compatible.js.map +3 -3
  30. package/dist/cjs/providers/llm/types.js.map +2 -2
  31. package/dist/cjs/providers/stt/adapters/openrouter/openrouter.js +1 -1
  32. package/dist/cjs/providers/stt/adapters/openrouter/openrouter.js.map +3 -3
  33. package/dist/cjs/providers/tts/adapters/kokoro/kokoro.js +3 -3
  34. package/dist/cjs/providers/tts/adapters/kokoro/kokoro.js.map +3 -3
  35. package/dist/cjs/providers/tts/adapters/openrouter/openrouter.js +1 -1
  36. package/dist/cjs/providers/tts/adapters/openrouter/openrouter.js.map +3 -3
  37. package/dist/esm/client/client.js +2 -0
  38. package/dist/esm/client/client.js.map +7 -0
  39. package/dist/esm/client/index.js +2 -0
  40. package/dist/esm/client/index.js.map +7 -0
  41. package/dist/esm/client/json.js +2 -0
  42. package/dist/esm/client/json.js.map +7 -0
  43. package/dist/esm/client/protocol/websocket.js +2 -0
  44. package/dist/esm/client/protocol/websocket.js.map +7 -0
  45. package/dist/esm/client/protocol.js +1 -0
  46. package/dist/esm/client/protocol.js.map +7 -0
  47. package/dist/esm/conversations/conversation/conversation.js +1 -1
  48. package/dist/esm/conversations/conversation/conversation.js.map +3 -3
  49. package/dist/esm/conversations/conversations.js +1 -1
  50. package/dist/esm/conversations/conversations.js.map +2 -2
  51. package/dist/esm/conversations/orchestration/orchestrator/generation/generation.js +1 -1
  52. package/dist/esm/conversations/orchestration/orchestrator/generation/generation.js.map +3 -3
  53. package/dist/esm/conversations/orchestration/orchestrator/orchestrator.js +1 -1
  54. package/dist/esm/conversations/orchestration/orchestrator/orchestrator.js.map +3 -3
  55. package/dist/esm/conversations/orchestration/orchestrator/speech/speech.js +1 -1
  56. package/dist/esm/conversations/orchestration/orchestrator/speech/speech.js.map +3 -3
  57. package/dist/esm/conversations/orchestration/orchestrator/tools/tools.js +1 -1
  58. package/dist/esm/conversations/orchestration/orchestrator/tools/tools.js.map +3 -3
  59. package/dist/esm/conversations/orchestration/test-harness.js +1 -1
  60. package/dist/esm/conversations/orchestration/test-harness.js.map +2 -2
  61. package/dist/esm/pipeflow/pipeflow.js +1 -1
  62. package/dist/esm/pipeflow/pipeflow.js.map +2 -2
  63. package/dist/esm/providers/llm/adapters/openai-compatible.js +6 -6
  64. package/dist/esm/providers/llm/adapters/openai-compatible.js.map +3 -3
  65. package/dist/esm/providers/llm/types.js.map +2 -2
  66. package/dist/esm/providers/stt/adapters/openrouter/openrouter.js +1 -1
  67. package/dist/esm/providers/stt/adapters/openrouter/openrouter.js.map +3 -3
  68. package/dist/esm/providers/tts/adapters/kokoro/kokoro.js +3 -3
  69. package/dist/esm/providers/tts/adapters/kokoro/kokoro.js.map +3 -3
  70. package/dist/esm/providers/tts/adapters/openrouter/openrouter.js +1 -1
  71. package/dist/esm/providers/tts/adapters/openrouter/openrouter.js.map +3 -3
  72. package/dist/types/client/client.d.ts +186 -0
  73. package/dist/types/client/index.d.ts +31 -0
  74. package/dist/types/client/json.d.ts +13 -0
  75. package/dist/types/client/protocol/websocket.d.ts +56 -0
  76. package/dist/types/client/protocol.d.ts +46 -0
  77. package/dist/types/conversations/conversation/conversation.d.ts +45 -0
  78. package/dist/types/conversations/conversations.d.ts +24 -0
  79. package/dist/types/conversations/orchestration/orchestrator/generation/generation.d.ts +17 -0
  80. package/dist/types/conversations/orchestration/orchestrator/orchestrator.d.ts +25 -6
  81. package/dist/types/conversations/orchestration/orchestrator/speech/speech.d.ts +31 -2
  82. package/dist/types/conversations/orchestration/orchestrator/tools/tools.d.ts +41 -7
  83. package/dist/types/conversations/orchestration/test-harness.d.ts +4 -0
  84. package/dist/types/pipeflow/pipeflow.d.ts +8 -0
  85. package/dist/types/providers/llm/types.d.ts +4 -1
  86. package/dist/types/providers/stt/adapters/openrouter/openrouter.d.ts +103 -1
  87. package/dist/types/providers/tts/adapters/kokoro/kokoro.d.ts +1 -1
  88. package/dist/types/providers/tts/adapters/openrouter/openrouter.d.ts +26 -7
  89. package/package.json +7 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/providers/llm/adapters/openai-compatible.ts"],
4
- "sourcesContent": ["import type {\n LLMEvent,\n LLMRequest,\n LLMToolDefinition,\n LLMStreamTimingCallback,\n LLMUsageCallback,\n ToolMode,\n} from \"../types\";\nimport type { FetchLike } from \"../../shared\";\n\ninterface DeltaToolCall {\n index?: number;\n id?: string;\n function?: { name?: string; arguments?: string };\n}\n\ninterface ChatCompletionChunk {\n choices?: Array<{\n delta?: {\n content?: string | null;\n tool_calls?: DeltaToolCall[];\n // OpenRouter normalizes reasoning tokens to `reasoning`; DeepSeek uses\n // `reasoning_content`. Both are surfaced on the delta event.\n reasoning?: string | null;\n reasoning_content?: string | null;\n };\n finish_reason?: string | null;\n }>;\n usage?: { prompt_tokens?: number; completion_tokens?: number };\n /** Provider failure delivered as an SSE data chunk (no choices). */\n error?: { code?: number | string; message?: string };\n}\n\nexport interface OpenAICompatibleStreamParams {\n /** Provider base URL, e.g. `https://openrouter.ai/api/v1`. */\n baseUrl: string;\n apiKey: string;\n model: string;\n /** Injectable fetch implementation, mainly for tests. */\n fetchImpl: FetchLike;\n request: LLMRequest;\n signal: AbortSignal;\n /** Extra headers merged into the request (e.g. attribution headers). */\n extraHeaders?: Record<string, string>;\n /** Extra body fields merged after the standard ones. */\n extraBody?: Record<string, unknown>;\n /** Provider name used in error messages. */\n label: string;\n /** Optional provider-timeline hook (see `LLMStreamTimingPoint`). */\n onTiming?: LLMStreamTimingCallback;\n /**\n * Default tool encoding for requests that do not set `toolMode`\n * themselves (the request's `toolMode` wins when present). Useful for\n * models whose endpoints lack native tool calling.\n */\n toolMode?: ToolMode;\n /**\n * Abort the stream if no data arrives for this long (default 8000ms).\n * Protects against providers whose connection goes silent after the model\n * has already produced its output (e.g. a tool call that is never followed\n * by a terminating frame).\n */\n idleTimeoutMs?: number;\n /**\n * Called with the provider-reported token usage when the stream includes\n * it (OpenRouter sends usage in the final chunk).\n */\n onUsage?: LLMUsageCallback;\n}\n\n/**\n * JSON schema for the `envelope`/`prompted` tool modes: the model answers\n * either directly (`answer`) or with a list of tool calls (`calls`). The\n * adapter turns the parsed envelope back into the standard `LLMEvent`\n * surface, so consumers never see the envelope format.\n *\n * The `arguments` of each call carry a per-tool `oneOf` of the tools' own\n * parameter schemas (index-aligned with the `name` enum), so the model sees\n * the same argument contract it would in native mode \u2014 without the native\n * tool-calling endpoint.\n */\nfunction buildEnvelopeSchema(tools: LLMToolDefinition[]): Record<string, unknown> {\n return {\n type: \"object\",\n properties: {\n answer: {\n type: \"string\",\n description: \"Your spoken reply when no tool call is needed.\",\n },\n calls: {\n type: \"array\",\n description: \"The tool calls to execute, when the request needs tools.\",\n items: {\n type: \"object\",\n properties: {\n name: {\n type: \"string\",\n enum: tools.map((tool) => tool.name),\n },\n arguments: {\n description: \"The arguments for the named tool, per its schema.\",\n oneOf: tools.map((tool) => tool.parameters ?? { type: \"object\" }),\n },\n },\n required: [\"name\", \"arguments\"],\n additionalProperties: false,\n },\n },\n },\n };\n}\n\n/** Instruction appended to the last user message in `prompted` mode. */\nfunction promptedEnvelopeInstruction(tools: LLMToolDefinition[]): string {\n return (\n \"Respond with ONLY valid JSON matching this schema, with no prose and no markdown fences:\\n\" +\n JSON.stringify(buildEnvelopeSchema(tools)) +\n '\\nThe JSON must be either {\"answer\": \"...\"} or {\"calls\": [{\"name\": \"...\", \"arguments\": {...}}]}.'\n );\n}\n\n/**\n * Fallback extraction for prompted mode: strip markdown fences and keep the\n * outermost JSON object, rescuing models that wrap or preface the envelope\n * despite the instruction (the endpoint guarantees nothing without\n * `response_format`).\n */\nfunction extractEnvelope(raw: string): string | null {\n const stripped = raw\n .replace(/```(?:json)?/gi, \"\")\n .replace(/^[^{]*/, \"\")\n .replace(/[^}]*$/, \"\");\n if (stripped.length === 0) return null;\n const start = stripped.indexOf(\"{\");\n const end = stripped.lastIndexOf(\"}\");\n if (start === -1 || end === -1 || end <= start) return null;\n return stripped.slice(start, end + 1);\n}\n\n/**\n * System-level tool contract for `envelope`/`prompted` modes. The envelope\n * schema itself carries only tool names + argument shapes, so without this\n * the model never sees tool descriptions or the output rule up front \u2014 which\n * invites prose, native tool-call syntax, or waffling instead of the envelope.\n */\nfunction buildToolContract(tools: LLMToolDefinition[]): string {\n const list = tools.map((tool) => `- ${tool.name}: ${tool.description}`).join(\"\\n\");\n return (\n \"You have access to the following tools. When the user's request requires one, \" +\n \"respond with ONLY the tool-call JSON envelope shown in the request \u2014 no prose, \" +\n \"no markdown, no tool-call syntax. When no tool is needed, respond with the \" +\n 'envelope\\'s \"answer\" field.\\n\\nAvailable tools:\\n' +\n list\n );\n}\n\n/**\n * Stream a request through any OpenAI-compatible `/chat/completions`\n * endpoint, reassembling fragmented tool calls and normalizing the SSE\n * stream into `LLMEvent`s. Shared by the DeepSeek and OpenRouter adapters.\n */\nexport async function* openAICompatibleStream(\n params: OpenAICompatibleStreamParams,\n): AsyncGenerator<LLMEvent> {\n const {\n baseUrl,\n apiKey,\n model,\n fetchImpl,\n request,\n signal,\n extraHeaders,\n extraBody,\n label,\n onTiming,\n idleTimeoutMs,\n onUsage,\n } = params;\n\n onTiming?.(\"request-start\");\n // Precedence: the per-request override wins, then the adapter default, then\n // native.\n const toolMode = request.toolMode ?? params.toolMode ?? \"native\";\n const usesEnvelope = toolMode !== \"native\" && (request.tools?.length ?? 0) > 0;\n // In envelope modes the tools are hidden from the provider, so the model\n // never sees their descriptions (or the output rule) unless we say so.\n // Insert the contract after the caller's system messages, where formatting\n // rules belong.\n const toolContract = usesEnvelope ? buildToolContract(request.tools!) : null;\n const contractAt = request.messages.findIndex((message) => message.role !== \"system\");\n const insertAt = contractAt === -1 ? request.messages.length : contractAt;\n const wireMessages: Record<string, unknown>[] = [];\n request.messages.forEach((message, index, all) => {\n if (index === insertAt && toolContract !== null) {\n wireMessages.push({ role: \"system\", content: toolContract });\n }\n const wire: Record<string, unknown> = {\n role: message.role,\n content: message.content,\n };\n if (message.name) wire.name = message.name;\n if (message.toolCallId) wire.tool_call_id = message.toolCallId;\n if (message.toolCalls) {\n wire.tool_calls = message.toolCalls.map((call) => ({\n id: call.id,\n type: \"function\",\n function: { name: call.name, arguments: call.arguments },\n }));\n }\n if (toolMode === \"prompted\" && usesEnvelope && index === all.length - 1) {\n // The only way to constrain output without provider support: ask\n // the model directly, and parse/validate the result ourselves.\n wire.content = `${message.content}\\n\\n${promptedEnvelopeInstruction(request.tools!)}`;\n }\n wireMessages.push(wire);\n });\n if (insertAt === request.messages.length && toolContract !== null) {\n wireMessages.push({ role: \"system\", content: toolContract });\n }\n const response = await fetchImpl(`${baseUrl}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${apiKey}`,\n ...extraHeaders,\n },\n body: JSON.stringify({\n model,\n // LLMMessage uses camelCase; the wire format is snake_case.\n messages: wireMessages,\n // Envelope modes hide the tools from the provider and encode them in a\n // schema the model must emit as JSON; native mode uses the provider's\n // own tool-calling contract.\n ...(!usesEnvelope && request.tools && request.tools.length > 0\n ? { tools: request.tools.map((tool) => ({ type: \"function\", function: tool })) }\n : {}),\n ...(usesEnvelope && toolMode === \"envelope\"\n ? {\n response_format: {\n type: \"json_schema\",\n json_schema: {\n name: \"tool_envelope\",\n strict: false,\n schema: buildEnvelopeSchema(request.tools!),\n },\n },\n }\n : {}),\n stream: true,\n temperature: request.temperature,\n max_tokens: request.maxTokens,\n // Ask the provider to include usage in the final chunk so token\n // accounting (and latency-vs-tokens curves) are possible.\n stream_options: { include_usage: true },\n ...extraBody,\n }),\n signal,\n });\n\n if (!response.ok) {\n const body = await response.text();\n throw new Error(`${label} request failed (${response.status}): ${body}`);\n }\n if (!response.body) {\n throw new Error(`${label} returned an empty body`);\n }\n onTiming?.(\"headers\");\n\n // Tool calls arrive fragmented across chunks and are reassembled by\n // index before being emitted once the stream signals `tool_calls`.\n const toolCalls = new Map<number, { id: string; name: string; arguments: string }>();\n // After the content finishes (finish_reason), keep reading briefly so the\n // provider-reported usage chunk \u2014 which arrives right after \u2014 is captured.\n let contentFinished = false;\n let finishAt = 0;\n let sawUsage = false;\n let toolCallsEmitted = false;\n // Envelope modes buffer the model's JSON and translate it into events at\n // the end of the stream (the JSON is not speech and cannot be acted on\n // until it is complete).\n let envelopeContent = \"\";\n\n for await (const chunk of parseSSE(response.body, signal, onTiming, idleTimeoutMs, label)) {\n const choice = chunk.choices?.[0];\n\n if (chunk.error) {\n // Some providers (e.g. Bedrock via OpenRouter) abort mid-stream and\n // deliver the failure as an SSE chunk with no choices. Surface it\n // instead of silently swallowing it into a confusing empty stream.\n yield {\n type: \"error\",\n error: new Error(\n `${label} provider aborted the stream: ${chunk.error.message ?? JSON.stringify(chunk.error)}`,\n ),\n };\n break;\n }\n\n if (chunk.usage && (chunk.usage.prompt_tokens ?? 0) > 0) {\n sawUsage = true;\n finishAt = 0;\n onUsage?.({\n promptTokens: chunk.usage.prompt_tokens ?? 0,\n completionTokens: chunk.usage.completion_tokens ?? 0,\n });\n }\n if (!choice) continue;\n\n const delta = choice.delta;\n const content = delta?.content ?? \"\";\n // Thinking models stream reasoning before content; emit it so consumers\n // can measure thinking time (and optionally surface it). Reasoning-only\n // chunks carry empty content.\n const reasoning = (delta?.reasoning ?? delta?.reasoning_content) ?? \"\";\n if (usesEnvelope) {\n envelopeContent += content;\n } else if (content.length > 0 || reasoning.length > 0) {\n yield {\n type: \"delta\",\n content,\n ...(reasoning.length > 0 ? { reasoning } : {}),\n };\n }\n\n for (const tc of delta?.tool_calls ?? []) {\n const index = tc.index ?? 0;\n const accumulated = toolCalls.get(index) ?? { id: \"\", name: \"\", arguments: \"\" };\n if (tc.id) accumulated.id = tc.id;\n if (tc.function?.name) accumulated.name += tc.function.name;\n if (tc.function?.arguments) accumulated.arguments += tc.function.arguments;\n toolCalls.set(index, accumulated);\n }\n\n if (choice.finish_reason === \"tool_calls\" && !toolCallsEmitted) {\n for (const call of toolCalls.values()) {\n yield {\n type: \"tool_call\",\n id: call.id,\n name: call.name,\n arguments: call.arguments,\n };\n }\n toolCallsEmitted = true;\n contentFinished = true;\n } else if (choice.finish_reason === \"stop\") {\n contentFinished = true;\n } else if (choice.finish_reason === \"error\" || choice.finish_reason === \"content_filter\") {\n // Some providers (e.g. gemini models via OpenRouter) return HTTP 200\n // with an empty body and finish_reason \"error\"/\"content_filter\"\n // instead of a real failure. Surface it as an error event rather than\n // silently completing with an empty generation.\n yield {\n type: \"error\",\n error: new Error(\n `${label} stream finished with \"${choice.finish_reason}\"` +\n (toolCalls.size > 0 ? \"\" : \" and produced no output\"),\n ),\n };\n break;\n }\n\n if (contentFinished) {\n if (sawUsage) break; // usage captured \u2014 no need to wait for [DONE]\n if (finishAt === 0) finishAt = Date.now();\n // Grace for providers that send finish_reason but never [DONE] or\n // usage: proceed after a short wait instead of stalling the run.\n if (Date.now() - finishAt > 500) break;\n }\n }\n\n if (usesEnvelope) {\n let envelope:\n | { answer?: unknown; calls?: Array<{ name?: unknown; arguments?: unknown }> }\n | undefined;\n try {\n envelope = JSON.parse(envelopeContent);\n } catch {\n // Prompted mode has no endpoint guarantee: the model may wrap the JSON\n // in fences or preface it with prose. Retry with extraction before\n // failing the stream.\n const extracted = extractEnvelope(envelopeContent);\n if (extracted !== null) {\n try {\n envelope = JSON.parse(extracted);\n } catch {\n envelope = undefined;\n }\n }\n if (envelope === undefined) {\n // Some providers drop the opening bytes of the stream (observed with\n // llama-4-scout via OpenRouter): the content starts mid-object but\n // closes cleanly. Prepend the brace and retry.\n try {\n envelope = JSON.parse(`{${envelopeContent}`);\n } catch {\n envelope = undefined;\n }\n }\n }\n if (envelope === undefined) {\n yield {\n type: \"error\",\n error: new Error(\n `${label} ${toolMode} tool mode: model did not return a JSON envelope (${envelopeContent.slice(0, 120)})`,\n ),\n };\n } else {\n if (typeof envelope.answer === \"string\" && envelope.answer.length > 0) {\n yield { type: \"delta\", content: envelope.answer };\n }\n for (const call of envelope.calls ?? []) {\n if (typeof call.name !== \"string\") continue;\n // Envelope mode has no provider-assigned call ids; synthesize them so\n // consumers can match results to calls (id uniqueness is the contract).\n yield {\n type: \"tool_call\",\n id: crypto.randomUUID(),\n name: call.name,\n arguments: JSON.stringify(call.arguments ?? {}),\n };\n }\n }\n }\n\n yield { type: \"done\" };\n}\n\n/**\n * Parse an SSE byte stream into JSON events. Supports both LF and CRLF\n * framing, ignores comments and malformed frames, and stops at `[DONE]`.\n * Aborts with an `AbortError` once the signal fires.\n */\nasync function* parseSSE(\n body: ReadableStream<Uint8Array>,\n signal: AbortSignal,\n onTiming?: LLMStreamTimingCallback,\n idleTimeoutMs = 8_000,\n label = \"LLM\",\n): AsyncGenerator<ChatCompletionChunk> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n let firstChunkEmitted = false;\n\n // `reader.read()` bounded by an idle timeout: if no bytes arrive within\n // `idleTimeoutMs`, cancel the connection and fail the stream. Only bytes\n // reset the timer \u2014 a connection that stays open but silent still trips it.\n const readWithTimeout = (): Promise<Awaited<ReturnType<typeof reader.read>>> => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n return new Promise((resolve, reject) => {\n const cleanup = () => {\n if (timer !== undefined) clearTimeout(timer);\n signal.removeEventListener(\"abort\", onAbort);\n };\n const onAbort = () => {\n cleanup();\n reject(new DOMException(\"The operation was aborted.\", \"AbortError\"));\n };\n reader.read().then(\n (result) => {\n cleanup();\n resolve(result);\n },\n (error) => {\n cleanup();\n reject(error);\n },\n );\n timer = setTimeout(() => {\n cleanup();\n // Cancel the reader so the underlying connection is actually closed.\n void reader.cancel().catch(() => {});\n reject(\n new Error(\n `${label} stream idle for ${idleTimeoutMs}ms \u2014 no data received; aborting the stalled stream`,\n ),\n );\n }, idleTimeoutMs);\n signal.addEventListener(\"abort\", onAbort, { once: true });\n });\n };\n\n while (true) {\n if (signal.aborted) {\n throw new DOMException(\"The operation was aborted.\", \"AbortError\");\n }\n const { done, value } = await readWithTimeout();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const frames = buffer.split(/\\r?\\n\\r?\\n/);\n buffer = frames.pop() ?? \"\";\n for (const frame of frames) {\n for (const line of frame.split(/\\r?\\n/)) {\n if (!line.startsWith(\"data:\")) continue;\n const data = line.slice(5).trim();\n if (data === \"[DONE]\") return;\n if (!firstChunkEmitted) {\n firstChunkEmitted = true;\n onTiming?.(\"first-chunk\");\n }\n try {\n yield JSON.parse(data) as ChatCompletionChunk;\n } catch {\n // Ignore malformed frames.\n }\n }\n }\n }\n}\n"],
5
- "mappings": "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",
6
- "names": ["buildEnvelopeSchema", "tools", "tool", "__name", "promptedEnvelopeInstruction", "extractEnvelope", "raw", "stripped", "start", "end", "buildToolContract", "openAICompatibleStream", "params", "baseUrl", "apiKey", "model", "fetchImpl", "request", "signal", "extraHeaders", "extraBody", "label", "onTiming", "idleTimeoutMs", "onUsage", "toolMode", "usesEnvelope", "toolContract", "contractAt", "message", "insertAt", "wireMessages", "index", "all", "wire", "call", "response", "body", "toolCalls", "contentFinished", "finishAt", "sawUsage", "toolCallsEmitted", "envelopeContent", "chunk", "parseSSE", "choice", "delta", "content", "reasoning", "tc", "accumulated", "envelope", "extracted", "reader", "decoder", "buffer", "firstChunkEmitted", "readWithTimeout", "timer", "resolve", "reject", "cleanup", "onAbort", "result", "error", "done", "value", "frames", "frame", "line", "data"]
4
+ "sourcesContent": ["import type {\n LLMEvent,\n LLMRequest,\n LLMToolDefinition,\n LLMStreamTimingCallback,\n LLMUsageCallback,\n ToolMode,\n} from \"../types\";\nimport type { FetchLike } from \"../../shared\";\n\ninterface DeltaToolCall {\n index?: number;\n id?: string;\n function?: { name?: string; arguments?: string };\n}\n\ninterface ChatCompletionChunk {\n choices?: Array<{\n delta?: {\n content?: string | null;\n tool_calls?: DeltaToolCall[];\n // OpenRouter normalizes reasoning tokens to `reasoning`; DeepSeek uses\n // `reasoning_content`. Both are surfaced on the delta event.\n reasoning?: string | null;\n reasoning_content?: string | null;\n };\n finish_reason?: string | null;\n }>;\n usage?: { prompt_tokens?: number; completion_tokens?: number };\n /** Provider failure delivered as an SSE data chunk (no choices). */\n error?: { code?: number | string; message?: string };\n}\n\nexport interface OpenAICompatibleStreamParams {\n /** Provider base URL, e.g. `https://openrouter.ai/api/v1`. */\n baseUrl: string;\n apiKey: string;\n model: string;\n /** Injectable fetch implementation, mainly for tests. */\n fetchImpl: FetchLike;\n request: LLMRequest;\n signal: AbortSignal;\n /** Extra headers merged into the request (e.g. attribution headers). */\n extraHeaders?: Record<string, string>;\n /** Extra body fields merged after the standard ones. */\n extraBody?: Record<string, unknown>;\n /** Provider name used in error messages. */\n label: string;\n /** Optional provider-timeline hook (see `LLMStreamTimingPoint`). */\n onTiming?: LLMStreamTimingCallback;\n /**\n * Default tool encoding for requests that do not set `toolMode`\n * themselves (the request's `toolMode` wins when present). Useful for\n * models whose endpoints lack native tool calling.\n */\n toolMode?: ToolMode;\n /**\n * Abort the stream if no data arrives for this long (default 8000ms).\n * Protects against providers whose connection goes silent after the model\n * has already produced its output (e.g. a tool call that is never followed\n * by a terminating frame).\n */\n idleTimeoutMs?: number;\n /**\n * Called with the provider-reported token usage when the stream includes\n * it (OpenRouter sends usage in the final chunk).\n */\n onUsage?: LLMUsageCallback;\n}\n\n/**\n * JSON schema for the `envelope`/`prompted` tool modes: the model answers\n * either directly (`answer`) or with a list of tool calls (`calls`). The\n * adapter turns the parsed envelope back into the standard `LLMEvent`\n * surface, so consumers never see the envelope format.\n *\n * The `arguments` of each call carry a per-tool `oneOf` of the tools' own\n * parameter schemas (index-aligned with the `name` enum), so the model sees\n * the same argument contract it would in native mode \u2014 without the native\n * tool-calling endpoint.\n */\nfunction buildEnvelopeSchema(tools: LLMToolDefinition[]): Record<string, unknown> {\n return {\n type: \"object\",\n properties: {\n answer: {\n type: \"string\",\n description: \"Your spoken reply when no tool call is needed.\",\n },\n calls: {\n type: \"array\",\n description: \"The tool calls to execute, when the request needs tools.\",\n items: {\n type: \"object\",\n properties: {\n name: {\n type: \"string\",\n enum: tools.map((tool) => tool.name),\n },\n arguments: {\n description: \"The arguments for the named tool, per its schema.\",\n oneOf: tools.map((tool) => tool.parameters ?? { type: \"object\" }),\n },\n },\n required: [\"name\", \"arguments\"],\n additionalProperties: false,\n },\n },\n },\n };\n}\n\n/** Instruction appended to the last user message in `prompted` mode. */\nfunction promptedEnvelopeInstruction(tools: LLMToolDefinition[]): string {\n return (\n \"When the user's request requires a tool, respond with ONLY valid JSON matching this \" +\n \"schema, with no prose and no markdown fences:\\n\" +\n JSON.stringify(buildEnvelopeSchema(tools)) +\n '\\nThe JSON must be either {\"answer\": \"...\"} or {\"calls\": [{\"name\": \"...\", \"arguments\": {...}}]}. ' +\n \"When no tool is needed, respond in plain conversational text with no JSON.\"\n );\n}\n\n/**\n * True when the model's output looks like a (possibly broken) JSON envelope\n * attempt rather than plain prose \u2014 used to decide whether a parse failure is\n * a garbled tool call (error) or an envelope-ignoring model that simply\n * answered (speak the text).\n */\nfunction looksLikeJsonAttempt(content: string): boolean {\n const trimmed = content.trim();\n return (\n trimmed.startsWith(\"{\") || trimmed.startsWith(\"[\") || /^```(?:json)?/i.test(trimmed)\n );\n}\n\n/**\n * Fallback extraction for prompted mode: strip markdown fences and keep the\n * outermost JSON object, rescuing models that wrap or preface the envelope\n * despite the instruction (the endpoint guarantees nothing without\n * `response_format`).\n */\nfunction extractEnvelope(raw: string): string | null {\n const stripped = raw\n .replace(/```(?:json)?/gi, \"\")\n .replace(/^[^{]*/, \"\")\n .replace(/[^}]*$/, \"\");\n if (stripped.length === 0) return null;\n const start = stripped.indexOf(\"{\");\n const end = stripped.lastIndexOf(\"}\");\n if (start === -1 || end === -1 || end <= start) return null;\n return stripped.slice(start, end + 1);\n}\n\n/**\n * Stream-partition inline `<thinking>\u2026</thinking>` spans out of model content.\n * Reasoning-style models (DeepSeek-R1 and distills, Qwen3, \u2026) emit their\n * chain of thought between visible tags whenever the serving layer has no\n * native reasoning channel, and some providers prompt tags even on models\n * without native thinking. The tags are reasoning \u2014 never reply text \u2014 so\n * they are streamed out as `reasoning` (the same surface a provider\n * `reasoning`/`reasoning_content` field uses) and the surrounding text alone\n * is content. Tags can split across any chunk boundary, so trailing\n * characters that could still form a tag are held back until the next chunk.\n */\nfunction inlineThinking() {\n const OPEN = \"<thinking>\";\n const CLOSE = \"</thinking>\";\n let buffer = \"\";\n let inTag = false;\n\n /** How many trailing chars of `text` could still begin `token`. */\n const prefixTail = (text: string, token: string): number => {\n const max = Math.min(text.length, token.length - 1);\n for (let keep = max; keep >= 1; keep--) {\n if (token.startsWith(text.slice(-keep))) return keep;\n }\n return 0;\n };\n\n return {\n /** Feed one content chunk; returns the text and reasoning it releases. */\n push(chunk: string): { text: string; reasoning: string } {\n let text = \"\";\n let reasoning = \"\";\n buffer += chunk;\n for (;;) {\n if (inTag) {\n const close = buffer.indexOf(CLOSE);\n if (close !== -1) {\n reasoning += buffer.slice(0, close);\n buffer = buffer.slice(close + CLOSE.length);\n inTag = false;\n continue;\n }\n const keep = prefixTail(buffer, CLOSE);\n reasoning += buffer.slice(0, buffer.length - keep);\n buffer = buffer.slice(buffer.length - keep);\n return { text, reasoning };\n }\n const open = buffer.indexOf(OPEN);\n if (open !== -1) {\n text += buffer.slice(0, open);\n buffer = buffer.slice(open + OPEN.length);\n inTag = true;\n continue;\n }\n const keep = prefixTail(buffer, OPEN);\n text += buffer.slice(0, buffer.length - keep);\n buffer = buffer.slice(buffer.length - keep);\n return { text, reasoning };\n }\n },\n /** Release whatever is still buffered once the stream ends. */\n flush(): { text: string; reasoning: string } {\n let text = \"\";\n let reasoning = \"\";\n if (inTag) {\n // Unclosed thinking (truncated stream) is reasoning, not content.\n reasoning = buffer;\n } else if (buffer.length > 0 && !OPEN.startsWith(buffer)) {\n // A tail shorter than the tag was an aborted tag start, not content.\n text = buffer;\n }\n buffer = \"\";\n inTag = false;\n return { text, reasoning };\n },\n };\n}\n\n/**\n * System-level tool contract for `envelope`/`prompted` modes. The envelope\n * schema itself carries only tool names + argument shapes, so without this\n * the model never sees tool descriptions or the output rule up front \u2014 which\n * invites prose, native tool-call syntax, or waffling instead of the envelope.\n */\nfunction buildToolContract(tools: LLMToolDefinition[]): string {\n const list = tools.map((tool) => `- ${tool.name}: ${tool.description}`).join(\"\\n\");\n return (\n \"You have access to the following tools. When the user's request requires one, \" +\n \"respond with ONLY the tool-call JSON envelope shown in the request \u2014 no prose, \" +\n \"no markdown, no tool-call syntax. When no tool is needed, reply conversationally \" +\n \"in plain text \u2014 no JSON at all.\\n\\nAvailable tools:\\n\" +\n list\n );\n}\n\n/**\n * Stream a request through any OpenAI-compatible `/chat/completions`\n * endpoint, reassembling fragmented tool calls and normalizing the SSE\n * stream into `LLMEvent`s. Shared by the DeepSeek and OpenRouter adapters.\n */\nexport async function* openAICompatibleStream(\n params: OpenAICompatibleStreamParams,\n): AsyncGenerator<LLMEvent> {\n const {\n baseUrl,\n apiKey,\n model,\n fetchImpl,\n request,\n signal,\n extraHeaders,\n extraBody,\n label,\n onTiming,\n idleTimeoutMs,\n onUsage,\n } = params;\n\n onTiming?.(\"request-start\");\n // Precedence: the per-request override wins, then the adapter default, then\n // native.\n const toolMode = request.toolMode ?? params.toolMode ?? \"native\";\n const usesEnvelope = toolMode !== \"native\" && (request.tools?.length ?? 0) > 0;\n // In envelope modes the tools are hidden from the provider, so the model\n // never sees their descriptions (or the output rule) unless we say so.\n // Insert the contract after the caller's system messages, where formatting\n // rules belong.\n const toolContract = usesEnvelope ? buildToolContract(request.tools!) : null;\n const contractAt = request.messages.findIndex((message) => message.role !== \"system\");\n const insertAt = contractAt === -1 ? request.messages.length : contractAt;\n const wireMessages: Record<string, unknown>[] = [];\n request.messages.forEach((message, index, all) => {\n if (index === insertAt && toolContract !== null) {\n wireMessages.push({ role: \"system\", content: toolContract });\n }\n const wire: Record<string, unknown> = {\n role: message.role,\n content: message.content,\n };\n if (message.name) wire.name = message.name;\n if (message.toolCallId) wire.tool_call_id = message.toolCallId;\n if (message.toolCalls) {\n wire.tool_calls = message.toolCalls.map((call) => ({\n id: call.id,\n type: \"function\",\n function: { name: call.name, arguments: call.arguments },\n }));\n }\n if (toolMode === \"prompted\" && usesEnvelope && index === all.length - 1) {\n // The only way to constrain output without provider support: ask\n // the model directly, and parse/validate the result ourselves.\n wire.content = `${message.content}\\n\\n${promptedEnvelopeInstruction(request.tools!)}`;\n }\n wireMessages.push(wire);\n });\n if (insertAt === request.messages.length && toolContract !== null) {\n wireMessages.push({ role: \"system\", content: toolContract });\n }\n const response = await fetchImpl(`${baseUrl}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${apiKey}`,\n ...extraHeaders,\n },\n body: JSON.stringify({\n model,\n // LLMMessage uses camelCase; the wire format is snake_case.\n messages: wireMessages,\n // Envelope modes hide the tools from the provider and encode them in a\n // schema the model must emit as JSON; native mode uses the provider's\n // own tool-calling contract.\n ...(!usesEnvelope && request.tools && request.tools.length > 0\n ? { tools: request.tools.map((tool) => ({ type: \"function\", function: tool })) }\n : {}),\n ...(usesEnvelope && toolMode === \"envelope\"\n ? {\n response_format: {\n type: \"json_schema\",\n json_schema: {\n name: \"tool_envelope\",\n strict: false,\n schema: buildEnvelopeSchema(request.tools!),\n },\n },\n }\n : {}),\n stream: true,\n temperature: request.temperature,\n max_tokens: request.maxTokens,\n // Ask the provider to include usage in the final chunk so token\n // accounting (and latency-vs-tokens curves) are possible.\n stream_options: { include_usage: true },\n ...extraBody,\n }),\n signal,\n });\n\n if (!response.ok) {\n const body = await response.text();\n throw new Error(`${label} request failed (${response.status}): ${body}`);\n }\n if (!response.body) {\n throw new Error(`${label} returned an empty body`);\n }\n onTiming?.(\"headers\");\n\n // Tool calls arrive fragmented across chunks and are reassembled by\n // index before being emitted once the stream signals `tool_calls`.\n const toolCalls = new Map<number, { id: string; name: string; arguments: string }>();\n // After the content finishes (finish_reason), keep reading briefly so the\n // provider-reported usage chunk \u2014 which arrives right after \u2014 is captured.\n let contentFinished = false;\n let finishAt = 0;\n let sawUsage = false;\n let toolCallsEmitted = false;\n // Envelope modes buffer the model's JSON and translate it into events at\n // the end of the stream (the JSON is not speech and cannot be acted on\n // until it is complete).\n let envelopeContent = \"\";\n // Reasoning models sometimes stream their chain of thought as inline\n // `<thinking>` tags instead of a native reasoning field; strip them out of\n // content and surface them as `reasoning` (stateful across chunk splits).\n const thinking = inlineThinking();\n\n for await (const chunk of parseSSE(response.body, signal, onTiming, idleTimeoutMs, label)) {\n const choice = chunk.choices?.[0];\n\n if (chunk.error) {\n // Some providers (e.g. Bedrock via OpenRouter) abort mid-stream and\n // deliver the failure as an SSE chunk with no choices. Surface it\n // instead of silently swallowing it into a confusing empty stream.\n yield {\n type: \"error\",\n error: new Error(\n `${label} provider aborted the stream: ${chunk.error.message ?? JSON.stringify(chunk.error)}`,\n ),\n };\n break;\n }\n\n if (chunk.usage && (chunk.usage.prompt_tokens ?? 0) > 0) {\n sawUsage = true;\n finishAt = 0;\n onUsage?.({\n promptTokens: chunk.usage.prompt_tokens ?? 0,\n completionTokens: chunk.usage.completion_tokens ?? 0,\n });\n }\n if (!choice) continue;\n\n const delta = choice.delta;\n // Strip inline `<thinking>` blocks out of the content (they are the\n // model's reasoning, not the reply); the stripped text rides on the same\n // `reasoning` surface as a provider reasoning field, which consumers can\n // measure or display but is never spoken or written into the reply.\n const partition = thinking.push(delta?.content ?? \"\");\n const providerReasoning = (delta?.reasoning ?? delta?.reasoning_content) ?? \"\";\n const reasoning = providerReasoning + partition.reasoning;\n if (usesEnvelope) {\n envelopeContent += partition.text;\n } else if (partition.text.length > 0 || reasoning.length > 0) {\n yield {\n type: \"delta\",\n content: partition.text,\n ...(reasoning.length > 0 ? { reasoning } : {}),\n };\n }\n\n for (const tc of delta?.tool_calls ?? []) {\n const index = tc.index ?? 0;\n const accumulated = toolCalls.get(index) ?? { id: \"\", name: \"\", arguments: \"\" };\n if (tc.id) accumulated.id = tc.id;\n if (tc.function?.name) accumulated.name += tc.function.name;\n if (tc.function?.arguments) accumulated.arguments += tc.function.arguments;\n toolCalls.set(index, accumulated);\n }\n\n if (choice.finish_reason === \"tool_calls\" && !toolCallsEmitted) {\n for (const call of toolCalls.values()) {\n yield {\n type: \"tool_call\",\n id: call.id,\n name: call.name,\n arguments: call.arguments,\n };\n }\n toolCallsEmitted = true;\n contentFinished = true;\n } else if (choice.finish_reason === \"stop\") {\n contentFinished = true;\n } else if (choice.finish_reason === \"error\" || choice.finish_reason === \"content_filter\") {\n // Some providers (e.g. gemini models via OpenRouter) return HTTP 200\n // with an empty body and finish_reason \"error\"/\"content_filter\"\n // instead of a real failure. Surface it as an error event rather than\n // silently completing with an empty generation.\n yield {\n type: \"error\",\n error: new Error(\n `${label} stream finished with \"${choice.finish_reason}\"` +\n (toolCalls.size > 0 ? \"\" : \" and produced no output\"),\n ),\n };\n break;\n }\n\n if (contentFinished) {\n if (sawUsage) break; // usage captured \u2014 no need to wait for [DONE]\n if (finishAt === 0) finishAt = Date.now();\n // Grace for providers that send finish_reason but never [DONE] or\n // usage: proceed after a short wait instead of stalling the run.\n if (Date.now() - finishAt > 500) break;\n }\n }\n\n // Anything left in the thinking partitioner once the stream ends: unclosed\n // reasoning is dropped (never content), and an aborted tag start is dropped\n // too. Real trailing content is flushed here.\n const tail = thinking.flush();\n if (tail.text) {\n if (usesEnvelope) envelopeContent += tail.text;\n else yield { type: \"delta\", content: tail.text };\n }\n\n if (usesEnvelope) {\n let envelope:\n | { answer?: unknown; calls?: Array<{ name?: unknown; arguments?: unknown }> }\n | undefined;\n try {\n envelope = JSON.parse(envelopeContent);\n } catch {\n // Prompted mode has no endpoint guarantee: the model may wrap the JSON\n // in fences or preface it with prose. Retry with extraction before\n // failing the stream.\n const extracted = extractEnvelope(envelopeContent);\n if (extracted !== null) {\n try {\n envelope = JSON.parse(extracted);\n } catch {\n envelope = undefined;\n }\n }\n if (envelope === undefined) {\n // Some providers drop the opening bytes of the stream (observed with\n // llama-4-scout via OpenRouter): the content starts mid-object but\n // closes cleanly. Prepend the brace and retry.\n try {\n envelope = JSON.parse(`{${envelopeContent}`);\n } catch {\n envelope = undefined;\n }\n }\n }\n if (envelope === undefined) {\n // Prompted mode has no endpoint guarantee (`envelope` mode's\n // response_format enforces JSON). Smaller chat models often ignore the\n // envelope instruction and reply in plain prose \u2014 that is still a valid\n // reply, so speak it as the model's `answer` instead of failing the\n // turn with a technical error. Only output that looks like a (broken)\n // JSON attempt keeps the loud error, so a truncated envelope is never\n // spoken as garbage.\n if (toolMode === \"prompted\" && !looksLikeJsonAttempt(envelopeContent)) {\n const answer = envelopeContent.trim();\n if (answer.length > 0) yield { type: \"delta\", content: answer };\n } else {\n yield {\n type: \"error\",\n error: new Error(\n `${label} ${toolMode} tool mode: model did not return a JSON envelope (${envelopeContent.slice(0, 120)})`,\n ),\n };\n }\n } else {\n if (typeof envelope.answer === \"string\" && envelope.answer.length > 0) {\n yield { type: \"delta\", content: envelope.answer };\n }\n for (const call of envelope.calls ?? []) {\n if (typeof call.name !== \"string\") continue;\n // Envelope mode has no provider-assigned call ids; synthesize them so\n // consumers can match results to calls (id uniqueness is the contract).\n yield {\n type: \"tool_call\",\n id: crypto.randomUUID(),\n name: call.name,\n arguments: JSON.stringify(call.arguments ?? {}),\n };\n }\n }\n }\n\n yield { type: \"done\" };\n}\n\n/**\n * Parse an SSE byte stream into JSON events. Supports both LF and CRLF\n * framing, ignores comments and malformed frames, and stops at `[DONE]`.\n * Aborts with an `AbortError` once the signal fires.\n */\nasync function* parseSSE(\n body: ReadableStream<Uint8Array>,\n signal: AbortSignal,\n onTiming?: LLMStreamTimingCallback,\n idleTimeoutMs = 8_000,\n label = \"LLM\",\n): AsyncGenerator<ChatCompletionChunk> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n let firstChunkEmitted = false;\n\n // `reader.read()` bounded by an idle timeout: if no bytes arrive within\n // `idleTimeoutMs`, cancel the connection and fail the stream. Only bytes\n // reset the timer \u2014 a connection that stays open but silent still trips it.\n const readWithTimeout = (): Promise<Awaited<ReturnType<typeof reader.read>>> => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n return new Promise((resolve, reject) => {\n const cleanup = () => {\n if (timer !== undefined) clearTimeout(timer);\n signal.removeEventListener(\"abort\", onAbort);\n };\n const onAbort = () => {\n cleanup();\n reject(new DOMException(\"The operation was aborted.\", \"AbortError\"));\n };\n reader.read().then(\n (result) => {\n cleanup();\n resolve(result);\n },\n (error) => {\n cleanup();\n reject(error);\n },\n );\n timer = setTimeout(() => {\n cleanup();\n // Cancel the reader so the underlying connection is actually closed.\n void reader.cancel().catch(() => {});\n reject(\n new Error(\n `${label} stream idle for ${idleTimeoutMs}ms \u2014 no data received; aborting the stalled stream`,\n ),\n );\n }, idleTimeoutMs);\n signal.addEventListener(\"abort\", onAbort, { once: true });\n });\n };\n\n while (true) {\n if (signal.aborted) {\n throw new DOMException(\"The operation was aborted.\", \"AbortError\");\n }\n const { done, value } = await readWithTimeout();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const frames = buffer.split(/\\r?\\n\\r?\\n/);\n buffer = frames.pop() ?? \"\";\n for (const frame of frames) {\n for (const line of frame.split(/\\r?\\n/)) {\n if (!line.startsWith(\"data:\")) continue;\n const data = line.slice(5).trim();\n if (data === \"[DONE]\") return;\n if (!firstChunkEmitted) {\n firstChunkEmitted = true;\n onTiming?.(\"first-chunk\");\n }\n try {\n yield JSON.parse(data) as ChatCompletionChunk;\n } catch {\n // Ignore malformed frames.\n }\n }\n }\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["buildEnvelopeSchema", "tools", "tool", "__name", "promptedEnvelopeInstruction", "looksLikeJsonAttempt", "content", "trimmed", "extractEnvelope", "raw", "stripped", "start", "end", "inlineThinking", "OPEN", "CLOSE", "buffer", "inTag", "prefixTail", "text", "token", "max", "keep", "chunk", "reasoning", "close", "open", "buildToolContract", "openAICompatibleStream", "params", "baseUrl", "apiKey", "model", "fetchImpl", "request", "signal", "extraHeaders", "extraBody", "label", "onTiming", "idleTimeoutMs", "onUsage", "toolMode", "usesEnvelope", "toolContract", "contractAt", "message", "insertAt", "wireMessages", "index", "all", "wire", "call", "response", "body", "toolCalls", "contentFinished", "finishAt", "sawUsage", "toolCallsEmitted", "envelopeContent", "thinking", "parseSSE", "choice", "delta", "partition", "tc", "accumulated", "tail", "envelope", "extracted", "answer", "reader", "decoder", "firstChunkEmitted", "readWithTimeout", "timer", "resolve", "reject", "cleanup", "onAbort", "result", "error", "done", "value", "frames", "frame", "line", "data"]
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/providers/llm/types.ts"],
4
- "sourcesContent": ["export type LLMRole = \"system\" | \"user\" | \"assistant\" | \"tool\";\n\nexport interface LLMToolCall {\n id: string;\n name: string;\n /** JSON-encoded arguments for the tool. */\n arguments: string;\n}\n\nexport interface LLMMessage {\n role: LLMRole;\n content: string;\n /** Tool name, for `tool` role messages. */\n name?: string;\n /** Tool call id this message answers, for `tool` role messages. */\n toolCallId?: string;\n /** Tool calls announced by the assistant, for `assistant` role messages. */\n toolCalls?: LLMToolCall[];\n}\n\nexport interface LLMToolDefinition {\n name: string;\n description: string;\n /** JSON schema describing the tool arguments. */\n parameters: Record<string, unknown>;\n}\n\n/**\n * How tool calls are encoded on the wire. The semantic contract is the same\n * in every mode \u2014 callers still pass `tools` and consume `tool_call` events \u2014\n * only the encoding differs. The right mode is a property of the model's\n * endpoints, not the caller: use `ToolModeBenchmark` to measure a model and\n * set the mode once at adapter construction.\n *\n * - `native` (default) \u2014 the provider's tool-calling contract: `tools` in\n * the request, `tool_calls` in the stream. Streaming deltas and\n * provider-enforced argument schemas, at the price of wire overhead: the\n * provider expands the schema into its native tool format, which bills\n * measurably more prompt tokens (often 5-10x the cost of the modes below).\n * - `envelope` \u2014 no `tools`; `response_format` forces the model to emit a\n * JSON envelope (`{ answer?, calls: [{ name, arguments }] }`) that the\n * adapter translates back into `tool_call` events. Endpoint-guaranteed\n * JSON and a lean prompt (dramatically cheaper per decision), but nothing\n * is actionable until the whole envelope arrives \u2014 no streaming deltas.\n * Only for endpoints that support structured outputs.\n * - `prompted` \u2014 no `tools`; the same envelope is requested by appending an\n * instruction to the last user message. The universal fallback: works on\n * any chat model, at the cost of extraction/repair/retry, higher token\n * use, and tail-latency risk.\n */\nexport type ToolMode = \"native\" | \"envelope\" | \"prompted\";\n\n/**\n * A string that may also be one of the branded literal values \u2014 e.g. any\n * model id, with the favorites offered for autocomplete. The `& {}` on the\n * string side stops TypeScript from absorbing the literals into `string`\n * (the reverse arrangement \u2014 `string | (T & {})` \u2014 collapses and loses the\n * literals).\n */\nexport type StringOr<T extends string> = T | (string & {});\n\n/** Models measured as usable; offered for autocomplete where a model id is taken. */\nexport const FAVORITE_MODELS = [\n \"meta-llama/llama-4-scout\",\n \"google/gemini-2.5-flash-lite\",\n \"amazon/nova-micro-v1\",\n \"amazon/nova-lite-v1\",\n \"inclusionai/ling-3.0-flash\",\n \"sao10k/l3-lunaris-8b\",\n \"openai/gpt-oss-20b\",\n] as const;\n\n/** Any model id, with the favorites offered for autocomplete. */\nexport type FavoriteModel = StringOr<(typeof FAVORITE_MODELS)[number]>;\n\nexport interface LLMRequest {\n messages: LLMMessage[];\n tools?: LLMToolDefinition[];\n temperature?: number;\n maxTokens?: number;\n /** How tools are encoded on the wire. Default `native`. */\n toolMode?: ToolMode;\n}\n\n/**\n * Moments in the provider round trip that separate application delay from\n * network/queue delay from model latency. The callback is invoked\n * synchronously at each point, so `performance.now()` taken inside it is the\n * event time.\n *\n * - `request-start` \u2014 immediately before the HTTP request is issued.\n * - `headers` \u2014 the response headers arrived (2xx).\n * - `first-chunk` \u2014 the first SSE chunk was parsed.\n */\nexport type LLMStreamTimingPoint = \"request-start\" | \"headers\" | \"first-chunk\";\n\nexport type LLMStreamTimingCallback = (point: LLMStreamTimingPoint) => void;\n\n/** Token usage reported by the provider (when it includes usage in the stream). */\nexport interface LLMUsage {\n promptTokens: number;\n completionTokens: number;\n}\n\nexport type LLMUsageCallback = (usage: LLMUsage) => void;\n\nexport type LLMEvent =\n | {\n type: \"delta\";\n content: string;\n /**\n * Reasoning/thinking tokens, when the provider streams them separately\n * from content (OpenRouter `reasoning`, DeepSeek `reasoning_content`).\n * Omitted when the model does not emit reasoning.\n */\n reasoning?: string;\n }\n | { type: \"tool_call\"; id: string; name: string; arguments: string }\n | { type: \"done\" }\n | { type: \"error\"; error: Error };\n\n/**\n * Vendor-independent language model interface.\n *\n * `stream()` produces deltas as they arrive plus optional tool calls, and\n * terminates with `done`. Transport-level failures (network, HTTP status)\n * are thrown from the generator rather than emitted.\n */\nexport interface LLM {\n stream(request: LLMRequest): AsyncGenerator<LLMEvent>;\n /** Cancel the currently in-flight generation, if any. */\n stop(): void;\n}\n"],
5
- "mappings": "AA8DO,MAAMA,EAAkB,CAC7B,2BACA,+BACA,uBACA,sBACA,6BACA,uBACA,oBACF",
4
+ "sourcesContent": ["export type LLMRole = \"system\" | \"user\" | \"assistant\" | \"tool\";\n\nexport interface LLMToolCall {\n id: string;\n name: string;\n /** JSON-encoded arguments for the tool. */\n arguments: string;\n}\n\nexport interface LLMMessage {\n role: LLMRole;\n content: string;\n /** Tool name, for `tool` role messages. */\n name?: string;\n /** Tool call id this message answers, for `tool` role messages. */\n toolCallId?: string;\n /** Tool calls announced by the assistant, for `assistant` role messages. */\n toolCalls?: LLMToolCall[];\n}\n\nexport interface LLMToolDefinition {\n name: string;\n description: string;\n /** JSON schema describing the tool arguments. */\n parameters: Record<string, unknown>;\n}\n\n/**\n * How tool calls are encoded on the wire. The semantic contract is the same\n * in every mode \u2014 callers still pass `tools` and consume `tool_call` events \u2014\n * only the encoding differs. The right mode is a property of the model's\n * endpoints, not the caller: use `ToolModeBenchmark` to measure a model and\n * set the mode once at adapter construction.\n *\n * - `native` (default) \u2014 the provider's tool-calling contract: `tools` in\n * the request, `tool_calls` in the stream. Streaming deltas and\n * provider-enforced argument schemas, at the price of wire overhead: the\n * provider expands the schema into its native tool format, which bills\n * measurably more prompt tokens (often 5-10x the cost of the modes below).\n * - `envelope` \u2014 no `tools`; `response_format` forces the model to emit a\n * JSON envelope (`{ answer?, calls: [{ name, arguments }] }`) that the\n * adapter translates back into `tool_call` events. Endpoint-guaranteed\n * JSON and a lean prompt (dramatically cheaper per decision), but nothing\n * is actionable until the whole envelope arrives \u2014 no streaming deltas.\n * Only for endpoints that support structured outputs.\n * - `prompted` \u2014 no `tools`; the same envelope is requested by appending an\n * instruction to the last user message. The universal fallback: works on\n * any chat model, at the cost of extraction/repair/retry, higher token\n * use, and tail-latency risk. Because nothing constrains the output, a\n * model that ignores the envelope and replies in plain prose is answered\n * directly \u2014 its text is treated as the reply rather than failing the\n * generation.\n */\nexport type ToolMode = \"native\" | \"envelope\" | \"prompted\";\n\n/**\n * A string that may also be one of the branded literal values \u2014 e.g. any\n * model id, with the favorites offered for autocomplete. The `& {}` on the\n * string side stops TypeScript from absorbing the literals into `string`\n * (the reverse arrangement \u2014 `string | (T & {})` \u2014 collapses and loses the\n * literals).\n */\nexport type StringOr<T extends string> = T | (string & {});\n\n/** Models measured as usable; offered for autocomplete where a model id is taken. */\nexport const FAVORITE_MODELS = [\n \"meta-llama/llama-4-scout\",\n \"google/gemini-2.5-flash-lite\",\n \"amazon/nova-micro-v1\",\n \"amazon/nova-lite-v1\",\n \"inclusionai/ling-3.0-flash\",\n \"sao10k/l3-lunaris-8b\",\n \"openai/gpt-oss-20b\",\n] as const;\n\n/** Any model id, with the favorites offered for autocomplete. */\nexport type FavoriteModel = StringOr<(typeof FAVORITE_MODELS)[number]>;\n\nexport interface LLMRequest {\n messages: LLMMessage[];\n tools?: LLMToolDefinition[];\n temperature?: number;\n maxTokens?: number;\n /** How tools are encoded on the wire. Default `native`. */\n toolMode?: ToolMode;\n}\n\n/**\n * Moments in the provider round trip that separate application delay from\n * network/queue delay from model latency. The callback is invoked\n * synchronously at each point, so `performance.now()` taken inside it is the\n * event time.\n *\n * - `request-start` \u2014 immediately before the HTTP request is issued.\n * - `headers` \u2014 the response headers arrived (2xx).\n * - `first-chunk` \u2014 the first SSE chunk was parsed.\n */\nexport type LLMStreamTimingPoint = \"request-start\" | \"headers\" | \"first-chunk\";\n\nexport type LLMStreamTimingCallback = (point: LLMStreamTimingPoint) => void;\n\n/** Token usage reported by the provider (when it includes usage in the stream). */\nexport interface LLMUsage {\n promptTokens: number;\n completionTokens: number;\n}\n\nexport type LLMUsageCallback = (usage: LLMUsage) => void;\n\nexport type LLMEvent =\n | {\n type: \"delta\";\n content: string;\n /**\n * Reasoning/thinking tokens, when the provider streams them separately\n * from content (OpenRouter `reasoning`, DeepSeek `reasoning_content`).\n * Omitted when the model does not emit reasoning.\n */\n reasoning?: string;\n }\n | { type: \"tool_call\"; id: string; name: string; arguments: string }\n | { type: \"done\" }\n | { type: \"error\"; error: Error };\n\n/**\n * Vendor-independent language model interface.\n *\n * `stream()` produces deltas as they arrive plus optional tool calls, and\n * terminates with `done`. Transport-level failures (network, HTTP status)\n * are thrown from the generator rather than emitted.\n */\nexport interface LLM {\n stream(request: LLMRequest): AsyncGenerator<LLMEvent>;\n /** Cancel the currently in-flight generation, if any. */\n stop(): void;\n}\n"],
5
+ "mappings": "AAiEO,MAAMA,EAAkB,CAC7B,2BACA,+BACA,uBACA,sBACA,6BACA,uBACA,oBACF",
6
6
  "names": ["FAVORITE_MODELS"]
7
7
  }
@@ -1,2 +1,2 @@
1
- var d=Object.defineProperty;var l=(r,e)=>d(r,"name",{value:e,configurable:!0});class g{static{l(this,"OpenRouterSTT")}options;sessions=new Set;constructor(e){if(!e.apiKey)throw new Error("OpenRouterSTT requires an apiKey");this.options=e}start(e={}){const i={...this.options,...e},t=new c(i);return this.sessions.add(t),t.on("close",()=>{this.sessions.delete(t)}),t}cancel(){for(const e of[...this.sessions])e.abort()}}class c{static{l(this,"OpenRouterSession")}listeners=new Map;apiKey;baseUrl;model;language;sampleRate;silenceMs;audioFormat;fetchImpl;chunks=[];timer=null;chain=Promise.resolve();controllers=new Set;ended=!1;aborted=!1;constructor(e){this.apiKey=e.apiKey,this.baseUrl=(e.baseUrl??"https://openrouter.ai/api/v1").replace(/\/+$/,""),this.model=e.model??"openai/whisper-large-v3-turbo",this.language=e.language==="auto"?void 0:e.language,this.sampleRate=e.sampleRate??16e3,this.silenceMs=e.silenceMs??800,this.audioFormat=e.audioFormat??"pcm",this.fetchImpl=e.fetch??fetch}write(e){if(this.ended||this.aborted)throw new Error("OpenRouter session is closed");this.chunks.push(e),this.timer&&clearTimeout(this.timer),this.timer=setTimeout(()=>this.flush(),this.silenceMs)}async end(){this.ended||(this.ended=!0,this.timer&&(clearTimeout(this.timer),this.timer=null),this.flush(),await this.chain,this.emit("close"))}abort(){if(!this.aborted){this.aborted=!0,this.timer&&(clearTimeout(this.timer),this.timer=null),this.chunks=[];for(const e of this.controllers)e.abort();this.emit("close")}}on(e,i){let t=this.listeners.get(e);t||(t=new Set,this.listeners.set(e,t)),t.add(i)}flush(){this.chain=this.chain.then(async()=>{if(this.aborted||this.chunks.length===0)return;const e=this.takeBuffer();await this.transcribe(e)})}takeBuffer(){const e=this.chunks;this.chunks=[];let i=0;for(const s of e)i+=s.byteLength;const t=new Uint8Array(i);let a=0;for(const s of e)t.set(s,a),a+=s.byteLength;return t}async transcribe(e){const i=new AbortController;this.controllers.add(i);try{const t=await h({apiKey:this.apiKey,baseUrl:this.baseUrl,model:this.model,language:this.language,sampleRate:this.sampleRate,audioFormat:this.audioFormat,audio:e,signal:i.signal,fetchImpl:this.fetchImpl});!this.aborted&&t&&this.emit("final",t)}catch(t){this.aborted||this.emit("error",t instanceof Error?t:new Error(String(t)))}finally{this.controllers.delete(i)}}emit(e,...i){const t=this.listeners.get(e);if(t)for(const a of[...t])a(...i)}}const p={mp3:"audio/mpeg",flac:"audio/flac",m4a:"audio/mp4",ogg:"audio/ogg",webm:"audio/webm",aac:"audio/aac"};async function h(r){const e=new FormData;e.append("model",r.model);let i,t,a;r.audioFormat==="pcm"?(i=m(r.audio,r.sampleRate),t="audio.wav",a="audio/wav"):(i=r.audio,t=`audio.${r.audioFormat}`,a=p[r.audioFormat]),e.append("file",new Blob([i],{type:a}),t),r.language&&e.append("language",r.language);const s=await r.fetchImpl(`${r.baseUrl}/audio/transcriptions`,{method:"POST",headers:{authorization:`Bearer ${r.apiKey}`},body:e,signal:r.signal});if(!s.ok){const n=await s.text().catch(()=>"");throw new Error(`OpenRouter transcription failed (${s.status})${n?`: ${n}`:""}`)}return((await s.json()).text??"").trim()}l(h,"transcribeClip");function m(r,e){const a=e*1*2,s=2,u=r.byteLength,n=new Uint8Array(44+u),o=new DataView(n.buffer);return n.set([82,73,70,70],0),o.setUint32(4,36+u,!0),n.set([87,65,86,69],8),n.set([102,109,116,32],12),o.setUint32(16,16,!0),o.setUint16(20,1,!0),o.setUint16(22,1,!0),o.setUint32(24,e,!0),o.setUint32(28,a,!0),o.setUint16(32,s,!0),o.setUint16(34,16,!0),n.set([100,97,116,97],36),o.setUint32(40,u,!0),n.set(r,44),n}l(m,"toWav");export{g as OpenRouterSTT,c as OpenRouterSession,m as toWav};
1
+ var p=Object.defineProperty;var u=(i,e)=>p(i,"name",{value:e,configurable:!0});class O{static{u(this,"OpenRouterSTT")}options;sessions=new Set;constructor(e){if(!e.apiKey)throw new Error("OpenRouterSTT requires an apiKey");this.options=e}start(e={}){const t={...this.options,...e},n=new c(t,this.options);return this.sessions.add(n),n.on("close",()=>{this.sessions.delete(n)}),n}set minClipRms(e){this.options.minClipRms=e}get minClipRms(){return this.options.minClipRms}cancel(){for(const e of[...this.sessions])e.abort()}}class c{static{u(this,"OpenRouterSession")}listeners=new Map;apiKey;baseUrl;model;language;sampleRate;silenceMs;audioFormat;filterHallucinations;fillerPhrases;onClipEnergy;transcriptionTimeoutMs;temperature;providerOptions;fetchImpl;liveOptions;get minClipRms(){return this.liveOptions.minClipRms}chunks=[];timer=null;chain=Promise.resolve();controllers=new Set;ended=!1;aborted=!1;constructor(e,t){this.apiKey=e.apiKey,this.baseUrl=(e.baseUrl??"https://openrouter.ai/api/v1").replace(/\/+$/,""),this.model=e.model??"openai/whisper-large-v3-turbo",this.language=e.language==="auto"?void 0:e.language,this.sampleRate=e.sampleRate??16e3,this.silenceMs=e.silenceMs??800,this.audioFormat=e.audioFormat??"pcm",this.filterHallucinations=e.filterHallucinations??!0,this.fillerPhrases=e.fillerPhrases,this.onClipEnergy=e.onClipEnergy,this.transcriptionTimeoutMs=e.transcriptionTimeoutMs??3e4,this.temperature=e.temperature,this.providerOptions=e.providerOptions,this.fetchImpl=e.fetch??fetch,this.liveOptions=t}write(e){if(this.ended||this.aborted)throw new Error("OpenRouter session is closed");this.chunks.push(e),this.timer&&clearTimeout(this.timer),this.timer=setTimeout(()=>this.flush(),this.silenceMs)}async end(){this.ended||(this.ended=!0,this.timer&&(clearTimeout(this.timer),this.timer=null),this.flush(),await this.chain,this.emit("close"))}abort(){if(!this.aborted){this.aborted=!0,this.timer&&(clearTimeout(this.timer),this.timer=null),this.chunks=[];for(const e of this.controllers)e.abort();this.emit("close")}}on(e,t){let n=this.listeners.get(e);n||(n=new Set,this.listeners.set(e,n)),n.add(t)}flush(){if(this.aborted)return;const e=this.takeBuffer();e.byteLength!==0&&(this.chain=this.chain.then(async()=>{this.aborted||await this.transcribe(e)}))}takeBuffer(){const e=this.chunks;this.chunks=[];let t=0;for(const s of e)t+=s.byteLength;const n=new Uint8Array(t);let r=0;for(const s of e)n.set(s,r),r+=s.byteLength;return n}async transcribe(e){if(this.audioFormat==="pcm"&&(this.minClipRms!==void 0||this.onClipEnergy)){const r=v(e),s=this.minClipRms===void 0||r>=this.minClipRms;if(this.onClipEnergy?.(r,s),!s)return}const t=new AbortController;this.controllers.add(t);let n=!1;try{const r=await f(h({apiKey:this.apiKey,baseUrl:this.baseUrl,model:this.model,language:this.language,sampleRate:this.sampleRate,audioFormat:this.audioFormat,temperature:this.temperature,providerOptions:this.providerOptions,audio:e,signal:t.signal,fetchImpl:this.fetchImpl}),this.transcriptionTimeoutMs,()=>{n=!0,t.abort()}),s=this.filterHallucinations?y(r,this.fillerPhrases):r;!this.aborted&&s&&this.emit("final",s)}catch(r){if(!this.aborted){const s=n?`OpenRouter transcription timed out after ${this.transcriptionTimeoutMs}ms`:r instanceof Error?r.message:String(r);this.emit("error",new Error(s))}}finally{this.controllers.delete(t)}}emit(e,...t){const n=this.listeners.get(e);if(n)for(const r of[...n])r(...t)}}const m={mp3:"audio/mpeg",flac:"audio/flac",m4a:"audio/mp4",ogg:"audio/ogg",webm:"audio/webm",aac:"audio/aac"};async function h(i){const e=new FormData;e.append("model",i.model);let t,n,r;i.audioFormat==="pcm"?(t=g(i.audio,i.sampleRate),n="audio.wav",r="audio/wav"):(t=i.audio,n=`audio.${i.audioFormat}`,r=m[i.audioFormat]),e.append("file",new Blob([t],{type:r}),n),i.language&&e.append("language",i.language),i.temperature!==void 0&&e.append("temperature",String(i.temperature)),i.providerOptions&&e.append("provider",JSON.stringify(i.providerOptions));const s=await i.fetchImpl(`${i.baseUrl}/audio/transcriptions`,{method:"POST",headers:{authorization:`Bearer ${i.apiKey}`},body:e,signal:i.signal});if(!s.ok){const o=await s.text().catch(()=>"");throw new Error(`OpenRouter transcription failed (${s.status})${o?`: ${o}`:""}`)}return((await s.json()).text??"").trim()}u(h,"transcribeClip");function f(i,e,t){return new Promise((n,r)=>{const s=setTimeout(()=>{t(),r(new Error(`timed out after ${e}ms`))},e);i.then(a=>{clearTimeout(s),n(a)},a=>{clearTimeout(s),r(a)})})}u(f,"withTimeout");function g(i,e){const r=e*1*2,s=2,a=i.byteLength,o=new Uint8Array(44+a),l=new DataView(o.buffer);return o.set([82,73,70,70],0),l.setUint32(4,36+a,!0),o.set([87,65,86,69],8),o.set([102,109,116,32],12),l.setUint32(16,16,!0),l.setUint16(20,1,!0),l.setUint16(22,1,!0),l.setUint32(24,e,!0),l.setUint32(28,r,!0),l.setUint16(32,s,!0),l.setUint16(34,16,!0),o.set([100,97,116,97],36),l.setUint32(40,a,!0),o.set(i,44),o}u(g,"toWav");function v(i){const e=Math.floor(i.byteLength/2);if(e===0)return 0;let t=0;for(let n=0;n<e;n++){const r=i[n*2],s=i[n*2+1],a=r|s<<8,o=a>=32768?a-65536:a;t+=o*o}return Math.sqrt(t/e)/32768}u(v,"pcmRms");const d=["thank you","thanks","thank you for watching","thanks for watching","please subscribe","bye bye","goodbye","bye","good night","i love you","love you","you're welcome","no problem","have a great day","see you later","see you","okay","ok","e a\xED","e ai","oi","ol\xE1","ola","tudo bem","obrigado","obrigada","t\xE1 bom","ta bom","tchau","adeus","bom dia","boa noite","entendi","ah bom","\xE9 isso","hola","qu\xE9 tal","que tal","vale","adi\xF3s","adios","bonjour","merci","d'accord","au revoir","hallo","danke","tsch\xFCss","tschuss"];function b(i,e){const t=i.toLowerCase().replace(/[.,!?;:]+/g," ").replace(/\s+/g," ").trim();if(t.length===0)return!1;const n=[...e].sort((a,o)=>o.length-a.length);let r=t,s=!1;for(;;){const a=n.find(o=>r.startsWith(o));if(!a||(s=!0,r=r.slice(a.length).replace(/^\s+/,""),r.length===0))break}return s&&r.length===0}u(b,"isFillerOnly");function y(i,e=[]){let t=i.replace(/\*[^*]*\*/g," ").replace(/\s+/g," ").trim();if(t.length===0)return"";const n=t.split(/(?<=[.!?])\s+/).filter(o=>o.length>0),r=[];for(const o of n){const l=r.at(-1);l!==void 0&&l.toLowerCase()===o.toLowerCase()||r.push(o)}if(t=r.join(" ").trim(),t.length===0)return"";const s=[...d,...e.map(o=>o.toLowerCase())],a=t.toLowerCase().replace(/[.!?]+$/g,"");return d.includes(a)||b(t,s)?"":t}u(y,"cleanTranscript");export{O as OpenRouterSTT,c as OpenRouterSession,y as cleanTranscript,v as pcmRms,g as toWav};
2
2
  //# sourceMappingURL=openrouter.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/providers/stt/adapters/openrouter/openrouter.ts"],
4
- "sourcesContent": ["import type { STT, STTOptions, STTSession } from \"../../types\";\nimport type { FetchLike } from \"../../../shared\";\n\n/** Audio formats the OpenRouter transcription endpoint accepts. */\nexport type OpenRouterAudioFormat =\n | \"pcm\"\n | \"mp3\"\n | \"flac\"\n | \"m4a\"\n | \"ogg\"\n | \"webm\"\n | \"aac\";\n\nexport interface OpenRouterSTTOptions extends STTOptions {\n apiKey: string;\n /** Defaults to `https://openrouter.ai/api/v1`. */\n baseUrl?: string;\n /** The STT model. Defaults to `openai/whisper-large-v3-turbo`. */\n model?: string;\n /**\n * Silence (ms) that ends an utterance. OpenRouter's transcription endpoint\n * is batch-only (one request per audio clip), so the adapter segments the\n * incoming stream client-side and transcribes a clip once this much silence\n * has elapsed since the last audio. Default 800.\n */\n silenceMs?: number;\n /**\n * Format of the audio written to the session. `\"pcm\"` (the default)\n * assumes raw linear16 samples at `sampleRate` and wraps each clip in a\n * WAV container. Pass the actual encoding (e.g. `\"mp3\"`, `\"ogg\"`, \u2026) when\n * feeding pre-encoded audio \u2014 those formats are self-describing and are\n * sent as-is.\n */\n audioFormat?: OpenRouterAudioFormat;\n /** Injectable fetch, mainly for tests. */\n fetch?: FetchLike;\n}\n\ntype OpenRouterEvent = \"partial\" | \"final\" | \"error\" | \"close\";\n\n/**\n * OpenRouter batch STT adapter (`POST /api/v1/audio/transcriptions`).\n *\n * OpenRouter does not stream transcripts \u2014 it transcribes a complete audio\n * clip and returns the text. The adapter therefore buffers the raw audio\n * written by `write()`, assumes linear16 PCM (mono, `sampleRate`, default\n * 16 kHz \u2014 matching the Deepgram adapter's defaults), wraps each utterance\n * in a WAV header, and posts it after `silenceMs` of silence. Interim\n * results are unavailable, so no `partial` events are ever emitted: a turn\n * arrives whole at the `final` event once its clip has been transcribed.\n *\n * `language` accepts an ISO-639-1 code to force a language (which whisper\n * docs say improves accuracy and latency); omitting it \u2014 or passing `\"auto\"`,\n * the whisper convention, which is normalized to \"omit\" \u2014 leaves detection to\n * the provider, matching OpenRouter's \"auto-detected if omitted\" contract.\n *\n * By default the written audio is treated as raw linear16 PCM and wrapped in\n * a WAV container; set `audioFormat` to the real encoding (e.g. `\"mp3\"`) to\n * send pre-encoded audio as-is instead.\n *\n * `end()` transcribes any remaining buffered audio; `cancel()` drops\n * buffered audio and aborts in-flight requests.\n */\nexport class OpenRouterSTT implements STT {\n private readonly options: OpenRouterSTTOptions;\n private readonly sessions = new Set<OpenRouterSession>();\n\n constructor(options: OpenRouterSTTOptions) {\n if (!options.apiKey) {\n throw new Error(\"OpenRouterSTT requires an apiKey\");\n }\n this.options = options;\n }\n\n start(options: STTOptions = {}): STTSession {\n const merged: OpenRouterSTTOptions = { ...this.options, ...options };\n const session = new OpenRouterSession(merged);\n this.sessions.add(session);\n session.on(\"close\", () => {\n this.sessions.delete(session);\n });\n return session;\n }\n\n cancel(): void {\n for (const session of [...this.sessions]) {\n session.abort();\n }\n }\n}\n\nexport class OpenRouterSession implements STTSession {\n private readonly listeners = new Map<OpenRouterEvent, Set<(...args: any[]) => void>>();\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly model: string;\n private readonly language: string | undefined;\n private readonly sampleRate: number;\n private readonly silenceMs: number;\n private readonly audioFormat: OpenRouterAudioFormat;\n private readonly fetchImpl: FetchLike;\n\n private chunks: Uint8Array[] = [];\n private timer: ReturnType<typeof setTimeout> | null = null;\n private chain: Promise<void> = Promise.resolve();\n private readonly controllers = new Set<AbortController>();\n private ended = false;\n private aborted = false;\n\n constructor(options: OpenRouterSTTOptions) {\n this.apiKey = options.apiKey;\n this.baseUrl = (options.baseUrl ?? \"https://openrouter.ai/api/v1\").replace(/\\/+$/, \"\");\n this.model = options.model ?? \"openai/whisper-large-v3-turbo\";\n // `\"auto\"` is the whisper convention for \"let the provider detect\";\n // OpenRouter detects when the field is omitted, so normalize it away\n // rather than forwarding a string some providers may reject.\n this.language = options.language === \"auto\" ? undefined : options.language;\n this.sampleRate = options.sampleRate ?? 16_000;\n this.silenceMs = options.silenceMs ?? 800;\n this.audioFormat = options.audioFormat ?? \"pcm\";\n this.fetchImpl = options.fetch ?? fetch;\n }\n\n write(audio: Uint8Array): void {\n if (this.ended || this.aborted) {\n throw new Error(\"OpenRouter session is closed\");\n }\n this.chunks.push(audio);\n // Restart the silence clock: a clip is finalized once no audio has\n // arrived for `silenceMs`.\n if (this.timer) clearTimeout(this.timer);\n this.timer = setTimeout(() => this.flush(), this.silenceMs);\n }\n\n async end(): Promise<void> {\n if (this.ended) return;\n this.ended = true;\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n // Transcribe any trailing audio, then wait for every queued clip.\n this.flush();\n await this.chain;\n this.emit(\"close\");\n }\n\n /** Drop buffered audio and abort in-flight requests (cancel). */\n abort(): void {\n if (this.aborted) return;\n this.aborted = true;\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n this.chunks = [];\n for (const controller of this.controllers) controller.abort();\n this.emit(\"close\");\n }\n\n on(event: \"partial\", listener: (transcript: string) => void): void;\n on(event: \"final\", listener: (transcript: string) => void): void;\n on(event: \"error\", listener: (error: Error) => void): void;\n on(event: \"close\", listener: () => void): void;\n on(event: OpenRouterEvent, listener: (...args: any[]) => void): void {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(listener);\n }\n\n /** Queue a transcription of whatever audio is buffered (serialized). */\n private flush(): void {\n this.chain = this.chain.then(async () => {\n if (this.aborted) return;\n if (this.chunks.length === 0) return;\n const audio = this.takeBuffer();\n await this.transcribe(audio);\n });\n }\n\n private takeBuffer(): Uint8Array {\n const chunks = this.chunks;\n this.chunks = [];\n let total = 0;\n for (const chunk of chunks) total += chunk.byteLength;\n const out = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n out.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return out;\n }\n\n private async transcribe(audio: Uint8Array): Promise<void> {\n const controller = new AbortController();\n this.controllers.add(controller);\n try {\n const text = await transcribeClip({\n apiKey: this.apiKey,\n baseUrl: this.baseUrl,\n model: this.model,\n language: this.language,\n sampleRate: this.sampleRate,\n audioFormat: this.audioFormat,\n audio,\n signal: controller.signal,\n fetchImpl: this.fetchImpl,\n });\n if (!this.aborted && text) this.emit(\"final\", text);\n } catch (error) {\n if (!this.aborted) {\n this.emit(\"error\", error instanceof Error ? error : new Error(String(error)));\n }\n } finally {\n this.controllers.delete(controller);\n }\n }\n\n private emit(event: \"partial\", arg: string): void;\n private emit(event: \"final\", arg: string): void;\n private emit(event: \"error\", arg: Error): void;\n private emit(event: \"close\"): void;\n private emit(event: OpenRouterEvent, ...args: any[]): void {\n const set = this.listeners.get(event);\n if (!set) return;\n for (const listener of [...set]) {\n listener(...args);\n }\n }\n}\n\ninterface TranscribeClipOptions {\n apiKey: string;\n baseUrl: string;\n model: string;\n language: string | undefined;\n sampleRate: number;\n audioFormat: OpenRouterAudioFormat;\n audio: Uint8Array;\n signal: AbortSignal;\n fetchImpl: FetchLike;\n}\n\nconst AUDIO_MIME: Record<Exclude<OpenRouterAudioFormat, \"pcm\">, string> = {\n mp3: \"audio/mpeg\",\n flac: \"audio/flac\",\n m4a: \"audio/mp4\",\n ogg: \"audio/ogg\",\n webm: \"audio/webm\",\n aac: \"audio/aac\",\n};\n\nasync function transcribeClip(options: TranscribeClipOptions): Promise<string> {\n const form = new FormData();\n form.append(\"model\", options.model);\n // OpenRouter accepts OpenAI-style multipart uploads. Raw linear16 PCM is\n // wrapped in a WAV container so whisper can read it; pre-encoded formats\n // (mp3, ogg, \u2026) are self-describing and sent as-is.\n let bytes: Uint8Array;\n let name: string;\n let type: string;\n if (options.audioFormat === \"pcm\") {\n bytes = toWav(options.audio, options.sampleRate);\n name = \"audio.wav\";\n type = \"audio/wav\";\n } else {\n bytes = options.audio;\n name = `audio.${options.audioFormat}`;\n type = AUDIO_MIME[options.audioFormat];\n }\n form.append(\"file\", new Blob([bytes], { type }), name);\n if (options.language) form.append(\"language\", options.language);\n\n const response = await options.fetchImpl(`${options.baseUrl}/audio/transcriptions`, {\n method: \"POST\",\n headers: { authorization: `Bearer ${options.apiKey}` },\n body: form,\n signal: options.signal,\n });\n if (!response.ok) {\n const detail = await response.text().catch(() => \"\");\n throw new Error(\n `OpenRouter transcription failed (${response.status})${detail ? `: ${detail}` : \"\"}`,\n );\n }\n const data = (await response.json()) as { text?: string };\n return (data.text ?? \"\").trim();\n}\n\n/**\n * Wrap linear16 PCM (mono) in a minimal WAV container so whisper can read it.\n * The header is the standard 44-byte RIFF/WAVE layout.\n */\nexport function toWav(pcm: Uint8Array, sampleRate: number): Uint8Array {\n const bytesPerSample = 2; // linear16\n const channels = 1;\n const byteRate = sampleRate * channels * bytesPerSample;\n const blockAlign = channels * bytesPerSample;\n const dataSize = pcm.byteLength;\n\n const wav = new Uint8Array(44 + dataSize);\n const view = new DataView(wav.buffer);\n wav.set([0x52, 0x49, 0x46, 0x46], 0); // \"RIFF\"\n view.setUint32(4, 36 + dataSize, true);\n wav.set([0x57, 0x41, 0x56, 0x45], 8); // \"WAVE\"\n wav.set([0x66, 0x6d, 0x74, 0x20], 12); // \"fmt \"\n view.setUint32(16, 16, true); // fmt chunk size\n view.setUint16(20, 1, true); // PCM\n view.setUint16(22, channels, true);\n view.setUint32(24, sampleRate, true);\n view.setUint32(28, byteRate, true);\n view.setUint16(32, blockAlign, true);\n view.setUint16(34, bytesPerSample * 8, true);\n wav.set([0x64, 0x61, 0x74, 0x61], 36); // \"data\"\n view.setUint32(40, dataSize, true);\n wav.set(pcm, 44);\n return wav;\n}\n"],
5
- "mappings": "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",
6
- "names": ["OpenRouterSTT", "__name", "options", "merged", "session", "OpenRouterSession", "audio", "controller", "event", "listener", "set", "chunks", "total", "chunk", "out", "offset", "text", "transcribeClip", "error", "args", "AUDIO_MIME", "form", "bytes", "name", "type", "toWav", "response", "detail", "pcm", "sampleRate", "byteRate", "blockAlign", "dataSize", "wav", "view"]
4
+ "sourcesContent": ["import type { STT, STTOptions, STTSession } from \"../../types\";\nimport type { FetchLike } from \"../../../shared\";\n\n/** Audio formats the OpenRouter transcription endpoint accepts. */\nexport type OpenRouterAudioFormat =\n | \"pcm\"\n | \"mp3\"\n | \"flac\"\n | \"m4a\"\n | \"ogg\"\n | \"webm\"\n | \"aac\";\n\nexport interface OpenRouterSTTOptions extends STTOptions {\n apiKey: string;\n /** Defaults to `https://openrouter.ai/api/v1`. */\n baseUrl?: string;\n /** The STT model. Defaults to `openai/whisper-large-v3-turbo`. */\n model?: string;\n /**\n * Silence (ms) that ends an utterance. OpenRouter's transcription endpoint\n * is batch-only (one request per audio clip), so the adapter segments the\n * incoming stream client-side and transcribes a clip once this much silence\n * has elapsed since the last audio. Default 800.\n */\n silenceMs?: number;\n /**\n * Format of the audio written to the session. `\"pcm\"` (the default)\n * assumes raw linear16 samples at `sampleRate` and wraps each clip in a\n * WAV container. Pass the actual encoding (e.g. `\"mp3\"`, `\"ogg\"`, \u2026) when\n * feeding pre-encoded audio \u2014 those formats are self-describing and are\n * sent as-is.\n */\n audioFormat?: OpenRouterAudioFormat;\n /**\n * Strip common whisper hallucinations from transcripts: asterisk stage\n * directions (\"*Dramatic music*\"), repeated filler phrases (\"Thank you.\n * Thank you.\"), and pure-filler clips whisper emits on near-silence\n * (\"Thank you.\", \"E a\u00ED.\", \"Bye.\", \u2026). Set `false` to pass transcripts\n * through untouched. Default `true`.\n */\n filterHallucinations?: boolean;\n /**\n * Extra phrases (lowercased) treated as hallucinations on top of the\n * built-in multilingual filler list \u2014 for fillers whisper \"hears\" in the\n * languages you deploy. A transcript that is *entirely* built from known\n * fillers is dropped. Default none.\n */\n fillerPhrases?: string[];\n /**\n * Energy floor (0\u20131 RMS, on the raw sample range) for transcribing a\n * buffered `pcm` clip. Near-silence clips \u2014 the ones whisper hallucinates\n * \"E a\u00ED.\" / \"Thank you.\" / *stage directions* on \u2014 are skipped entirely\n * (no transcription request, no `final`), instead of being filtered after\n * the fact. Real speech sits far above any reasonable floor; start around\n * `0.01`\u2013`0.02` and raise it if artifacts persist. Only applies to\n * `audioFormat: \"pcm\"` (the only format whose samples are measurable).\n * Default: no floor (transcribe everything the client sends).\n */\n minClipRms?: number;\n /**\n * Called for every buffered `pcm` clip with its measured mean RMS and\n * whether it was transcribed \u2014 for logging the real distribution and\n * tuning `minClipRms` against it (speech clips sit far above artifact\n * clips). Fires when this callback or `minClipRms` is set.\n */\n onClipEnergy?: (rms: number, transcribed: boolean) => void;\n /**\n * Abort a transcription request that produces no response within this\n * long (default 30000ms). Transcriptions are serialized on one session\n * chain, so a hung provider request would otherwise block every later\n * clip (each turn silently waits) until the server restarts. Raise it for\n * very long clips or slow providers.\n */\n transcriptionTimeoutMs?: number;\n /**\n * Sampling temperature (0\u20131) for transcription. Lower is more\n * deterministic; whisper's API default is already 0, so this is rarely the\n * lever for hallucinations.\n */\n temperature?: number;\n /**\n * Provider-specific passthrough, serialized as the `provider` multipart\n * field. OpenRouter ignores the top-level `prompt` field (\"accepted but\n * ignored\"), so the only way to reach whisper's prompt \u2014 vocabulary /\n * context steering \u2014 is per-provider:\n *\n * ```ts\n * providerOptions: { options: { groq: { prompt: \"Transcribe exactly what is said.\" } } }\n * ```\n *\n * Only the options for the provider that actually serves the request are\n * forwarded, so the key must match the serving provider (openai, groq,\n * together, \u2026).\n */\n providerOptions?: Record<string, unknown>;\n /** Injectable fetch, mainly for tests. */\n fetch?: FetchLike;\n}\n\ntype OpenRouterEvent = \"partial\" | \"final\" | \"error\" | \"close\";\n\n/**\n * OpenRouter batch STT adapter (`POST /api/v1/audio/transcriptions`).\n *\n * OpenRouter does not stream transcripts \u2014 it transcribes a complete audio\n * clip and returns the text. The adapter therefore buffers the raw audio\n * written by `write()`, assumes linear16 PCM (mono, `sampleRate`, default\n * 16 kHz \u2014 matching the Deepgram adapter's defaults), wraps each utterance\n * in a WAV header, and posts it after `silenceMs` of silence. Interim\n * results are unavailable, so no `partial` events are ever emitted: a turn\n * arrives whole at the `final` event once its clip has been transcribed.\n *\n * `language` accepts an ISO-639-1 code to force a language (which whisper\n * docs say improves accuracy and latency); omitting it \u2014 or passing `\"auto\"`,\n * the whisper convention, which is normalized to \"omit\" \u2014 leaves detection to\n * the provider, matching OpenRouter's \"auto-detected if omitted\" contract.\n *\n * By default the written audio is treated as raw linear16 PCM and wrapped in\n * a WAV container; set `audioFormat` to the real encoding (e.g. `\"mp3\"`) to\n * send pre-encoded audio as-is instead.\n *\n * `end()` transcribes any remaining buffered audio; `cancel()` drops\n * buffered audio and aborts in-flight requests.\n */\nexport class OpenRouterSTT implements STT {\n private readonly options: OpenRouterSTTOptions;\n private readonly sessions = new Set<OpenRouterSession>();\n\n constructor(options: OpenRouterSTTOptions) {\n if (!options.apiKey) {\n throw new Error(\"OpenRouterSTT requires an apiKey\");\n }\n this.options = options;\n }\n\n start(options: STTOptions = {}): STTSession {\n const merged: OpenRouterSTTOptions = { ...this.options, ...options };\n // Pass the adapter's own options object as the live source: the session\n // snapshots the per-session overrides but reads live-tunable options\n // (minClipRms) through the adapter, so runtime changes apply to every\n // session without recreating it.\n const session = new OpenRouterSession(merged, this.options);\n this.sessions.add(session);\n session.on(\"close\", () => {\n this.sessions.delete(session);\n });\n return session;\n }\n\n /**\n * Live-tunable energy floor (0\u20131 RMS) for `pcm` clips: raise it to skip\n * more near-silence clips (the whisper hallucinations), lower it to keep\n * quieter speech. Applies to all sessions immediately.\n */\n set minClipRms(value: number | undefined) {\n this.options.minClipRms = value;\n }\n\n get minClipRms(): number | undefined {\n return this.options.minClipRms;\n }\n\n cancel(): void {\n for (const session of [...this.sessions]) {\n session.abort();\n }\n }\n}\n\nexport class OpenRouterSession implements STTSession {\n private readonly listeners = new Map<OpenRouterEvent, Set<(...args: any[]) => void>>();\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly model: string;\n private readonly language: string | undefined;\n private readonly sampleRate: number;\n private readonly silenceMs: number;\n private readonly audioFormat: OpenRouterAudioFormat;\n private readonly filterHallucinations: boolean;\n private readonly fillerPhrases: string[] | undefined;\n private readonly onClipEnergy: ((rms: number, transcribed: boolean) => void) | undefined;\n private readonly transcriptionTimeoutMs: number;\n private readonly temperature: number | undefined;\n private readonly providerOptions: Record<string, unknown> | undefined;\n private readonly fetchImpl: FetchLike;\n /** The adapter's live options object; live-tunable fields are read here. */\n private readonly liveOptions: OpenRouterSTTOptions;\n\n /** Live-tunable energy floor, read through the adapter (not a snapshot). */\n private get minClipRms(): number | undefined {\n return this.liveOptions.minClipRms;\n }\n\n private chunks: Uint8Array[] = [];\n private timer: ReturnType<typeof setTimeout> | null = null;\n private chain: Promise<void> = Promise.resolve();\n private readonly controllers = new Set<AbortController>();\n private ended = false;\n private aborted = false;\n\n constructor(options: OpenRouterSTTOptions, liveOptions: OpenRouterSTTOptions) {\n this.apiKey = options.apiKey;\n this.baseUrl = (options.baseUrl ?? \"https://openrouter.ai/api/v1\").replace(/\\/+$/, \"\");\n this.model = options.model ?? \"openai/whisper-large-v3-turbo\";\n // `\"auto\"` is the whisper convention for \"let the provider detect\";\n // OpenRouter detects when the field is omitted, so normalize it away\n // rather than forwarding a string some providers may reject.\n this.language = options.language === \"auto\" ? undefined : options.language;\n this.sampleRate = options.sampleRate ?? 16_000;\n this.silenceMs = options.silenceMs ?? 800;\n this.audioFormat = options.audioFormat ?? \"pcm\";\n this.filterHallucinations = options.filterHallucinations ?? true;\n this.fillerPhrases = options.fillerPhrases;\n this.onClipEnergy = options.onClipEnergy;\n this.transcriptionTimeoutMs = options.transcriptionTimeoutMs ?? 30_000;\n this.temperature = options.temperature;\n this.providerOptions = options.providerOptions;\n this.fetchImpl = options.fetch ?? fetch;\n this.liveOptions = liveOptions;\n }\n\n write(audio: Uint8Array): void {\n if (this.ended || this.aborted) {\n throw new Error(\"OpenRouter session is closed\");\n }\n this.chunks.push(audio);\n // Restart the silence clock: a clip is finalized once no audio has\n // arrived for `silenceMs`.\n if (this.timer) clearTimeout(this.timer);\n this.timer = setTimeout(() => this.flush(), this.silenceMs);\n }\n\n async end(): Promise<void> {\n if (this.ended) return;\n this.ended = true;\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n // Transcribe any trailing audio, then wait for every queued clip.\n this.flush();\n await this.chain;\n this.emit(\"close\");\n }\n\n /** Drop buffered audio and abort in-flight requests (cancel). */\n abort(): void {\n if (this.aborted) return;\n this.aborted = true;\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n this.chunks = [];\n for (const controller of this.controllers) controller.abort();\n this.emit(\"close\");\n }\n\n on(event: \"partial\", listener: (transcript: string) => void): void;\n on(event: \"final\", listener: (transcript: string) => void): void;\n on(event: \"error\", listener: (error: Error) => void): void;\n on(event: \"close\", listener: () => void): void;\n on(event: OpenRouterEvent, listener: (...args: any[]) => void): void {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(listener);\n }\n\n /** Queue a transcription of whatever audio is buffered (serialized). */\n /**\n * Queue a transcription of whatever audio is buffered (serialized). The\n * clip boundary is frozen *now* \u2014 the moment the trailing silence was\n * detected \u2014 so audio that arrives while this transcription is queued or\n * in flight starts the next clip instead of being swept into this one.\n */\n private flush(): void {\n if (this.aborted) return;\n const audio = this.takeBuffer();\n if (audio.byteLength === 0) return;\n this.chain = this.chain.then(async () => {\n if (this.aborted) return;\n await this.transcribe(audio);\n });\n }\n\n private takeBuffer(): Uint8Array {\n const chunks = this.chunks;\n this.chunks = [];\n let total = 0;\n for (const chunk of chunks) total += chunk.byteLength;\n const out = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n out.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return out;\n }\n\n private async transcribe(audio: Uint8Array): Promise<void> {\n // Near-silence clips (the ones whisper hallucinates on) are dropped at\n // the source: below the energy floor the clip is never sent, saving the\n // transcription round trip and preventing the fabricated turn entirely.\n if (this.audioFormat === \"pcm\" && (this.minClipRms !== undefined || this.onClipEnergy)) {\n const rms = pcmRms(audio);\n const transcribed = this.minClipRms === undefined || rms >= this.minClipRms;\n this.onClipEnergy?.(rms, transcribed);\n if (!transcribed) return;\n }\n const controller = new AbortController();\n this.controllers.add(controller);\n // A transcription request that never responds would otherwise block this\n // session's serialized chain forever \u2014 every later clip silently waits.\n // Bound it: on timeout the request is aborted and surfaces as an error,\n // and the chain moves on to the next clip.\n let timedOut = false;\n try {\n const raw = await withTimeout(\n transcribeClip({\n apiKey: this.apiKey,\n baseUrl: this.baseUrl,\n model: this.model,\n language: this.language,\n sampleRate: this.sampleRate,\n audioFormat: this.audioFormat,\n temperature: this.temperature,\n providerOptions: this.providerOptions,\n audio,\n signal: controller.signal,\n fetchImpl: this.fetchImpl,\n }),\n this.transcriptionTimeoutMs,\n () => {\n timedOut = true;\n controller.abort();\n },\n );\n const text = this.filterHallucinations\n ? cleanTranscript(raw, this.fillerPhrases)\n : raw;\n if (!this.aborted && text) this.emit(\"final\", text);\n } catch (error) {\n if (!this.aborted) {\n const message = timedOut\n ? `OpenRouter transcription timed out after ${this.transcriptionTimeoutMs}ms`\n : error instanceof Error\n ? error.message\n : String(error);\n this.emit(\"error\", new Error(message));\n }\n } finally {\n this.controllers.delete(controller);\n }\n }\n\n private emit(event: \"partial\", arg: string): void;\n private emit(event: \"final\", arg: string): void;\n private emit(event: \"error\", arg: Error): void;\n private emit(event: \"close\"): void;\n private emit(event: OpenRouterEvent, ...args: any[]): void {\n const set = this.listeners.get(event);\n if (!set) return;\n for (const listener of [...set]) {\n listener(...args);\n }\n }\n}\n\ninterface TranscribeClipOptions {\n apiKey: string;\n baseUrl: string;\n model: string;\n language: string | undefined;\n sampleRate: number;\n audioFormat: OpenRouterAudioFormat;\n temperature: number | undefined;\n providerOptions: Record<string, unknown> | undefined;\n audio: Uint8Array;\n signal: AbortSignal;\n fetchImpl: FetchLike;\n}\n\nconst AUDIO_MIME: Record<Exclude<OpenRouterAudioFormat, \"pcm\">, string> = {\n mp3: \"audio/mpeg\",\n flac: \"audio/flac\",\n m4a: \"audio/mp4\",\n ogg: \"audio/ogg\",\n webm: \"audio/webm\",\n aac: \"audio/aac\",\n};\n\nasync function transcribeClip(options: TranscribeClipOptions): Promise<string> {\n const form = new FormData();\n form.append(\"model\", options.model);\n // OpenRouter accepts OpenAI-style multipart uploads. Raw linear16 PCM is\n // wrapped in a WAV container so whisper can read it; pre-encoded formats\n // (mp3, ogg, \u2026) are self-describing and sent as-is.\n let bytes: Uint8Array;\n let name: string;\n let type: string;\n if (options.audioFormat === \"pcm\") {\n bytes = toWav(options.audio, options.sampleRate);\n name = \"audio.wav\";\n type = \"audio/wav\";\n } else {\n bytes = options.audio;\n name = `audio.${options.audioFormat}`;\n type = AUDIO_MIME[options.audioFormat];\n }\n form.append(\"file\", new Blob([bytes], { type }), name);\n if (options.language) form.append(\"language\", options.language);\n if (options.temperature !== undefined) {\n form.append(\"temperature\", String(options.temperature));\n }\n // The only way to reach whisper's `prompt` through OpenRouter (the\n // top-level field is accepted but ignored) is per-provider passthrough.\n if (options.providerOptions) {\n form.append(\"provider\", JSON.stringify(options.providerOptions));\n }\n\n const response = await options.fetchImpl(`${options.baseUrl}/audio/transcriptions`, {\n method: \"POST\",\n headers: { authorization: `Bearer ${options.apiKey}` },\n body: form,\n signal: options.signal,\n });\n if (!response.ok) {\n const detail = await response.text().catch(() => \"\");\n throw new Error(\n `OpenRouter transcription failed (${response.status})${detail ? `: ${detail}` : \"\"}`,\n );\n }\n const data = (await response.json()) as { text?: string };\n return (data.text ?? \"\").trim();\n}\n\n/**\n * Race `promise` against a timeout: reject after `ms` and fire `onTimeout`\n * (which should abort the underlying request) so the hanging operation is\n * released rather than left pending forever.\n */\nfunction withTimeout<T>(\n promise: Promise<T>,\n ms: number,\n onTimeout: () => void,\n): Promise<T> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n onTimeout();\n reject(new Error(`timed out after ${ms}ms`));\n }, ms);\n promise.then(\n (value) => {\n clearTimeout(timer);\n resolve(value);\n },\n (error) => {\n clearTimeout(timer);\n reject(error);\n },\n );\n });\n}\n\n/**\n * Wrap linear16 PCM (mono) in a minimal WAV container so whisper can read it.\n * The header is the standard 44-byte RIFF/WAVE layout.\n */\nexport function toWav(pcm: Uint8Array, sampleRate: number): Uint8Array {\n const bytesPerSample = 2; // linear16\n const channels = 1;\n const byteRate = sampleRate * channels * bytesPerSample;\n const blockAlign = channels * bytesPerSample;\n const dataSize = pcm.byteLength;\n\n const wav = new Uint8Array(44 + dataSize);\n const view = new DataView(wav.buffer);\n wav.set([0x52, 0x49, 0x46, 0x46], 0); // \"RIFF\"\n view.setUint32(4, 36 + dataSize, true);\n wav.set([0x57, 0x41, 0x56, 0x45], 8); // \"WAVE\"\n wav.set([0x66, 0x6d, 0x74, 0x20], 12); // \"fmt \"\n view.setUint32(16, 16, true); // fmt chunk size\n view.setUint16(20, 1, true); // PCM\n view.setUint16(22, channels, true);\n view.setUint32(24, sampleRate, true);\n view.setUint32(28, byteRate, true);\n view.setUint16(32, blockAlign, true);\n view.setUint16(34, bytesPerSample * 8, true);\n wav.set([0x64, 0x61, 0x74, 0x61], 36); // \"data\"\n view.setUint32(40, dataSize, true);\n wav.set(pcm, 44);\n return wav;\n}\n\n/**\n * Mean RMS of linear16 PCM (mono), in the 0\u20131 sample range. Used as the\n * clip-level energy floor: near-silence clips sit far below real speech.\n */\nexport function pcmRms(pcm: Uint8Array): number {\n const sampleCount = Math.floor(pcm.byteLength / 2);\n if (sampleCount === 0) return 0;\n let sum = 0;\n for (let i = 0; i < sampleCount; i++) {\n const lo = pcm[i * 2]!;\n const hi = pcm[i * 2 + 1]!;\n const sample = lo | (hi << 8);\n const signed = sample >= 0x8000 ? sample - 0x10000 : sample;\n sum += signed * signed;\n }\n return Math.sqrt(sum / sampleCount) / 32768;\n}\n\n/**\n * Conversational fillers whisper loves to \"hear\" when a clip is mostly\n * silence (a trailing gap, a cough, the speaker's own voice echoing back).\n * Only a transcript that is *entirely* built from these is dropped, so a\n * genuine \"thank you\" still passes through when it is part of a longer\n * utterance. The list is multilingual because whisper fills near-silence\n * with the language it thinks it heard (\"E a\u00ED\" for Portuguese speech, \u2026).\n */\nconst FILLER_PHRASES = [\n // English\n \"thank you\",\n \"thanks\",\n \"thank you for watching\",\n \"thanks for watching\",\n \"please subscribe\",\n \"bye bye\",\n \"goodbye\",\n \"bye\",\n \"good night\",\n \"i love you\",\n \"love you\",\n \"you're welcome\",\n \"no problem\",\n \"have a great day\",\n \"see you later\",\n \"see you\",\n \"okay\",\n \"ok\",\n // Portuguese\n \"e a\u00ED\",\n \"e ai\",\n \"oi\",\n \"ol\u00E1\",\n \"ola\",\n \"tudo bem\",\n \"obrigado\",\n \"obrigada\",\n \"t\u00E1 bom\",\n \"ta bom\",\n \"tchau\",\n \"adeus\",\n \"bom dia\",\n \"boa noite\",\n \"entendi\",\n \"ah bom\",\n \"\u00E9 isso\",\n // Spanish\n \"hola\",\n \"qu\u00E9 tal\",\n \"que tal\",\n \"vale\",\n \"adi\u00F3s\",\n \"adios\",\n // French\n \"bonjour\",\n \"merci\",\n \"d'accord\",\n \"au revoir\",\n // German\n \"hallo\",\n \"danke\",\n \"tsch\u00FCss\",\n \"tschuss\",\n];\n\n/**\n * True when `text` is entirely a concatenation of known filler phrases\n * (\"E a\u00ED.\", \"E a\u00ED E a\u00ED\", \"ok ok thank you\"), with punctuation and extra\n * whitespace ignored. Repeated fillers are the classic near-silence\n * artifact \u2014 whisper often emits them with no sentence boundary at all.\n */\nfunction isFillerOnly(text: string, phrases: string[]): boolean {\n const normalized = text\n .toLowerCase()\n .replace(/[.,!?;:]+/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n if (normalized.length === 0) return false;\n const sorted = [...phrases].sort((a, b) => b.length - a.length);\n let rest = normalized;\n let matched = false;\n for (;;) {\n const found = sorted.find((phrase) => rest.startsWith(phrase));\n if (!found) break;\n matched = true;\n rest = rest.slice(found.length).replace(/^\\s+/, \"\");\n if (rest.length === 0) break;\n }\n return matched && rest.length === 0;\n}\n\n/**\n * Clean a whisper transcript of its most common hallucinations, returning\n * \"\" when nothing worth speaking remains:\n *\n * 1. Asterisk stage directions (\"*Dramatic music*\", \"*laughs*\") are\n * dropped \u2014 whisper transcribes background noise as stage notes.\n * 2. Consecutive repeated sentences (\"Thank you. Thank you.\") collapse to\n * one \u2014 a doubled filler is a classic near-silence artifact.\n * 3. A transcript built entirely from filler phrases (the built-in\n * multilingual list plus `extraFillers`) is dropped.\n */\nexport function cleanTranscript(text: string, extraFillers: string[] = []): string {\n let out = text\n .replace(/\\*[^*]*\\*/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n if (out.length === 0) return \"\";\n\n const sentences = out.split(/(?<=[.!?])\\s+/).filter((sentence) => sentence.length > 0);\n const collapsed: string[] = [];\n for (const sentence of sentences) {\n const last = collapsed.at(-1);\n if (last !== undefined && last.toLowerCase() === sentence.toLowerCase()) continue;\n collapsed.push(sentence);\n }\n out = collapsed.join(\" \").trim();\n if (out.length === 0) return \"\";\n\n const phrases = [...FILLER_PHRASES, ...extraFillers.map((phrase) => phrase.toLowerCase())];\n const normalized = out.toLowerCase().replace(/[.!?]+$/g, \"\");\n return FILLER_PHRASES.includes(normalized) || isFillerOnly(out, phrases) ? \"\" : out;\n}\n"],
5
+ "mappings": "+EA6HO,MAAMA,CAA6B,CA7H1C,MA6H0C,CAAAC,EAAA,sBACvB,QACA,SAAW,IAAI,IAEhC,YAAYC,EAA+B,CACzC,GAAI,CAACA,EAAQ,OACX,MAAM,IAAI,MAAM,kCAAkC,EAEpD,KAAK,QAAUA,CACjB,CAEA,MAAMA,EAAsB,CAAC,EAAe,CAC1C,MAAMC,EAA+B,CAAE,GAAG,KAAK,QAAS,GAAGD,CAAQ,EAK7DE,EAAU,IAAIC,EAAkBF,EAAQ,KAAK,OAAO,EAC1D,YAAK,SAAS,IAAIC,CAAO,EACzBA,EAAQ,GAAG,QAAS,IAAM,CACxB,KAAK,SAAS,OAAOA,CAAO,CAC9B,CAAC,EACMA,CACT,CAOA,IAAI,WAAWE,EAA2B,CACxC,KAAK,QAAQ,WAAaA,CAC5B,CAEA,IAAI,YAAiC,CACnC,OAAO,KAAK,QAAQ,UACtB,CAEA,QAAe,CACb,UAAWF,IAAW,CAAC,GAAG,KAAK,QAAQ,EACrCA,EAAQ,MAAM,CAElB,CACF,CAEO,MAAMC,CAAwC,CA1KrD,MA0KqD,CAAAJ,EAAA,0BAClC,UAAY,IAAI,IAChB,OACA,QACA,MACA,SACA,WACA,UACA,YACA,qBACA,cACA,aACA,uBACA,YACA,gBACA,UAEA,YAGjB,IAAY,YAAiC,CAC3C,OAAO,KAAK,YAAY,UAC1B,CAEQ,OAAuB,CAAC,EACxB,MAA8C,KAC9C,MAAuB,QAAQ,QAAQ,EAC9B,YAAc,IAAI,IAC3B,MAAQ,GACR,QAAU,GAElB,YAAYC,EAA+BK,EAAmC,CAC5E,KAAK,OAASL,EAAQ,OACtB,KAAK,SAAWA,EAAQ,SAAW,gCAAgC,QAAQ,OAAQ,EAAE,EACrF,KAAK,MAAQA,EAAQ,OAAS,gCAI9B,KAAK,SAAWA,EAAQ,WAAa,OAAS,OAAYA,EAAQ,SAClE,KAAK,WAAaA,EAAQ,YAAc,KACxC,KAAK,UAAYA,EAAQ,WAAa,IACtC,KAAK,YAAcA,EAAQ,aAAe,MAC1C,KAAK,qBAAuBA,EAAQ,sBAAwB,GAC5D,KAAK,cAAgBA,EAAQ,cAC7B,KAAK,aAAeA,EAAQ,aAC5B,KAAK,uBAAyBA,EAAQ,wBAA0B,IAChE,KAAK,YAAcA,EAAQ,YAC3B,KAAK,gBAAkBA,EAAQ,gBAC/B,KAAK,UAAYA,EAAQ,OAAS,MAClC,KAAK,YAAcK,CACrB,CAEA,MAAMC,EAAyB,CAC7B,GAAI,KAAK,OAAS,KAAK,QACrB,MAAM,IAAI,MAAM,8BAA8B,EAEhD,KAAK,OAAO,KAAKA,CAAK,EAGlB,KAAK,OAAO,aAAa,KAAK,KAAK,EACvC,KAAK,MAAQ,WAAW,IAAM,KAAK,MAAM,EAAG,KAAK,SAAS,CAC5D,CAEA,MAAM,KAAqB,CACrB,KAAK,QACT,KAAK,MAAQ,GACT,KAAK,QACP,aAAa,KAAK,KAAK,EACvB,KAAK,MAAQ,MAGf,KAAK,MAAM,EACX,MAAM,KAAK,MACX,KAAK,KAAK,OAAO,EACnB,CAGA,OAAc,CACZ,GAAI,MAAK,QACT,MAAK,QAAU,GACX,KAAK,QACP,aAAa,KAAK,KAAK,EACvB,KAAK,MAAQ,MAEf,KAAK,OAAS,CAAC,EACf,UAAWC,KAAc,KAAK,YAAaA,EAAW,MAAM,EAC5D,KAAK,KAAK,OAAO,EACnB,CAMA,GAAGC,EAAwBC,EAA0C,CACnE,IAAIC,EAAM,KAAK,UAAU,IAAIF,CAAK,EAC7BE,IACHA,EAAM,IAAI,IACV,KAAK,UAAU,IAAIF,EAAOE,CAAG,GAE/BA,EAAI,IAAID,CAAQ,CAClB,CASQ,OAAc,CACpB,GAAI,KAAK,QAAS,OAClB,MAAMH,EAAQ,KAAK,WAAW,EAC1BA,EAAM,aAAe,IACzB,KAAK,MAAQ,KAAK,MAAM,KAAK,SAAY,CACnC,KAAK,SACT,MAAM,KAAK,WAAWA,CAAK,CAC7B,CAAC,EACH,CAEQ,YAAyB,CAC/B,MAAMK,EAAS,KAAK,OACpB,KAAK,OAAS,CAAC,EACf,IAAIC,EAAQ,EACZ,UAAWC,KAASF,EAAQC,GAASC,EAAM,WAC3C,MAAMC,EAAM,IAAI,WAAWF,CAAK,EAChC,IAAIG,EAAS,EACb,UAAWF,KAASF,EAClBG,EAAI,IAAID,EAAOE,CAAM,EACrBA,GAAUF,EAAM,WAElB,OAAOC,CACT,CAEA,MAAc,WAAWR,EAAkC,CAIzD,GAAI,KAAK,cAAgB,QAAU,KAAK,aAAe,QAAa,KAAK,cAAe,CACtF,MAAMU,EAAMC,EAAOX,CAAK,EAClBY,EAAc,KAAK,aAAe,QAAaF,GAAO,KAAK,WAEjE,GADA,KAAK,eAAeA,EAAKE,CAAW,EAChC,CAACA,EAAa,MACpB,CACA,MAAMX,EAAa,IAAI,gBACvB,KAAK,YAAY,IAAIA,CAAU,EAK/B,IAAIY,EAAW,GACf,GAAI,CACF,MAAMC,EAAM,MAAMC,EAChBC,EAAe,CACb,OAAQ,KAAK,OACb,QAAS,KAAK,QACd,MAAO,KAAK,MACZ,SAAU,KAAK,SACf,WAAY,KAAK,WACjB,YAAa,KAAK,YAClB,YAAa,KAAK,YAClB,gBAAiB,KAAK,gBACtB,MAAAhB,EACA,OAAQC,EAAW,OACnB,UAAW,KAAK,SAClB,CAAC,EACD,KAAK,uBACL,IAAM,CACJY,EAAW,GACXZ,EAAW,MAAM,CACnB,CACF,EACMgB,EAAO,KAAK,qBACdC,EAAgBJ,EAAK,KAAK,aAAa,EACvCA,EACA,CAAC,KAAK,SAAWG,GAAM,KAAK,KAAK,QAASA,CAAI,CACpD,OAASE,EAAO,CACd,GAAI,CAAC,KAAK,QAAS,CACjB,MAAMC,EAAUP,EACZ,4CAA4C,KAAK,sBAAsB,KACvEM,aAAiB,MACfA,EAAM,QACN,OAAOA,CAAK,EAClB,KAAK,KAAK,QAAS,IAAI,MAAMC,CAAO,CAAC,CACvC,CACF,QAAE,CACA,KAAK,YAAY,OAAOnB,CAAU,CACpC,CACF,CAMQ,KAAKC,KAA2BmB,EAAmB,CACzD,MAAMjB,EAAM,KAAK,UAAU,IAAIF,CAAK,EACpC,GAAKE,EACL,UAAWD,IAAY,CAAC,GAAGC,CAAG,EAC5BD,EAAS,GAAGkB,CAAI,CAEpB,CACF,CAgBA,MAAMC,EAAoE,CACxE,IAAK,aACL,KAAM,aACN,IAAK,YACL,IAAK,YACL,KAAM,aACN,IAAK,WACP,EAEA,eAAeN,EAAetB,EAAiD,CAC7E,MAAM6B,EAAO,IAAI,SACjBA,EAAK,OAAO,QAAS7B,EAAQ,KAAK,EAIlC,IAAI8B,EACAC,EACAC,EACAhC,EAAQ,cAAgB,OAC1B8B,EAAQG,EAAMjC,EAAQ,MAAOA,EAAQ,UAAU,EAC/C+B,EAAO,YACPC,EAAO,cAEPF,EAAQ9B,EAAQ,MAChB+B,EAAO,SAAS/B,EAAQ,WAAW,GACnCgC,EAAOJ,EAAW5B,EAAQ,WAAW,GAEvC6B,EAAK,OAAO,OAAQ,IAAI,KAAK,CAACC,CAAK,EAAG,CAAE,KAAAE,CAAK,CAAC,EAAGD,CAAI,EACjD/B,EAAQ,UAAU6B,EAAK,OAAO,WAAY7B,EAAQ,QAAQ,EAC1DA,EAAQ,cAAgB,QAC1B6B,EAAK,OAAO,cAAe,OAAO7B,EAAQ,WAAW,CAAC,EAIpDA,EAAQ,iBACV6B,EAAK,OAAO,WAAY,KAAK,UAAU7B,EAAQ,eAAe,CAAC,EAGjE,MAAMkC,EAAW,MAAMlC,EAAQ,UAAU,GAAGA,EAAQ,OAAO,wBAAyB,CAClF,OAAQ,OACR,QAAS,CAAE,cAAe,UAAUA,EAAQ,MAAM,EAAG,EACrD,KAAM6B,EACN,OAAQ7B,EAAQ,MAClB,CAAC,EACD,GAAI,CAACkC,EAAS,GAAI,CAChB,MAAMC,EAAS,MAAMD,EAAS,KAAK,EAAE,MAAM,IAAM,EAAE,EACnD,MAAM,IAAI,MACR,oCAAoCA,EAAS,MAAM,IAAIC,EAAS,KAAKA,CAAM,GAAK,EAAE,EACpF,CACF,CAEA,QADc,MAAMD,EAAS,KAAK,GACrB,MAAQ,IAAI,KAAK,CAChC,CA3CenC,EAAAuB,EAAA,kBAkDf,SAASD,EACPe,EACAC,EACAC,EACY,CACZ,OAAO,IAAI,QAAQ,CAACC,EAASC,IAAW,CACtC,MAAMC,EAAQ,WAAW,IAAM,CAC7BH,EAAU,EACVE,EAAO,IAAI,MAAM,mBAAmBH,CAAE,IAAI,CAAC,CAC7C,EAAGA,CAAE,EACLD,EAAQ,KACLhC,GAAU,CACT,aAAaqC,CAAK,EAClBF,EAAQnC,CAAK,CACf,EACCqB,GAAU,CACT,aAAagB,CAAK,EAClBD,EAAOf,CAAK,CACd,CACF,CACF,CAAC,CACH,CArBS1B,EAAAsB,EAAA,eA2BF,SAASY,EAAMS,EAAiBC,EAAgC,CAGrE,MAAMC,EAAWD,EAAa,EAAW,EACnCE,EAAa,EACbC,EAAWJ,EAAI,WAEfK,EAAM,IAAI,WAAW,GAAKD,CAAQ,EAClCE,EAAO,IAAI,SAASD,EAAI,MAAM,EACpC,OAAAA,EAAI,IAAI,CAAC,GAAM,GAAM,GAAM,EAAI,EAAG,CAAC,EACnCC,EAAK,UAAU,EAAG,GAAKF,EAAU,EAAI,EACrCC,EAAI,IAAI,CAAC,GAAM,GAAM,GAAM,EAAI,EAAG,CAAC,EACnCA,EAAI,IAAI,CAAC,IAAM,IAAM,IAAM,EAAI,EAAG,EAAE,EACpCC,EAAK,UAAU,GAAI,GAAI,EAAI,EAC3BA,EAAK,UAAU,GAAI,EAAG,EAAI,EAC1BA,EAAK,UAAU,GAAI,EAAU,EAAI,EACjCA,EAAK,UAAU,GAAIL,EAAY,EAAI,EACnCK,EAAK,UAAU,GAAIJ,EAAU,EAAI,EACjCI,EAAK,UAAU,GAAIH,EAAY,EAAI,EACnCG,EAAK,UAAU,GAAI,GAAoB,EAAI,EAC3CD,EAAI,IAAI,CAAC,IAAM,GAAM,IAAM,EAAI,EAAG,EAAE,EACpCC,EAAK,UAAU,GAAIF,EAAU,EAAI,EACjCC,EAAI,IAAIL,EAAK,EAAE,EACRK,CACT,CAxBgBhD,EAAAkC,EAAA,SA8BT,SAAShB,EAAOyB,EAAyB,CAC9C,MAAMO,EAAc,KAAK,MAAMP,EAAI,WAAa,CAAC,EACjD,GAAIO,IAAgB,EAAG,MAAO,GAC9B,IAAIC,EAAM,EACV,QAASC,EAAI,EAAGA,EAAIF,EAAaE,IAAK,CACpC,MAAMC,EAAKV,EAAIS,EAAI,CAAC,EACdE,EAAKX,EAAIS,EAAI,EAAI,CAAC,EAClBG,EAASF,EAAMC,GAAM,EACrBE,EAASD,GAAU,MAASA,EAAS,MAAUA,EACrDJ,GAAOK,EAASA,CAClB,CACA,OAAO,KAAK,KAAKL,EAAMD,CAAW,EAAI,KACxC,CAZgBlD,EAAAkB,EAAA,UAsBhB,MAAMuC,EAAiB,CAErB,YACA,SACA,yBACA,sBACA,mBACA,UACA,UACA,MACA,aACA,aACA,WACA,iBACA,aACA,mBACA,gBACA,UACA,OACA,KAEA,UACA,OACA,KACA,SACA,MACA,WACA,WACA,WACA,YACA,SACA,QACA,QACA,UACA,YACA,UACA,SACA,YAEA,OACA,aACA,UACA,OACA,WACA,QAEA,UACA,QACA,WACA,YAEA,QACA,QACA,aACA,SACF,EAQA,SAASC,EAAalC,EAAcmC,EAA4B,CAC9D,MAAMC,EAAapC,EAChB,YAAY,EACZ,QAAQ,aAAc,GAAG,EACzB,QAAQ,OAAQ,GAAG,EACnB,KAAK,EACR,GAAIoC,EAAW,SAAW,EAAG,MAAO,GACpC,MAAMC,EAAS,CAAC,GAAGF,CAAO,EAAE,KAAK,CAAC,EAAGG,IAAMA,EAAE,OAAS,EAAE,MAAM,EAC9D,IAAIC,EAAOH,EACPI,EAAU,GACd,OAAS,CACP,MAAMC,EAAQJ,EAAO,KAAMK,GAAWH,EAAK,WAAWG,CAAM,CAAC,EAI7D,GAHI,CAACD,IACLD,EAAU,GACVD,EAAOA,EAAK,MAAME,EAAM,MAAM,EAAE,QAAQ,OAAQ,EAAE,EAC9CF,EAAK,SAAW,GAAG,KACzB,CACA,OAAOC,GAAWD,EAAK,SAAW,CACpC,CAlBS/D,EAAA0D,EAAA,gBA+BF,SAASjC,EAAgBD,EAAc2C,EAAyB,CAAC,EAAW,CACjF,IAAIpD,EAAMS,EACP,QAAQ,aAAc,GAAG,EACzB,QAAQ,OAAQ,GAAG,EACnB,KAAK,EACR,GAAIT,EAAI,SAAW,EAAG,MAAO,GAE7B,MAAMqD,EAAYrD,EAAI,MAAM,eAAe,EAAE,OAAQsD,GAAaA,EAAS,OAAS,CAAC,EAC/EC,EAAsB,CAAC,EAC7B,UAAWD,KAAYD,EAAW,CAChC,MAAMG,EAAOD,EAAU,GAAG,EAAE,EACxBC,IAAS,QAAaA,EAAK,YAAY,IAAMF,EAAS,YAAY,GACtEC,EAAU,KAAKD,CAAQ,CACzB,CAEA,GADAtD,EAAMuD,EAAU,KAAK,GAAG,EAAE,KAAK,EAC3BvD,EAAI,SAAW,EAAG,MAAO,GAE7B,MAAM4C,EAAU,CAAC,GAAGF,EAAgB,GAAGU,EAAa,IAAKD,GAAWA,EAAO,YAAY,CAAC,CAAC,EACnFN,EAAa7C,EAAI,YAAY,EAAE,QAAQ,WAAY,EAAE,EAC3D,OAAO0C,EAAe,SAASG,CAAU,GAAKF,EAAa3C,EAAK4C,CAAO,EAAI,GAAK5C,CAClF,CApBgBf,EAAAyB,EAAA",
6
+ "names": ["OpenRouterSTT", "__name", "options", "merged", "session", "OpenRouterSession", "value", "liveOptions", "audio", "controller", "event", "listener", "set", "chunks", "total", "chunk", "out", "offset", "rms", "pcmRms", "transcribed", "timedOut", "raw", "withTimeout", "transcribeClip", "text", "cleanTranscript", "error", "message", "args", "AUDIO_MIME", "form", "bytes", "name", "type", "toWav", "response", "detail", "promise", "ms", "onTimeout", "resolve", "reject", "timer", "pcm", "sampleRate", "byteRate", "blockAlign", "dataSize", "wav", "view", "sampleCount", "sum", "i", "lo", "hi", "sample", "signed", "FILLER_PHRASES", "isFillerOnly", "phrases", "normalized", "sorted", "b", "rest", "matched", "found", "phrase", "extraFillers", "sentences", "sentence", "collapsed", "last"]
7
7
  }
@@ -1,5 +1,5 @@
1
- var A=Object.defineProperty;var s=(r,e)=>A(r,"name",{value:e,configurable:!0});class T{static{s(this,"KokoroTTS")}baseUrl;voice;model;apiKey;chunkSize;streaming;fetchImpl;abort=null;constructor(e={}){this.baseUrl=(e.baseUrl??"http://localhost:8880").replace(/\/+$/,""),this.voice=e.voice??"af_heart",this.model=e.model??"kokoro",this.apiKey=e.apiKey,this.chunkSize=e.chunkSize??8192,this.streaming=e.stream??!1,this.fetchImpl=e.fetch??fetch}stop(){this.abort?.abort()}async*stream(e){if(!e.text)throw new Error("KokoroTTS requires request.text");this.abort?.abort();const t=new AbortController;this.abort=t;try{const o={model:this.model,input:e.text,voice:e.voice??this.voice,speed:e.speed};this.streaming?(o.stream=!0,o.response_format=e.format===void 0||e.format==="pcm"?"raw":e.format):e.format!==void 0&&(o.response_format=e.format);const n=await this.fetchImpl(`${this.baseUrl}/v1/audio/speech`,{method:"POST",headers:{"content-type":"application/json",...this.apiKey?{authorization:`Bearer ${this.apiKey}`}:{}},body:JSON.stringify(o),signal:t.signal});if(!n.ok){const i=await n.text();throw new Error(`Kokoro request failed (${n.status}): ${i}`)}if(!n.body)throw new Error("Kokoro returned an empty body");const c=n.body.getReader(),b=(n.headers.get("content-type")??"").includes("text/event-stream");let a=new Uint8Array(0);if(b){let i="",l=!1;for(;!l;){if(t.signal.aborted)throw new DOMException("The operation was aborted.","AbortError");const{done:m,value:g}=await c.read();if(m)break;i+=new TextDecoder().decode(g);const d=i.split(`
1
+ var A=Object.defineProperty;var s=(r,e)=>A(r,"name",{value:e,configurable:!0});class T{static{s(this,"KokoroTTS")}baseUrl;voice;model;apiKey;chunkSize;streaming;fetchImpl;streams=new Set;constructor(e={}){this.baseUrl=(e.baseUrl??"http://localhost:8880").replace(/\/+$/,""),this.voice=e.voice??"af_heart",this.model=e.model??"kokoro",this.apiKey=e.apiKey,this.chunkSize=e.chunkSize??8192,this.streaming=e.stream??!1,this.fetchImpl=e.fetch??fetch}stop(){for(const e of this.streams)e.abort()}async*stream(e){if(!e.text)throw new Error("KokoroTTS requires request.text");const t=new AbortController;this.streams.add(t);try{const o={model:this.model,input:e.text,voice:e.voice??this.voice,speed:e.speed};this.streaming?(o.stream=!0,o.response_format=e.format===void 0||e.format==="pcm"?"raw":e.format):e.format!==void 0&&(o.response_format=e.format);const i=await this.fetchImpl(`${this.baseUrl}/v1/audio/speech`,{method:"POST",headers:{"content-type":"application/json",...this.apiKey?{authorization:`Bearer ${this.apiKey}`}:{}},body:JSON.stringify(o),signal:t.signal});if(!i.ok){const n=await i.text();throw new Error(`Kokoro request failed (${i.status}): ${n}`)}if(!i.body)throw new Error("Kokoro returned an empty body");const c=i.body.getReader(),m=(i.headers.get("content-type")??"").includes("text/event-stream");let a=new Uint8Array(0);if(m){let n="",l=!1;for(;!l;){if(t.signal.aborted)throw new DOMException("The operation was aborted.","AbortError");const{done:b,value:g}=await c.read();if(b)break;n+=new TextDecoder().decode(g);const d=n.split(`
2
2
 
3
- `);i=d.pop()??"";for(const w of d)for(const h of w.split(`
4
- `)){if(!h.startsWith("data:"))continue;const y=h.slice(5).trim();if(y==="[DONE]"){l=!0;break}try{const f=JSON.parse(y);f.type==="conversation.item.audio_output.delta"&&typeof f.delta=="string"&&(a=yield*u(p(a,v(f.delta)),this.chunkSize))}catch{}}}}else for(;;){if(t.signal.aborted)throw new DOMException("The operation was aborted.","AbortError");const{done:i,value:l}=await c.read();if(i)break;a=yield*u(p(a,l),this.chunkSize)}a.length>0&&(yield a)}finally{this.abort===t&&(this.abort=null)}}}function p(r,e){const t=new Uint8Array(r.length+e.length);return t.set(r),t.set(e,r.length),t}s(p,"concat");function*u(r,e){let t=r;for(;t.length>=e;)yield t.slice(0,e),t=t.slice(e);return t}s(u,"sliceChunks");function v(r){const e=atob(r),t=new Uint8Array(e.length);for(let o=0;o<e.length;o++)t[o]=e.charCodeAt(o);return t}s(v,"base64ToBytes");export{T as KokoroTTS};
3
+ `);n=d.pop()??"";for(const w of d)for(const h of w.split(`
4
+ `)){if(!h.startsWith("data:"))continue;const y=h.slice(5).trim();if(y==="[DONE]"){l=!0;break}try{const f=JSON.parse(y);f.type==="conversation.item.audio_output.delta"&&typeof f.delta=="string"&&(a=yield*u(p(a,v(f.delta)),this.chunkSize))}catch{}}}}else for(;;){if(t.signal.aborted)throw new DOMException("The operation was aborted.","AbortError");const{done:n,value:l}=await c.read();if(n)break;a=yield*u(p(a,l),this.chunkSize)}a.length>0&&(yield a)}finally{this.streams.delete(t)}}}function p(r,e){const t=new Uint8Array(r.length+e.length);return t.set(r),t.set(e,r.length),t}s(p,"concat");function*u(r,e){let t=r;for(;t.length>=e;)yield t.slice(0,e),t=t.slice(e);return t}s(u,"sliceChunks");function v(r){const e=atob(r),t=new Uint8Array(e.length);for(let o=0;o<e.length;o++)t[o]=e.charCodeAt(o);return t}s(v,"base64ToBytes");export{T as KokoroTTS};
5
5
  //# sourceMappingURL=kokoro.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/providers/tts/adapters/kokoro/kokoro.ts"],
4
- "sourcesContent": ["import type { TTS, TTSRequest } from \"../../types\";\nimport type { FetchLike } from \"../../../shared\";\n\nexport interface KokoroOptions {\n /**\n * Base URL of a Kokoro TTS server exposing the OpenAI-compatible\n * `/v1/audio/speech` endpoint. Defaults to a local kokoro-fastapi server.\n * For Together AI use `https://api.together.ai`.\n */\n baseUrl?: string;\n /** Default voice, e.g. `af_heart`. */\n voice?: string;\n /**\n * Model name. Defaults to `kokoro` (local kokoro-fastapi); Together AI\n * serves the model as `hexgrad/Kokoro-82M`.\n */\n model?: string;\n apiKey?: string;\n /** Size of the audio chunks yielded from the response stream. */\n chunkSize?: number;\n /**\n * Ask the server for progressive (streaming) synthesis. Local\n * kokoro-fastapi streams by default; hosted endpoints such as Together AI\n * require this flag. Together AI responds with server-sent events carrying\n * base64-encoded audio deltas (pcm_s16le, 24 kHz for Kokoro) terminated by\n * `data: [DONE]`; the adapter decodes them into raw audio chunks. wav/mp3\n * formats are not streamable.\n */\n stream?: boolean;\n /** Injectable fetch implementation, mainly for tests. */\n fetch?: FetchLike;\n}\n\n/**\n * Kokoro TTS adapter. Consumes a Kokoro server's OpenAI-compatible speech\n * endpoint (local kokoro-fastapi or a hosted service such as Together AI)\n * and re-chunks the returned audio so it can be played as it arrives.\n */\nexport class KokoroTTS implements TTS {\n private readonly baseUrl: string;\n private readonly voice: string;\n private readonly model: string;\n private readonly apiKey: string | undefined;\n private readonly chunkSize: number;\n private readonly streaming: boolean;\n private readonly fetchImpl: FetchLike;\n private abort: AbortController | null = null;\n\n constructor(options: KokoroOptions = {}) {\n this.baseUrl = (options.baseUrl ?? \"http://localhost:8880\").replace(/\\/+$/, \"\");\n this.voice = options.voice ?? \"af_heart\";\n this.model = options.model ?? \"kokoro\";\n this.apiKey = options.apiKey;\n this.chunkSize = options.chunkSize ?? 8192;\n this.streaming = options.stream ?? false;\n this.fetchImpl = options.fetch ?? fetch;\n }\n\n stop(): void {\n this.abort?.abort();\n }\n\n async *stream(request: TTSRequest): AsyncGenerator<Uint8Array> {\n if (!request.text) {\n throw new Error(\"KokoroTTS requires request.text\");\n }\n\n this.abort?.abort();\n const controller = new AbortController();\n this.abort = controller;\n\n try {\n const body: Record<string, unknown> = {\n model: this.model,\n input: request.text,\n voice: request.voice ?? this.voice,\n speed: request.speed,\n };\n if (this.streaming) {\n // Streaming synthesis: hosted endpoints (Together AI) only accept\n // raw audio; `pcm` requests map to it. wav/mp3 pass through and are\n // rejected server-side when not streamable.\n body.stream = true;\n body.response_format =\n request.format === undefined || request.format === \"pcm\"\n ? \"raw\"\n : request.format;\n } else if (request.format !== undefined) {\n body.response_format = request.format;\n }\n\n const response = await this.fetchImpl(`${this.baseUrl}/v1/audio/speech`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n ...(this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : {}),\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n\n if (!response.ok) {\n const body = await response.text();\n throw new Error(`Kokoro request failed (${response.status}): ${body}`);\n }\n if (!response.body) {\n throw new Error(\"Kokoro returned an empty body\");\n }\n\n const reader = response.body.getReader();\n const isSse = (response.headers.get(\"content-type\") ?? \"\").includes(\n \"text/event-stream\",\n );\n let audioBuffer: Uint8Array<ArrayBuffer> = new Uint8Array(0);\n\n if (isSse) {\n // Together AI streams server-sent events with base64-encoded audio\n // deltas; the stream ends with `data: [DONE]`.\n let textBuffer = \"\";\n let doneEvent = false;\n while (!doneEvent) {\n if (controller.signal.aborted) {\n throw new DOMException(\"The operation was aborted.\", \"AbortError\");\n }\n const { done, value } = await reader.read();\n if (done) break;\n textBuffer += new TextDecoder().decode(value);\n const events = textBuffer.split(\"\\n\\n\");\n textBuffer = events.pop() ?? \"\";\n for (const event of events) {\n for (const line of event.split(\"\\n\")) {\n if (!line.startsWith(\"data:\")) continue;\n const data = line.slice(5).trim();\n if (data === \"[DONE]\") {\n doneEvent = true;\n break;\n }\n try {\n const parsed = JSON.parse(data) as {\n type?: string;\n delta?: string;\n };\n if (\n parsed.type === \"conversation.item.audio_output.delta\" &&\n typeof parsed.delta === \"string\"\n ) {\n audioBuffer = yield* sliceChunks(\n concat(audioBuffer, base64ToBytes(parsed.delta)),\n this.chunkSize,\n );\n }\n } catch {\n // Ignore malformed events.\n }\n }\n }\n }\n } else {\n while (true) {\n if (controller.signal.aborted) {\n throw new DOMException(\"The operation was aborted.\", \"AbortError\");\n }\n const { done, value } = await reader.read();\n if (done) break;\n audioBuffer = yield* sliceChunks(concat(audioBuffer, value), this.chunkSize);\n }\n }\n\n if (audioBuffer.length > 0) {\n yield audioBuffer;\n }\n } finally {\n if (this.abort === controller) {\n this.abort = null;\n }\n }\n }\n}\n\nfunction concat(\n a: Uint8Array<ArrayBufferLike>,\n b: Uint8Array<ArrayBufferLike>,\n): Uint8Array<ArrayBuffer> {\n const out = new Uint8Array(a.length + b.length);\n out.set(a);\n out.set(b, a.length);\n return out;\n}\n\n/**\n * Yield up to `chunkSize` bytes from `audio`, returning the remainder for\n * the caller to keep buffering. Providers deliver arbitrary chunk\n * boundaries, so audio is re-sliced into uniform pieces.\n */\nfunction* sliceChunks(\n audio: Uint8Array<ArrayBuffer>,\n chunkSize: number,\n): Generator<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>, undefined> {\n let buffer = audio;\n while (buffer.length >= chunkSize) {\n yield buffer.slice(0, chunkSize);\n buffer = buffer.slice(chunkSize);\n }\n return buffer;\n}\n\nfunction base64ToBytes(base64: string): Uint8Array<ArrayBuffer> {\n const binary = atob(base64);\n const out = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n out[i] = binary.charCodeAt(i);\n }\n return out;\n}\n"],
5
- "mappings": "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",
6
- "names": ["KokoroTTS", "__name", "options", "request", "controller", "body", "response", "reader", "isSse", "audioBuffer", "textBuffer", "doneEvent", "done", "value", "events", "event", "line", "data", "parsed", "sliceChunks", "concat", "base64ToBytes", "a", "b", "out", "audio", "chunkSize", "buffer", "base64", "binary", "i"]
4
+ "sourcesContent": ["import type { TTS, TTSRequest } from \"../../types\";\nimport type { FetchLike } from \"../../../shared\";\n\nexport interface KokoroOptions {\n /**\n * Base URL of a Kokoro TTS server exposing the OpenAI-compatible\n * `/v1/audio/speech` endpoint. Defaults to a local kokoro-fastapi server.\n * For Together AI use `https://api.together.ai`.\n */\n baseUrl?: string;\n /** Default voice, e.g. `af_heart`. */\n voice?: string;\n /**\n * Model name. Defaults to `kokoro` (local kokoro-fastapi); Together AI\n * serves the model as `hexgrad/Kokoro-82M`.\n */\n model?: string;\n apiKey?: string;\n /** Size of the audio chunks yielded from the response stream. */\n chunkSize?: number;\n /**\n * Ask the server for progressive (streaming) synthesis. Local\n * kokoro-fastapi streams by default; hosted endpoints such as Together AI\n * require this flag. Together AI responds with server-sent events carrying\n * base64-encoded audio deltas (pcm_s16le, 24 kHz for Kokoro) terminated by\n * `data: [DONE]`; the adapter decodes them into raw audio chunks. wav/mp3\n * formats are not streamable.\n */\n stream?: boolean;\n /** Injectable fetch implementation, mainly for tests. */\n fetch?: FetchLike;\n}\n\n/**\n * Kokoro TTS adapter. Consumes a Kokoro server's OpenAI-compatible speech\n * endpoint (local kokoro-fastapi or a hosted service such as Together AI)\n * and re-chunks the returned audio so it can be played as it arrives.\n */\nexport class KokoroTTS implements TTS {\n private readonly baseUrl: string;\n private readonly voice: string;\n private readonly model: string;\n private readonly apiKey: string | undefined;\n private readonly chunkSize: number;\n private readonly streaming: boolean;\n private readonly fetchImpl: FetchLike;\n private readonly streams = new Set<AbortController>();\n\n constructor(options: KokoroOptions = {}) {\n this.baseUrl = (options.baseUrl ?? \"http://localhost:8880\").replace(/\\/+$/, \"\");\n this.voice = options.voice ?? \"af_heart\";\n this.model = options.model ?? \"kokoro\";\n this.apiKey = options.apiKey;\n this.chunkSize = options.chunkSize ?? 8192;\n this.streaming = options.stream ?? false;\n this.fetchImpl = options.fetch ?? fetch;\n }\n\n stop(): void {\n // Abort every in-flight synthesis. Streams are tracked individually so\n // concurrent requests (the speech pipeline pre-starts the next sentence\n // while the current one is still streaming) do not cancel each other.\n for (const controller of this.streams) controller.abort();\n }\n\n async *stream(request: TTSRequest): AsyncGenerator<Uint8Array> {\n if (!request.text) {\n throw new Error(\"KokoroTTS requires request.text\");\n }\n\n const controller = new AbortController();\n this.streams.add(controller);\n\n try {\n const body: Record<string, unknown> = {\n model: this.model,\n input: request.text,\n voice: request.voice ?? this.voice,\n speed: request.speed,\n };\n if (this.streaming) {\n // Streaming synthesis: hosted endpoints (Together AI) only accept\n // raw audio; `pcm` requests map to it. wav/mp3 pass through and are\n // rejected server-side when not streamable.\n body.stream = true;\n body.response_format =\n request.format === undefined || request.format === \"pcm\"\n ? \"raw\"\n : request.format;\n } else if (request.format !== undefined) {\n body.response_format = request.format;\n }\n\n const response = await this.fetchImpl(`${this.baseUrl}/v1/audio/speech`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n ...(this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : {}),\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n\n if (!response.ok) {\n const body = await response.text();\n throw new Error(`Kokoro request failed (${response.status}): ${body}`);\n }\n if (!response.body) {\n throw new Error(\"Kokoro returned an empty body\");\n }\n\n const reader = response.body.getReader();\n const isSse = (response.headers.get(\"content-type\") ?? \"\").includes(\n \"text/event-stream\",\n );\n let audioBuffer: Uint8Array<ArrayBuffer> = new Uint8Array(0);\n\n if (isSse) {\n // Together AI streams server-sent events with base64-encoded audio\n // deltas; the stream ends with `data: [DONE]`.\n let textBuffer = \"\";\n let doneEvent = false;\n while (!doneEvent) {\n if (controller.signal.aborted) {\n throw new DOMException(\"The operation was aborted.\", \"AbortError\");\n }\n const { done, value } = await reader.read();\n if (done) break;\n textBuffer += new TextDecoder().decode(value);\n const events = textBuffer.split(\"\\n\\n\");\n textBuffer = events.pop() ?? \"\";\n for (const event of events) {\n for (const line of event.split(\"\\n\")) {\n if (!line.startsWith(\"data:\")) continue;\n const data = line.slice(5).trim();\n if (data === \"[DONE]\") {\n doneEvent = true;\n break;\n }\n try {\n const parsed = JSON.parse(data) as {\n type?: string;\n delta?: string;\n };\n if (\n parsed.type === \"conversation.item.audio_output.delta\" &&\n typeof parsed.delta === \"string\"\n ) {\n audioBuffer = yield* sliceChunks(\n concat(audioBuffer, base64ToBytes(parsed.delta)),\n this.chunkSize,\n );\n }\n } catch {\n // Ignore malformed events.\n }\n }\n }\n }\n } else {\n while (true) {\n if (controller.signal.aborted) {\n throw new DOMException(\"The operation was aborted.\", \"AbortError\");\n }\n const { done, value } = await reader.read();\n if (done) break;\n audioBuffer = yield* sliceChunks(concat(audioBuffer, value), this.chunkSize);\n }\n }\n\n if (audioBuffer.length > 0) {\n yield audioBuffer;\n }\n } finally {\n this.streams.delete(controller);\n }\n }\n}\n\nfunction concat(\n a: Uint8Array<ArrayBufferLike>,\n b: Uint8Array<ArrayBufferLike>,\n): Uint8Array<ArrayBuffer> {\n const out = new Uint8Array(a.length + b.length);\n out.set(a);\n out.set(b, a.length);\n return out;\n}\n\n/**\n * Yield up to `chunkSize` bytes from `audio`, returning the remainder for\n * the caller to keep buffering. Providers deliver arbitrary chunk\n * boundaries, so audio is re-sliced into uniform pieces.\n */\nfunction* sliceChunks(\n audio: Uint8Array<ArrayBuffer>,\n chunkSize: number,\n): Generator<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>, undefined> {\n let buffer = audio;\n while (buffer.length >= chunkSize) {\n yield buffer.slice(0, chunkSize);\n buffer = buffer.slice(chunkSize);\n }\n return buffer;\n}\n\nfunction base64ToBytes(base64: string): Uint8Array<ArrayBuffer> {\n const binary = atob(base64);\n const out = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n out[i] = binary.charCodeAt(i);\n }\n return out;\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["KokoroTTS", "__name", "options", "controller", "request", "body", "response", "reader", "isSse", "audioBuffer", "textBuffer", "doneEvent", "done", "value", "events", "event", "line", "data", "parsed", "sliceChunks", "concat", "base64ToBytes", "a", "b", "out", "audio", "chunkSize", "buffer", "base64", "binary", "i"]
7
7
  }
@@ -1,2 +1,2 @@
1
- var c=Object.defineProperty;var a=(t,e)=>c(t,"name",{value:e,configurable:!0});class d{static{a(this,"OpenRouterTTS")}apiKey;baseUrl;model;voice;chunkSize;fetchImpl;abort=null;constructor(e){if(!e.apiKey)throw new Error("OpenRouterTTS requires an apiKey");this.apiKey=e.apiKey,this.baseUrl=(e.baseUrl??"https://openrouter.ai/api/v1").replace(/\/+$/,""),this.model=e.model??"fish-audio/s2.1-pro-free:free",this.voice=e.voice??"",this.chunkSize=e.chunkSize??8192,this.fetchImpl=e.fetch??fetch}stop(){this.abort?.abort()}async*stream(e){if(!e.text)throw new Error("OpenRouterTTS requires request.text");this.abort?.abort();const r=new AbortController;this.abort=r;try{const s={model:this.model,input:e.text,...e.voice??this.voice?{voice:e.voice??this.voice}:{},response_format:e.format==="mp3"?"mp3":"pcm"};e.speed!==void 0&&(s.speed=e.speed);const i=await this.fetchImpl(`${this.baseUrl}/audio/speech`,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${this.apiKey}`},body:JSON.stringify(s),signal:r.signal});if(!i.ok){const o=await i.text().catch(()=>"");throw new Error(`OpenRouter TTS failed (${i.status})${o?`: ${o}`:""}`)}if(!i.body)throw new Error("OpenRouter returned an empty body");const l=i.body.getReader();let n=new Uint8Array(0);for(;;){if(r.signal.aborted)throw new DOMException("The operation was aborted.","AbortError");const{done:o,value:h}=await l.read();if(o)break;n=yield*f(p(n,h),this.chunkSize)}n.length>0&&(yield n)}finally{this.abort===r&&(this.abort=null)}}}function p(t,e){const r=new Uint8Array(t.length+e.length);return r.set(t),r.set(e,t.length),r}a(p,"concat");function*f(t,e){let r=t;for(;r.length>=e;)yield r.slice(0,e),r=r.slice(e);return r}a(f,"sliceChunks");export{d as OpenRouterTTS};
1
+ var h=Object.defineProperty;var a=(t,e)=>h(t,"name",{value:e,configurable:!0});class y{static{a(this,"OpenRouterTTS")}apiKey;baseUrl;model;voice;format;chunkSize;idleTimeoutMs;fetchImpl;streams=new Set;constructor(e){if(!e.apiKey)throw new Error("OpenRouterTTS requires an apiKey");this.apiKey=e.apiKey,this.baseUrl=(e.baseUrl??"https://openrouter.ai/api/v1").replace(/\/+$/,""),this.model=e.model??"fish-audio/s2.1-pro-free:free",this.voice=e.voice??"",this.format=e.format??"pcm",this.chunkSize=e.chunkSize??8192,this.idleTimeoutMs=e.idleTimeoutMs??15e3,this.fetchImpl=e.fetch??fetch}stop(){for(const e of this.streams)e.abort()}async*stream(e){if(!e.text)throw new Error("OpenRouterTTS requires request.text");const r=new AbortController;this.streams.add(r);let s=!1;try{const n={model:this.model,input:e.text,...e.voice??this.voice?{voice:e.voice??this.voice}:{},response_format:(e.format??this.format)==="mp3"?"mp3":"pcm"};e.speed!==void 0&&(n.speed=e.speed);const i=await d(this.fetchImpl(`${this.baseUrl}/audio/speech`,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${this.apiKey}`},body:JSON.stringify(n),signal:r.signal}),this.idleTimeoutMs,()=>{s=!0,r.abort()});if(!i.ok){const u=await i.text().catch(()=>"");throw new Error(`OpenRouter TTS failed (${i.status})${u?`: ${u}`:""}`)}if(!i.body)throw new Error("OpenRouter returned an empty body");const l=i.body.getReader();let o=new Uint8Array(0);for(;;){if(r.signal.aborted)throw new DOMException("The operation was aborted.","AbortError");const{done:u,value:c}=await d(l.read(),this.idleTimeoutMs,()=>{s=!0,r.abort()});if(u)break;o=yield*p(m(o,c),this.chunkSize)}o.length>0&&(yield o)}catch(n){throw s?new Error(`OpenRouter TTS produced no audio for ${this.idleTimeoutMs}ms`):n}finally{this.streams.delete(r)}}}function d(t,e,r){return new Promise((s,n)=>{let i;const l=a(()=>clearTimeout(i),"cleanup");i=setTimeout(()=>{l(),r(),n(new Error(`timed out after ${e}ms`))},e),t.then(o=>{l(),s(o)},o=>{l(),n(o)})})}a(d,"withTimeout");function m(t,e){const r=new Uint8Array(t.length+e.length);return r.set(t),r.set(e,t.length),r}a(m,"concat");function*p(t,e){let r=t;for(;r.length>=e;)yield r.slice(0,e),r=r.slice(e);return r}a(p,"sliceChunks");export{y as OpenRouterTTS};
2
2
  //# sourceMappingURL=openrouter.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/providers/tts/adapters/openrouter/openrouter.ts"],
4
- "sourcesContent": ["import type { TTS, TTSRequest } from \"../../types\";\nimport type { FetchLike } from \"../../../shared\";\n\nexport interface OpenRouterTTSOptions {\n apiKey: string;\n /** Defaults to `https://openrouter.ai/api/v1`. */\n baseUrl?: string;\n /**\n * TTS model. Defaults to the free `fish-audio/s2.1-pro-free:free` variant \u2014\n * fast, per-character priced. Pass `fish-audio/s2.1-pro` for the paid tier\n * (no free-variant rate limits).\n */\n model?: string;\n /**\n * Default voice, sent when the request carries none. Omitted entirely when\n * unset: providers with a built-in default voice (e.g. fish-audio) accept\n * omission, and some reject an explicit `voice` outright \u2014 while providers\n * that require one (e.g. OpenAI TTS) need it set. Voice support varies by\n * model and provider.\n */\n voice?: string;\n /** Size of the audio chunks yielded from the response stream. Default 8192. */\n chunkSize?: number;\n /** Injectable fetch, mainly for tests. */\n fetch?: FetchLike;\n}\n\n/**\n * OpenRouter TTS adapter (`POST /api/v1/audio/speech`, OpenAI-compatible).\n *\n * The endpoint returns raw audio bytes (not JSON) in `mp3` or `pcm` format.\n * The adapter re-chunks the response stream so audio can be played as it\n * arrives; `stop()` aborts the in-flight synthesis. `pcm` is the default\n * output (OpenRouter's own recommendation for realtime pipelines).\n */\nexport class OpenRouterTTS implements TTS {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly model: string;\n private readonly voice: string;\n private readonly chunkSize: number;\n private readonly fetchImpl: FetchLike;\n private abort: AbortController | null = null;\n\n constructor(options: OpenRouterTTSOptions) {\n if (!options.apiKey) {\n throw new Error(\"OpenRouterTTS requires an apiKey\");\n }\n this.apiKey = options.apiKey;\n this.baseUrl = (options.baseUrl ?? \"https://openrouter.ai/api/v1\").replace(/\\/+$/, \"\");\n this.model = options.model ?? \"fish-audio/s2.1-pro-free:free\";\n this.voice = options.voice ?? \"\";\n this.chunkSize = options.chunkSize ?? 8192;\n this.fetchImpl = options.fetch ?? fetch;\n }\n\n stop(): void {\n this.abort?.abort();\n }\n\n async *stream(request: TTSRequest): AsyncGenerator<Uint8Array> {\n if (!request.text) {\n throw new Error(\"OpenRouterTTS requires request.text\");\n }\n\n this.abort?.abort();\n const controller = new AbortController();\n this.abort = controller;\n\n try {\n const body: Record<string, unknown> = {\n model: this.model,\n input: request.text,\n // Voice is only sent when configured: fish-audio (the default model)\n // rejects an explicit voice with a 400, while models like OpenAI TTS\n // require one.\n ...(request.voice ?? this.voice ? { voice: request.voice ?? this.voice } : {}),\n // OpenRouter supports mp3 and pcm only; anything else (or\n // unspecified) maps to pcm, the lower-latency realtime format.\n response_format: request.format === \"mp3\" ? \"mp3\" : \"pcm\",\n };\n if (request.speed !== undefined) body.speed = request.speed;\n\n const response = await this.fetchImpl(`${this.baseUrl}/audio/speech`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n\n if (!response.ok) {\n // Non-200 responses carry a JSON error body, not audio.\n const detail = await response.text().catch(() => \"\");\n throw new Error(\n `OpenRouter TTS failed (${response.status})${detail ? `: ${detail}` : \"\"}`,\n );\n }\n if (!response.body) {\n throw new Error(\"OpenRouter returned an empty body\");\n }\n\n const reader = response.body.getReader();\n let buffer = new Uint8Array(0);\n while (true) {\n if (controller.signal.aborted) {\n throw new DOMException(\"The operation was aborted.\", \"AbortError\");\n }\n const { done, value } = await reader.read();\n if (done) break;\n buffer = yield* sliceChunks(concat(buffer, value), this.chunkSize);\n }\n if (buffer.length > 0) {\n yield buffer;\n }\n } finally {\n if (this.abort === controller) {\n this.abort = null;\n }\n }\n }\n}\n\nfunction concat(\n a: Uint8Array<ArrayBufferLike>,\n b: Uint8Array<ArrayBufferLike>,\n): Uint8Array<ArrayBuffer> {\n const out = new Uint8Array(a.length + b.length);\n out.set(a);\n out.set(b, a.length);\n return out;\n}\n\n/**\n * Yield up to `chunkSize` bytes from `audio`, returning the remainder for\n * the caller to keep buffering. Providers deliver arbitrary chunk\n * boundaries, so audio is re-sliced into uniform pieces.\n */\nfunction* sliceChunks(\n audio: Uint8Array<ArrayBuffer>,\n chunkSize: number,\n): Generator<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>, undefined> {\n let buffer = audio;\n while (buffer.length >= chunkSize) {\n yield buffer.slice(0, chunkSize);\n buffer = buffer.slice(chunkSize);\n }\n return buffer;\n}\n"],
5
- "mappings": "+EAmCO,MAAMA,CAA6B,CAnC1C,MAmC0C,CAAAC,EAAA,sBACvB,OACA,QACA,MACA,MACA,UACA,UACT,MAAgC,KAExC,YAAYC,EAA+B,CACzC,GAAI,CAACA,EAAQ,OACX,MAAM,IAAI,MAAM,kCAAkC,EAEpD,KAAK,OAASA,EAAQ,OACtB,KAAK,SAAWA,EAAQ,SAAW,gCAAgC,QAAQ,OAAQ,EAAE,EACrF,KAAK,MAAQA,EAAQ,OAAS,gCAC9B,KAAK,MAAQA,EAAQ,OAAS,GAC9B,KAAK,UAAYA,EAAQ,WAAa,KACtC,KAAK,UAAYA,EAAQ,OAAS,KACpC,CAEA,MAAa,CACX,KAAK,OAAO,MAAM,CACpB,CAEA,MAAO,OAAOC,EAAiD,CAC7D,GAAI,CAACA,EAAQ,KACX,MAAM,IAAI,MAAM,qCAAqC,EAGvD,KAAK,OAAO,MAAM,EAClB,MAAMC,EAAa,IAAI,gBACvB,KAAK,MAAQA,EAEb,GAAI,CACF,MAAMC,EAAgC,CACpC,MAAO,KAAK,MACZ,MAAOF,EAAQ,KAIf,GAAIA,EAAQ,OAAS,KAAK,MAAQ,CAAE,MAAOA,EAAQ,OAAS,KAAK,KAAM,EAAI,CAAC,EAG5E,gBAAiBA,EAAQ,SAAW,MAAQ,MAAQ,KACtD,EACIA,EAAQ,QAAU,SAAWE,EAAK,MAAQF,EAAQ,OAEtD,MAAMG,EAAW,MAAM,KAAK,UAAU,GAAG,KAAK,OAAO,gBAAiB,CACpE,OAAQ,OACR,QAAS,CACP,eAAgB,mBAChB,cAAe,UAAU,KAAK,MAAM,EACtC,EACA,KAAM,KAAK,UAAUD,CAAI,EACzB,OAAQD,EAAW,MACrB,CAAC,EAED,GAAI,CAACE,EAAS,GAAI,CAEhB,MAAMC,EAAS,MAAMD,EAAS,KAAK,EAAE,MAAM,IAAM,EAAE,EACnD,MAAM,IAAI,MACR,0BAA0BA,EAAS,MAAM,IAAIC,EAAS,KAAKA,CAAM,GAAK,EAAE,EAC1E,CACF,CACA,GAAI,CAACD,EAAS,KACZ,MAAM,IAAI,MAAM,mCAAmC,EAGrD,MAAME,EAASF,EAAS,KAAK,UAAU,EACvC,IAAIG,EAAS,IAAI,WAAW,CAAC,EAC7B,OAAa,CACX,GAAIL,EAAW,OAAO,QACpB,MAAM,IAAI,aAAa,6BAA8B,YAAY,EAEnE,KAAM,CAAE,KAAAM,EAAM,MAAAC,CAAM,EAAI,MAAMH,EAAO,KAAK,EAC1C,GAAIE,EAAM,MACVD,EAAS,MAAOG,EAAYC,EAAOJ,EAAQE,CAAK,EAAG,KAAK,SAAS,CACnE,CACIF,EAAO,OAAS,IAClB,MAAMA,EAEV,QAAE,CACI,KAAK,QAAUL,IACjB,KAAK,MAAQ,KAEjB,CACF,CACF,CAEA,SAASS,EACPC,EACAC,EACyB,CACzB,MAAMC,EAAM,IAAI,WAAWF,EAAE,OAASC,EAAE,MAAM,EAC9C,OAAAC,EAAI,IAAIF,CAAC,EACTE,EAAI,IAAID,EAAGD,EAAE,MAAM,EACZE,CACT,CARSf,EAAAY,EAAA,UAeT,SAAUD,EACRK,EACAC,EACwE,CACxE,IAAIT,EAASQ,EACb,KAAOR,EAAO,QAAUS,GACtB,MAAMT,EAAO,MAAM,EAAGS,CAAS,EAC/BT,EAASA,EAAO,MAAMS,CAAS,EAEjC,OAAOT,CACT,CAVUR,EAAAW,EAAA",
6
- "names": ["OpenRouterTTS", "__name", "options", "request", "controller", "body", "response", "detail", "reader", "buffer", "done", "value", "sliceChunks", "concat", "a", "b", "out", "audio", "chunkSize"]
4
+ "sourcesContent": ["import type { TTS, TTSRequest } from \"../../types\";\nimport type { FetchLike } from \"../../../shared\";\n\nexport interface OpenRouterTTSOptions {\n apiKey: string;\n /** Defaults to `https://openrouter.ai/api/v1`. */\n baseUrl?: string;\n /**\n * TTS model. Defaults to the free `fish-audio/s2.1-pro-free:free` variant \u2014\n * fast, per-character priced. Pass `fish-audio/s2.1-pro` for the paid tier\n * (no free-variant rate limits). Note the free variant is intermittently\n * unavailable on OpenRouter (404 \"No endpoints found\").\n */\n model?: string;\n /**\n * Default voice, sent when the request carries none. Set it for a\n * consistent voice: the default fish model accepts exactly `\"alloy\"` and\n * rejects other names, and omitting `voice` lets the free variant vary the\n * voice per request. Models like OpenAI TTS require a voice. Voice support\n * varies by model and provider.\n */\n voice?: string;\n /**\n * Output format used when the request doesn't specify one. `\"pcm\"` (the\n * default) is the lowest-latency realtime format, but its sample rate is\n * provider-defined and opaque \u2014 clients must know it. `\"mp3\"` is\n * self-describing: decoders read the real rate from the stream.\n */\n format?: \"pcm\" | \"mp3\";\n /** Size of the audio chunks yielded from the response stream. Default 8192. */\n chunkSize?: number;\n /**\n * Abort a synthesis that delivers no audio bytes for this long (default\n * 15000ms). A provider connection that goes silent \u2014 or never responds \u2014\n * would otherwise wedge the speech pipeline forever, since the pipeline\n * holds one synthesis slot and its delivery chain until the stream ends.\n * Any byte resets the clock; a genuinely slow synthesis just needs a\n * response within this window.\n */\n idleTimeoutMs?: number;\n /** Injectable fetch, mainly for tests. */\n fetch?: FetchLike;\n}\n\n/**\n * OpenRouter TTS adapter (`POST /api/v1/audio/speech`, OpenAI-compatible).\n *\n * The endpoint returns raw audio bytes (not JSON) in `mp3` or `pcm` format.\n * The adapter re-chunks the response stream so audio can be played as it\n * arrives; `stop()` aborts the in-flight synthesis. `pcm` is the default\n * output (OpenRouter's own recommendation for realtime pipelines).\n */\nexport class OpenRouterTTS implements TTS {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly model: string;\n private readonly voice: string;\n private readonly format: \"pcm\" | \"mp3\";\n private readonly chunkSize: number;\n private readonly idleTimeoutMs: number;\n private readonly fetchImpl: FetchLike;\n private readonly streams = new Set<AbortController>();\n\n constructor(options: OpenRouterTTSOptions) {\n if (!options.apiKey) {\n throw new Error(\"OpenRouterTTS requires an apiKey\");\n }\n this.apiKey = options.apiKey;\n this.baseUrl = (options.baseUrl ?? \"https://openrouter.ai/api/v1\").replace(/\\/+$/, \"\");\n this.model = options.model ?? \"fish-audio/s2.1-pro-free:free\";\n this.voice = options.voice ?? \"\";\n this.format = options.format ?? \"pcm\";\n this.chunkSize = options.chunkSize ?? 8192;\n this.idleTimeoutMs = options.idleTimeoutMs ?? 15_000;\n this.fetchImpl = options.fetch ?? fetch;\n }\n\n stop(): void {\n // Abort every in-flight synthesis. Streams are tracked individually so\n // concurrent requests (the speech pipeline pre-starts the next sentence\n // while the current one is still streaming) do not cancel each other.\n for (const controller of this.streams) controller.abort();\n }\n\n async *stream(request: TTSRequest): AsyncGenerator<Uint8Array> {\n if (!request.text) {\n throw new Error(\"OpenRouterTTS requires request.text\");\n }\n\n const controller = new AbortController();\n this.streams.add(controller);\n // Every await is bounded by an idle clock: a request that never responds,\n // or a connection that goes silent mid-stream, aborts with a clear error\n // instead of holding the speech pipeline's synthesis slot and delivery\n // chain open forever (the reply would otherwise wait for the next input).\n let timedOut = false;\n\n try {\n const body: Record<string, unknown> = {\n model: this.model,\n input: request.text,\n // Voice is only sent when configured: fish-audio (the default model)\n // rejects an explicit voice with a 400, while models like OpenAI TTS\n // require one.\n ...(request.voice ?? this.voice ? { voice: request.voice ?? this.voice } : {}),\n // OpenRouter supports mp3 and pcm only; anything else (or\n // unspecified) maps to pcm, the lower-latency realtime format.\n response_format: (request.format ?? this.format) === \"mp3\" ? \"mp3\" : \"pcm\",\n };\n if (request.speed !== undefined) body.speed = request.speed;\n\n const response = await withTimeout(\n this.fetchImpl(`${this.baseUrl}/audio/speech`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n }),\n this.idleTimeoutMs,\n () => {\n timedOut = true;\n controller.abort();\n },\n );\n\n if (!response.ok) {\n // Non-200 responses carry a JSON error body, not audio.\n const detail = await response.text().catch(() => \"\");\n throw new Error(\n `OpenRouter TTS failed (${response.status})${detail ? `: ${detail}` : \"\"}`,\n );\n }\n if (!response.body) {\n throw new Error(\"OpenRouter returned an empty body\");\n }\n\n const reader = response.body.getReader();\n let buffer = new Uint8Array(0);\n while (true) {\n if (controller.signal.aborted) {\n throw new DOMException(\"The operation was aborted.\", \"AbortError\");\n }\n const { done, value } = await withTimeout(\n reader.read(),\n this.idleTimeoutMs,\n () => {\n timedOut = true;\n controller.abort();\n },\n );\n if (done) break;\n buffer = yield* sliceChunks(concat(buffer, value), this.chunkSize);\n }\n if (buffer.length > 0) {\n yield buffer;\n }\n } catch (error) {\n // Translate the watchdog's abort into a clear failure. Real aborts\n // (`stop()`, interrupts) keep their AbortError so callers can tell the\n // difference.\n if (timedOut) {\n throw new Error(`OpenRouter TTS produced no audio for ${this.idleTimeoutMs}ms`);\n }\n throw error;\n } finally {\n this.streams.delete(controller);\n }\n }\n}\n\n/**\n * Race `promise` against a timeout: reject after `ms` and fire `onTimeout`\n * (which should abort the underlying request) so a hanging operation is\n * released rather than left pending forever.\n */\nfunction withTimeout<T>(\n promise: Promise<T>,\n ms: number,\n onTimeout: () => void,\n): Promise<T> {\n return new Promise((resolve, reject) => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const cleanup = () => clearTimeout(timer);\n timer = setTimeout(() => {\n cleanup();\n onTimeout();\n reject(new Error(`timed out after ${ms}ms`));\n }, ms);\n promise.then(\n (value) => {\n cleanup();\n resolve(value);\n },\n (error) => {\n cleanup();\n reject(error);\n },\n );\n });\n}\n\nfunction concat(\n a: Uint8Array<ArrayBufferLike>,\n b: Uint8Array<ArrayBufferLike>,\n): Uint8Array<ArrayBuffer> {\n const out = new Uint8Array(a.length + b.length);\n out.set(a);\n out.set(b, a.length);\n return out;\n}\n\n/**\n * Yield up to `chunkSize` bytes from `audio`, returning the remainder for\n * the caller to keep buffering. Providers deliver arbitrary chunk\n * boundaries, so audio is re-sliced into uniform pieces.\n */\nfunction* sliceChunks(\n audio: Uint8Array<ArrayBuffer>,\n chunkSize: number,\n): Generator<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>, undefined> {\n let buffer = audio;\n while (buffer.length >= chunkSize) {\n yield buffer.slice(0, chunkSize);\n buffer = buffer.slice(chunkSize);\n }\n return buffer;\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["OpenRouterTTS", "__name", "options", "controller", "request", "timedOut", "body", "response", "withTimeout", "detail", "reader", "buffer", "done", "value", "sliceChunks", "concat", "error", "promise", "ms", "onTimeout", "resolve", "reject", "timer", "cleanup", "a", "b", "out", "audio", "chunkSize"]
7
7
  }