@stabgan/openrouter-mcp-multimodal 4.6.0 → 4.6.2

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 (67) hide show
  1. package/README.md +60 -52
  2. package/dist/errors.d.ts +3 -12
  3. package/dist/errors.js +2 -6
  4. package/dist/index.js +11 -11
  5. package/dist/logger.d.ts +1 -18
  6. package/dist/logger.js +0 -7
  7. package/dist/model-cache.d.ts +1 -20
  8. package/dist/model-cache.js +1 -20
  9. package/dist/openrouter-api.d.ts +4 -18
  10. package/dist/openrouter-api.js +4 -20
  11. package/dist/tool-definitions.d.ts +1 -0
  12. package/dist/tool-definitions.js +708 -0
  13. package/dist/tool-descriptions.js +6 -8
  14. package/dist/tool-handlers/analyze-audio.d.ts +0 -5
  15. package/dist/tool-handlers/analyze-image.d.ts +0 -6
  16. package/dist/tool-handlers/analyze-image.js +1 -8
  17. package/dist/tool-handlers/analyze-video.d.ts +0 -5
  18. package/dist/tool-handlers/analyze-video.js +0 -7
  19. package/dist/tool-handlers/async-chat.d.ts +17 -13
  20. package/dist/tool-handlers/async-chat.js +43 -66
  21. package/dist/tool-handlers/audio-utils.d.ts +1 -4
  22. package/dist/tool-handlers/audio-utils.js +4 -14
  23. package/dist/tool-handlers/cache.d.ts +2 -18
  24. package/dist/tool-handlers/cache.js +1 -19
  25. package/dist/tool-handlers/chat-completion.d.ts +2 -30
  26. package/dist/tool-handlers/chat-completion.js +13 -33
  27. package/dist/tool-handlers/chat-request.d.ts +25 -0
  28. package/dist/tool-handlers/chat-request.js +38 -0
  29. package/dist/tool-handlers/completion-utils.d.ts +1 -18
  30. package/dist/tool-handlers/completion-utils.js +0 -2
  31. package/dist/tool-handlers/fetch-utils.d.ts +2 -18
  32. package/dist/tool-handlers/fetch-utils.js +3 -51
  33. package/dist/tool-handlers/generate-audio.js +11 -21
  34. package/dist/tool-handlers/generate-image-dedicated.js +17 -65
  35. package/dist/tool-handlers/generate-image-input.d.ts +0 -1
  36. package/dist/tool-handlers/generate-image-input.js +2 -18
  37. package/dist/tool-handlers/generate-image.js +8 -15
  38. package/dist/tool-handlers/generate-video.d.ts +1 -7
  39. package/dist/tool-handlers/generate-video.js +24 -109
  40. package/dist/tool-handlers/health-check.d.ts +1 -9
  41. package/dist/tool-handlers/health-check.js +1 -12
  42. package/dist/tool-handlers/image-source.d.ts +14 -0
  43. package/dist/tool-handlers/image-source.js +23 -0
  44. package/dist/tool-handlers/image-utils.d.ts +5 -0
  45. package/dist/tool-handlers/image-utils.js +23 -0
  46. package/dist/tool-handlers/openai-withresponse.d.ts +1 -10
  47. package/dist/tool-handlers/openai-withresponse.js +0 -7
  48. package/dist/tool-handlers/openrouter-errors.d.ts +2 -18
  49. package/dist/tool-handlers/openrouter-errors.js +2 -34
  50. package/dist/tool-handlers/path-safety.d.ts +10 -15
  51. package/dist/tool-handlers/path-safety.js +30 -55
  52. package/dist/tool-handlers/provider-routing.d.ts +0 -9
  53. package/dist/tool-handlers/provider-routing.js +1 -14
  54. package/dist/tool-handlers/rerank.js +0 -2
  55. package/dist/tool-handlers/search-models.d.ts +0 -6
  56. package/dist/tool-handlers/speech-to-text.js +19 -29
  57. package/dist/tool-handlers/structured-output.d.ts +1 -4
  58. package/dist/tool-handlers/structured-output.js +2 -16
  59. package/dist/tool-handlers/text-to-speech.js +8 -26
  60. package/dist/tool-handlers/video-utils.d.ts +1 -6
  61. package/dist/tool-handlers/video-utils.js +2 -17
  62. package/dist/tool-handlers.js +9 -730
  63. package/dist/tool-icons.d.ts +9 -0
  64. package/dist/tool-icons.js +52 -0
  65. package/dist/version.d.ts +1 -15
  66. package/dist/version.js +1 -15
  67. package/package.json +3 -2
@@ -1,34 +1,23 @@
1
- /**
2
- * Output-path sandbox. Tools that write files (`generate_image`,
3
- * `generate_audio`, future `generate_video`) route their `save_path`
4
- * through `resolveSafeOutputPath` so an untrusted MCP caller cannot
5
- * traverse outside the configured output root.
6
- *
7
- * Root resolution order:
8
- * 1. `OPENROUTER_OUTPUT_DIR` env var (if set and non-empty).
9
- * 2. `process.cwd()`.
10
- *
11
- * Set `OPENROUTER_ALLOW_UNSAFE_PATHS=1` to disable the sandbox entirely
12
- * (legacy v2 behavior). This is discouraged — document the trade-off
13
- * where it appears in user configs.
14
- */
15
1
  import path from 'node:path';
16
2
  import { promises as fs } from 'node:fs';
17
3
  import os from 'node:os';
4
+ import { ErrorCode, toolErrorFrom } from '../errors.js';
18
5
  /**
19
- * Resolve the output root directory. On Windows, `process.cwd()` can be
20
- * a system directory (e.g. `C:\Windows\System32`) when spawned by MCP
21
- * clients without a working directory override. We detect that case and
22
- * fall back to a writable temp directory to avoid EPERM errors.
6
+ * Path sandboxes for MCP caller-supplied input/output paths.
7
+ * Set `OPENROUTER_ALLOW_UNSAFE_PATHS=1` to disable (legacy v2 behavior).
8
+ *
9
+ * Output paths: walk to the first existing ancestor, realpath it, and reject
10
+ * escapes before mkdir. Re-realpath the parent after mkdir to catch symlink traversal.
11
+ *
12
+ * Input paths: realpath when the file exists; otherwise reject via resolved prefix
13
+ * so `../escape` fails without leaking ENOENT.
23
14
  */
15
+ /** Output root: OPENROUTER_OUTPUT_DIR, or cwd (with Windows system-dir fallback). */
24
16
  function getOutputRoot() {
25
17
  const override = process.env.OPENROUTER_OUTPUT_DIR;
26
18
  if (override && override.length > 0)
27
19
  return path.resolve(override);
28
20
  const cwd = process.cwd();
29
- // On Windows, avoid using system directories as the default output root.
30
- // Common non-writable defaults when MCP clients spawn without a cwd:
31
- // C:\Windows\System32, C:\Windows, C:\Program Files\...
32
21
  if (process.platform === 'win32') {
33
22
  const cwdLower = cwd.toLowerCase().replace(/\\/g, '/');
34
23
  if (cwdLower.startsWith('c:/windows') ||
@@ -51,11 +40,8 @@ export class UnsafeOutputPathError extends Error {
51
40
  }
52
41
  }
53
42
  /**
54
- * Resolve and validate a caller-supplied output path. Creates the parent
55
- * directory if needed. Returns the absolute path that is safe to write.
56
- *
57
- * Throws `UnsafeOutputPathError` when the resolved path escapes the root
58
- * (traversal attempt) and the sandbox is enabled.
43
+ * Resolve and validate a caller-supplied output path.
44
+ * Throws `UnsafeOutputPathError` on traversal attempts.
59
45
  */
60
46
  export async function resolveSafeOutputPath(savePath) {
61
47
  if (isUnsafeMode()) {
@@ -65,26 +51,18 @@ export async function resolveSafeOutputPath(savePath) {
65
51
  }
66
52
  const root = getOutputRoot();
67
53
  const rootReal = await fs.realpath(root).catch(() => path.resolve(root));
68
- // Resolve relative paths against the real root; absolute paths stay as
69
- // given so we can check them against the root prefix below.
70
54
  const candidate = path.isAbsolute(savePath)
71
55
  ? path.resolve(savePath)
72
56
  : path.resolve(rootReal, savePath);
73
- // Walk up from the candidate dir to find the first component that exists
74
- // so we can realpath it. This lets us create new subdirectories under the
75
- // root while still catching symlink-based traversal.
76
57
  const withSep = rootReal.endsWith(path.sep) ? rootReal : rootReal + path.sep;
77
58
  const candidateDir = path.dirname(candidate);
78
59
  const existingAncestor = await findExistingAncestor(candidateDir);
79
60
  const ancestorReal = await fs.realpath(existingAncestor);
80
- // The realpath of the first-existing ancestor MUST be within the root.
81
61
  if (!(ancestorReal === rootReal || ancestorReal.startsWith(withSep))) {
82
62
  throw new UnsafeOutputPathError(`save_path resolves outside OPENROUTER_OUTPUT_DIR (${rootReal}). ` +
83
63
  `Set OPENROUTER_OUTPUT_DIR to a wider root or OPENROUTER_ALLOW_UNSAFE_PATHS=1 to disable this check.`);
84
64
  }
85
- // Safe to create missing intermediate directories now.
86
65
  await fs.mkdir(candidateDir, { recursive: true });
87
- // Re-realpath the final parent in case mkdir traversed a symlink.
88
66
  const parentReal = await fs.realpath(candidateDir);
89
67
  if (!(parentReal === rootReal || parentReal.startsWith(withSep))) {
90
68
  throw new UnsafeOutputPathError(`save_path escapes OPENROUTER_OUTPUT_DIR via symlink (${rootReal}).`);
@@ -106,11 +84,7 @@ async function findExistingAncestor(dir) {
106
84
  }
107
85
  }
108
86
  }
109
- /**
110
- * Root-resolution for caller-supplied INPUT paths. Prefers
111
- * `OPENROUTER_INPUT_DIR`, then `OPENROUTER_OUTPUT_DIR`, then `process.cwd()`.
112
- * On Windows, applies the same system-directory detection as getOutputRoot.
113
- */
87
+ /** Input root: OPENROUTER_INPUT_DIR, then OPENROUTER_OUTPUT_DIR, then cwd. */
114
88
  function getInputRoot() {
115
89
  const inputDir = process.env.OPENROUTER_INPUT_DIR;
116
90
  if (inputDir && inputDir.length > 0)
@@ -129,16 +103,7 @@ function getInputRoot() {
129
103
  }
130
104
  return cwd;
131
105
  }
132
- /**
133
- * Resolve and validate a caller-supplied INPUT path. Unlike
134
- * `resolveSafeOutputPath`, this never creates directories — it only
135
- * confirms the path lives inside the input sandbox and returns the
136
- * absolute path the caller can `fs.readFile` from.
137
- *
138
- * Accepts the same `OPENROUTER_ALLOW_UNSAFE_PATHS=1` legacy bypass.
139
- * Throws `UnsafeOutputPathError` on traversal attempts (re-used type so
140
- * handlers map errors uniformly to `ErrorCode.UNSAFE_PATH`).
141
- */
106
+ /** Resolve and validate a caller-supplied input path (read-only, no mkdir). */
142
107
  export async function resolveSafeInputPath(inputPath) {
143
108
  if (isUnsafeMode()) {
144
109
  return path.resolve(inputPath);
@@ -149,12 +114,6 @@ export async function resolveSafeInputPath(inputPath) {
149
114
  const abs = path.isAbsolute(inputPath)
150
115
  ? path.resolve(inputPath)
151
116
  : path.resolve(rootReal, inputPath);
152
- // Prefer realpath for the prefix check so callers can pass paths
153
- // through symlinks (e.g. macOS `/var/...` → `/private/var/...`)
154
- // without us rejecting them. If the file doesn't exist yet, fall
155
- // back to a textual check on the resolved path so traversal
156
- // (`../escape.png`) is still rejected with the right error type
157
- // instead of leaking an ENOENT to the caller.
158
117
  let canonical;
159
118
  try {
160
119
  canonical = await fs.realpath(abs);
@@ -167,3 +126,19 @@ export async function resolveSafeInputPath(inputPath) {
167
126
  }
168
127
  return abs;
169
128
  }
129
+ export function isToolErrorResult(result) {
130
+ return 'isError' in result;
131
+ }
132
+ /** Resolve optional save_path; returns a tool error result on sandbox violation. */
133
+ export async function resolveOptionalOutputPath(savePath) {
134
+ if (!savePath)
135
+ return { path: null };
136
+ try {
137
+ return { path: await resolveSafeOutputPath(savePath) };
138
+ }
139
+ catch (err) {
140
+ if (err instanceof UnsafeOutputPathError)
141
+ return toolErrorFrom(ErrorCode.UNSAFE_PATH, err);
142
+ return toolErrorFrom(ErrorCode.INTERNAL, err);
143
+ }
144
+ }
@@ -1,12 +1,3 @@
1
- /**
2
- * OpenRouter provider routing. Merges caller-supplied `provider` args on
3
- * top of env-var defaults and emits the `provider` object that goes into
4
- * `POST /chat/completions`. See
5
- * https://openrouter.ai/docs/features/provider-routing for the spec.
6
- *
7
- * Precedence: explicit tool arg > env var > unset. Empty arrays / empty
8
- * objects are dropped so we don't send noise to the API.
9
- */
10
1
  export type ProviderSort = 'price' | 'throughput' | 'latency';
11
2
  export type DataCollectionPolicy = 'allow' | 'deny';
12
3
  export interface ProviderRoutingOptions {
@@ -1,11 +1,5 @@
1
1
  /**
2
- * OpenRouter provider routing. Merges caller-supplied `provider` args on
3
- * top of env-var defaults and emits the `provider` object that goes into
4
- * `POST /chat/completions`. See
5
- * https://openrouter.ai/docs/features/provider-routing for the spec.
6
- *
7
- * Precedence: explicit tool arg > env var > unset. Empty arrays / empty
8
- * objects are dropped so we don't send noise to the API.
2
+ * OpenRouter provider routing — merges tool args over OPENROUTER_PROVIDER_* env defaults.
9
3
  */
10
4
  import { logger } from '../logger.js';
11
5
  function parseCsv(raw) {
@@ -21,9 +15,6 @@ function parseJsonArray(raw, name) {
21
15
  if (!raw)
22
16
  return undefined;
23
17
  const trimmed = raw.trim();
24
- // If the value looks like a JSON array (`[...]`), require it to BE valid JSON.
25
- // Otherwise fall back to CSV parsing — that way `a,b,c` works too, but a
26
- // malformed `[bogus]` doesn't silently become a single-element string array.
27
18
  if (trimmed.startsWith('[')) {
28
19
  try {
29
20
  const parsed = JSON.parse(trimmed);
@@ -84,10 +75,6 @@ export function readProviderDefaults() {
84
75
  out.order = order;
85
76
  }
86
77
  catch (err) {
87
- // Don't crash the server on a malformed env var — log once so an
88
- // operator notices instead of wondering why their ordering is being
89
- // ignored. All other OPENROUTER_PROVIDER_* fields follow the same
90
- // "silent drop" policy for consistency.
91
78
  logger.warn('OPENROUTER_PROVIDER_ORDER ignored', {
92
79
  err: err instanceof Error ? err.message : String(err),
93
80
  });
@@ -26,8 +26,6 @@ export async function handleRerankDocuments(request, apiClient) {
26
26
  catch (err) {
27
27
  return classifyUpstreamError(err, 'rerank');
28
28
  }
29
- // Normalize to a stable shape: always expose `score` (OpenRouter
30
- // providers sometimes return `relevance_score`, sometimes `score`).
31
29
  const normalized = (response.results ?? []).map((r) => {
32
30
  const score = typeof r.score === 'number' ? r.score : r.relevance_score;
33
31
  const out = { index: r.index, score };
@@ -9,12 +9,6 @@ export interface SearchModelsArgs {
9
9
  video?: boolean;
10
10
  };
11
11
  limit?: number;
12
- /**
13
- * Skip this many matching results before returning `limit`. Paired with
14
- * `limit` and the returned `next_offset` to let large model lists be
15
- * paged safely. Follows Phil Schmid's "paginate large results" best
16
- * practice.
17
- */
18
12
  offset?: number;
19
13
  }
20
14
  export declare function handleSearchModels(request: {
@@ -1,14 +1,8 @@
1
- /**
2
- * speech_to_text — uses OpenRouter's dedicated POST /api/v1/audio/transcriptions
3
- * endpoint (launched May 2026) for speech-to-text transcription. Faster and more
4
- * cost-efficient than routing through chat completions for pure transcription.
5
- *
6
- * Supported models: OpenAI Whisper-1, GPT-4o Transcribe, GPT-4o Mini Transcribe,
7
- * Mistral Voxtral Mini Transcribe.
8
- */
9
- import { promises as fs } from 'fs';
1
+ /** Dedicated POST /api/v1/audio/transcriptions — Whisper, GPT-4o Transcribe, Voxtral. */
2
+ import { promises as fs } from 'node:fs';
10
3
  import path from 'node:path';
11
4
  import { resolveSafeInputPath, UnsafeOutputPathError } from './path-safety.js';
5
+ import { fetchHttpResource } from './fetch-utils.js';
12
6
  import { ErrorCode, toolError, toolErrorFrom } from '../errors.js';
13
7
  import { SERVER_VERSION } from '../version.js';
14
8
  import { logger } from '../logger.js';
@@ -20,38 +14,37 @@ const VALID_RESPONSE_FORMATS = new Set(['json', 'text', 'srt', 'verbose_json', '
20
14
  function audioFormatFromExt(ext) {
21
15
  const normalized = ext.toLowerCase().replace('.', '');
22
16
  switch (normalized) {
23
- case 'mp3': return 'mp3';
17
+ case 'mp3':
18
+ return 'mp3';
24
19
  case 'mp4':
25
- case 'm4a': return 'mp4';
26
- case 'wav': return 'wav';
27
- case 'flac': return 'flac';
20
+ case 'm4a':
21
+ return 'mp4';
22
+ case 'wav':
23
+ return 'wav';
24
+ case 'flac':
25
+ return 'flac';
28
26
  case 'ogg':
29
- case 'oga': return 'ogg';
30
- case 'webm': return 'webm';
31
- case 'opus': return 'opus';
32
- default: return 'mp3';
27
+ case 'oga':
28
+ return 'ogg';
29
+ case 'webm':
30
+ return 'webm';
31
+ case 'opus':
32
+ return 'opus';
33
+ default:
34
+ return 'mp3';
33
35
  }
34
36
  }
35
- /**
36
- * Resolve audio input to base64 + format, supporting:
37
- * - data: URLs (pass through)
38
- * - http(s) URLs (fetch)
39
- * - local file paths (sandboxed read)
40
- */
41
37
  async function resolveAudioInput(audioPath) {
42
38
  const trimmed = audioPath.trim();
43
39
  if (!trimmed)
44
40
  throw new Error('audio_path is empty');
45
- // Data URL
46
41
  if (trimmed.startsWith('data:')) {
47
42
  const match = trimmed.match(/^data:audio\/([^;,]+)(?:;[^,]*)*;base64,(.+)$/);
48
43
  if (!match)
49
44
  throw new Error('Invalid audio data URL format');
50
45
  return { data: match[2], format: match[1] };
51
46
  }
52
- // HTTP URL
53
47
  if (/^https?:\/\//i.test(trimmed)) {
54
- const { fetchHttpResource } = await import('./fetch-utils.js');
55
48
  const { buffer, contentType } = await fetchHttpResource(trimmed, {
56
49
  timeoutMs: 60_000,
57
50
  maxBytes: 100 * 1024 * 1024,
@@ -60,7 +53,6 @@ async function resolveAudioInput(audioPath) {
60
53
  const format = contentType?.match(/audio\/(\w+)/)?.[1] || 'mp3';
61
54
  return { data: buffer.toString('base64'), format };
62
55
  }
63
- // Local file
64
56
  const abs = await resolveSafeInputPath(trimmed);
65
57
  const buf = await fs.readFile(abs);
66
58
  const ext = path.extname(abs);
@@ -82,7 +74,6 @@ export async function handleSpeechToText(request, apiClient) {
82
74
  language,
83
75
  response_format,
84
76
  });
85
- // Resolve audio input
86
77
  let audioInput;
87
78
  try {
88
79
  audioInput = await resolveAudioInput(audio_path);
@@ -95,7 +86,6 @@ export async function handleSpeechToText(request, apiClient) {
95
86
  return toolErrorFrom(ErrorCode.UPSTREAM_REFUSED, err);
96
87
  return toolErrorFrom(ErrorCode.INVALID_INPUT, err);
97
88
  }
98
- // Build request body
99
89
  const body = {
100
90
  model: model || DEFAULT_MODEL,
101
91
  input_audio: {
@@ -6,10 +6,7 @@ export interface StructuredResult<T = unknown> {
6
6
  structuredContent: T;
7
7
  _meta: Record<string, unknown>;
8
8
  }
9
- /**
10
- * Wrap a JSON-serializable object in the MCP-spec dual-representation
11
- * format. `meta` is merged on top of the default `server_version` stamp.
12
- */
9
+ /** Wrap JSON-serializable data in MCP dual-representation format. */
13
10
  export declare function buildStructuredResult<T>(data: T, meta?: Record<string, unknown>): StructuredResult<T>;
14
11
  /** Read typed JSON from an MCP tool result (structuredContent or legacy text). */
15
12
  export declare function readToolPayload<T = unknown>(result: {
@@ -1,20 +1,6 @@
1
- /**
2
- * Helper for building MCP tool responses that carry structured data
3
- * alongside the legacy text representation.
4
- *
5
- * Per MCP spec 2025-06-18 §5.2.6-7, when a tool has an `outputSchema`
6
- * the response SHOULD include `structuredContent` (the typed object)
7
- * AND, for backwards compatibility with clients that don't parse that
8
- * field, `content` with a serialized JSON text block.
9
- *
10
- * Consumers use `buildStructuredResult(data, meta?)` and get back the
11
- * full `{ content, structuredContent, _meta }` shape.
12
- */
1
+ /** Build MCP tool responses with `structuredContent` plus legacy JSON text. */
13
2
  import { SERVER_VERSION } from '../version.js';
14
- /**
15
- * Wrap a JSON-serializable object in the MCP-spec dual-representation
16
- * format. `meta` is merged on top of the default `server_version` stamp.
17
- */
3
+ /** Wrap JSON-serializable data in MCP dual-representation format. */
18
4
  export function buildStructuredResult(data, meta = {}) {
19
5
  return {
20
6
  content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
@@ -1,14 +1,7 @@
1
- /**
2
- * text_to_speech — uses OpenRouter's dedicated POST /api/v1/audio/speech
3
- * endpoint (launched May 2026) for text-to-speech. Faster and more cost-efficient
4
- * than routing through chat completions with audio modality.
5
- *
6
- * Supported providers: OpenAI (GPT-4o Mini TTS), Google (Gemini Flash TTS),
7
- * Mistral (Voxtral Mini TTS).
8
- */
9
- import { promises as fs } from 'fs';
10
- import { extname } from 'path';
11
- import { resolveSafeOutputPath, UnsafeOutputPathError } from './path-safety.js';
1
+ /** Dedicated POST /api/v1/audio/speech — OpenAI, Gemini Flash TTS, Voxtral. */
2
+ import { promises as fs } from 'node:fs';
3
+ import { extname } from 'node:path';
4
+ import { resolveOptionalOutputPath, isToolErrorResult } from './path-safety.js';
12
5
  import { ErrorCode, toolError, toolErrorFrom } from '../errors.js';
13
6
  import { SERVER_VERSION } from '../version.js';
14
7
  import { logger } from '../logger.js';
@@ -33,19 +26,10 @@ export async function handleTextToSpeech(request, apiClient) {
33
26
  input_preview: input.slice(0, 80),
34
27
  save_path: save_path ? 'provided' : 'none',
35
28
  });
36
- // Resolve save path early
37
- let safeSavePath = null;
38
- if (save_path) {
39
- try {
40
- safeSavePath = await resolveSafeOutputPath(save_path);
41
- }
42
- catch (err) {
43
- if (err instanceof UnsafeOutputPathError)
44
- return toolErrorFrom(ErrorCode.UNSAFE_PATH, err);
45
- return toolErrorFrom(ErrorCode.INTERNAL, err);
46
- }
47
- }
48
- // Build request body
29
+ const savePathResult = await resolveOptionalOutputPath(save_path);
30
+ if (isToolErrorResult(savePathResult))
31
+ return savePathResult;
32
+ const safeSavePath = savePathResult.path;
49
33
  const body = {
50
34
  model: model || DEFAULT_MODEL,
51
35
  input,
@@ -67,7 +51,6 @@ export async function handleTextToSpeech(request, apiClient) {
67
51
  }
68
52
  const { buffer, contentType } = result;
69
53
  const mimeType = contentType.split(';')[0]?.trim() || 'audio/mpeg';
70
- // Determine file extension from format
71
54
  const ext = response_format || 'mp3';
72
55
  const baseMeta = {
73
56
  server_version: SERVER_VERSION,
@@ -77,7 +60,6 @@ export async function handleTextToSpeech(request, apiClient) {
77
60
  voice: voice || DEFAULT_VOICE,
78
61
  };
79
62
  if (safeSavePath) {
80
- // Ensure extension matches
81
63
  const currentExt = extname(safeSavePath).toLowerCase().slice(1);
82
64
  const actualPath = currentExt === ext ? safeSavePath : `${safeSavePath}.${ext}`;
83
65
  try {
@@ -20,10 +20,5 @@ export interface VideoData {
20
20
  mediaType: string;
21
21
  sizeBytes: number;
22
22
  }
23
- /**
24
- * Prepare a video from any source (data URL / HTTP URL / local file) as
25
- * base64 + MIME. OpenRouter requires the client to send video as either a
26
- * URL or a data URL; we always base64 it so the tool works regardless of
27
- * provider quirks.
28
- */
23
+ /** Prepare video from data URL, HTTP URL, or sandboxed local file. */
29
24
  export declare function prepareVideoData(source: string): Promise<VideoData>;
@@ -1,11 +1,4 @@
1
- /**
2
- * Video format detection and fetch utilities. Mirrors the structure of
3
- * `audio-utils.ts`: all network/security logic comes from `fetch-utils.ts`,
4
- * this module owns format detection, base64 encoding, and MIME mapping.
5
- *
6
- * OpenRouter's video-understanding docs (accessed 2026-04-20) list four
7
- * supported container formats: mp4, mpeg, mov, webm.
8
- */
1
+ /** Video format detection and fetch utilities. */
9
2
  import path from 'node:path';
10
3
  import { promises as fs } from 'node:fs';
11
4
  import { readEnvInt, fetchHttpResource, parseBase64DataUrl } from './fetch-utils.js';
@@ -107,14 +100,8 @@ export function detectVideoFormat(buffer) {
107
100
  }
108
101
  return undefined;
109
102
  }
110
- /**
111
- * Prepare a video from any source (data URL / HTTP URL / local file) as
112
- * base64 + MIME. OpenRouter requires the client to send video as either a
113
- * URL or a data URL; we always base64 it so the tool works regardless of
114
- * provider quirks.
115
- */
103
+ /** Prepare video from data URL, HTTP URL, or sandboxed local file. */
116
104
  export async function prepareVideoData(source) {
117
- // --- data URL ---
118
105
  if (source.startsWith('data:')) {
119
106
  const parsed = parseBase64DataUrl(source);
120
107
  if (!parsed)
@@ -136,7 +123,6 @@ export async function prepareVideoData(source) {
136
123
  sizeBytes: approxBytes,
137
124
  };
138
125
  }
139
- // --- HTTP(S) URL ---
140
126
  if (source.startsWith('http://') || source.startsWith('https://')) {
141
127
  const { buffer, contentType } = await fetchHttpResource(source, {
142
128
  timeoutMs: getFetchTimeoutMs(),
@@ -155,7 +141,6 @@ export async function prepareVideoData(source) {
155
141
  sizeBytes: buffer.length,
156
142
  };
157
143
  }
158
- // --- local file ---
159
144
  const safe = await resolveSafeInputPath(source);
160
145
  const buffer = await fs.readFile(safe);
161
146
  const format = detectVideoFormat(buffer) ?? getVideoFormat(safe);