@stabgan/openrouter-mcp-multimodal 4.8.0 → 5.0.0

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.
package/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
  <a href="https://hub.docker.com/r/stabgan/openrouter-mcp-multimodal"><img src="https://img.shields.io/docker/v/stabgan/openrouter-mcp-multimodal/latest?label=docker&color=2496ed&logo=docker&logoColor=white" alt="Docker version" /></a>
17
17
  <a href="https://github.com/stabgan/openrouter-mcp-multimodal/actions/workflows/ci.yml"><img src="https://github.com/stabgan/openrouter-mcp-multimodal/actions/workflows/ci.yml/badge.svg" alt="CI status" /></a>
18
18
  <a href="https://www.apache.org/licenses/LICENSE-2.0"><img src="https://img.shields.io/badge/License-Apache_2.0-blue.svg" alt="Apache 2.0 license" /></a>
19
- <a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%E2%89%A520-43853d?logo=node.js&logoColor=white" alt="Node.js 20+" /></a>
19
+ <a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%E2%89%A522-43853d?logo=node.js&logoColor=white" alt="Node.js 22+" /></a>
20
20
  </p>
21
21
 
22
22
  <p align="center">
@@ -49,7 +49,7 @@ Unlike text-only MCP servers, one install covers the **full multimodal surface**
49
49
  | :---------- | :-------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
50
50
  | **Chat** | `chat_completion`, `start_chat_completion`, `get_chat_completion_status` | 300+ models, `:nitro` / `:floor` / `:free` / `:online` / `:exacto` suffixes, provider routing, web search, response caching, reasoning tokens, async jobs for long-running models |
51
51
  | **Vision** | `analyze_image`, `generate_image`, `generate_image_dedicated` | OCR, captioning, VQA, image generation with reference inputs, dedicated Image API with resolution/quality/format control |
52
- | **Audio** | `analyze_audio`, `generate_audio`, `text_to_speech`, `speech_to_text` | Transcription, speech/music generation, dedicated TTS (OpenAI/Gemini/Voxtral voices), dedicated STT (Whisper/GPT-4o Transcribe) |
52
+ | **Audio** | `analyze_audio`, `generate_audio`, `text_to_speech`, `speech_to_text` | Transcription, speech/music generation, dedicated TTS (free Deepgram default; model-specific voices, mp3/pcm), dedicated STT (Whisper/GPT-4o Transcribe) |
53
53
  | **Video** | `analyze_video`, `generate_video`, `generate_video_from_image`, `get_video_status` | Clip understanding, Veo 3.1 / Seedance 2.0 / Wan 2.7 generation with progress notifications |
54
54
  | **Catalog** | `search_models`, `get_model_info`, `validate_model`, `rerank_documents`, `health_check` | Model discovery, validation, reranking, ops health |
55
55
 
@@ -85,10 +85,10 @@ MCP servers are distributed through several packaging models. **This server is i
85
85
 
86
86
  | Method | Runtime | Best for | This server |
87
87
  | :-------------------------------------- | :------------------------------- | :------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------- |
88
- | **[npx](#manual-config)** | Node.js 20+ | Most MCP clients (default) | ✅ `@stabgan/openrouter-mcp-multimodal` |
89
- | **[uvx / pipx](#manual-config)** | Python 3.10+ **and** Node.js 20+ | Python-first workflows, same pattern as PyPI MCP servers | ✅ [`mcp-server-openrouter-multimodal`](https://pypi.org/project/mcp-server-openrouter-multimodal/) |
90
- | **[npm global](#manual-config)** | Node.js 20+ | Pin a version without re-downloading | ✅ |
91
- | **[node (local)](#manual-config)** | Node.js 20+ | Contributors / air-gapped builds | ✅ |
88
+ | **[npx](#manual-config)** | Node.js 22+ | Most MCP clients (default) | ✅ `@stabgan/openrouter-mcp-multimodal` |
89
+ | **[uvx / pipx](#manual-config)** | Python 3.10+ **and** Node.js 22+ | Python-first workflows, same pattern as PyPI MCP servers | ✅ [`mcp-server-openrouter-multimodal`](https://pypi.org/project/mcp-server-openrouter-multimodal/) |
90
+ | **[npm global](#manual-config)** | Node.js 22+ | Pin a version without re-downloading | ✅ |
91
+ | **[node (local)](#manual-config)** | Node.js 22+ | Contributors / air-gapped builds | ✅ |
92
92
  | **[Docker Hub](#manual-config)** | Docker | Isolation, no Node on host | ✅ `stabgan/openrouter-mcp-multimodal` |
93
93
  | **[GHCR](#manual-config)** | Docker | GitHub-native OCI pulls | ✅ `ghcr.io/stabgan/openrouter-mcp-multimodal` |
94
94
  | **[Smithery CLI](#smithery)** | Node.js (via installer) | Interactive install into Claude/Cursor/etc. | ✅ |
@@ -140,19 +140,19 @@ npx -y @stabgan/openrouter-mcp-multimodal
140
140
  }
141
141
  ```
142
142
 
143
- Pin a release: `"args": ["-y", "@stabgan/openrouter-mcp-multimodal@4.8.0"]`
143
+ Pin a release: `"args": ["-y", "@stabgan/openrouter-mcp-multimodal@5.0.0"]`
144
144
 
145
145
  </details>
146
146
 
147
147
  <details>
148
148
  <summary><strong>uvx / pipx (Python launcher)</strong></summary>
149
149
 
150
- Install [uv](https://docs.astral.sh/uv/getting-started/installation/) (includes `uvx`), ensure **Node.js 20+** is also on your `PATH`, then:
150
+ Install [uv](https://docs.astral.sh/uv/getting-started/installation/) (includes `uvx`), ensure **Node.js 22+** is also on your `PATH`, then:
151
151
 
152
152
  ```bash
153
153
  export OPENROUTER_API_KEY=sk-or-v1-...
154
154
  uvx mcp-server-openrouter-multimodal
155
- # pin npm version: OPENROUTER_MCP_NPM_VERSION=4.8.0 uvx mcp-server-openrouter-multimodal
155
+ # pin npm version: OPENROUTER_MCP_NPM_VERSION=5.0.0 uvx mcp-server-openrouter-multimodal
156
156
  ```
157
157
 
158
158
  ```json
@@ -171,7 +171,7 @@ uvx mcp-server-openrouter-multimodal
171
171
 
172
172
  **pipx equivalent:** `pipx run mcp-server-openrouter-multimodal`
173
173
 
174
- Optional: `OPENROUTER_MCP_NPM_VERSION=4.8.0` pins the underlying npm package.
174
+ Optional: `OPENROUTER_MCP_NPM_VERSION=5.0.0` pins the underlying npm package.
175
175
 
176
176
  </details>
177
177
 
@@ -252,7 +252,7 @@ Use `-i` (interactive stdio). Avoid `-t` (TTY corrupts MCP framing on some hosts
252
252
 
253
253
  ```bash
254
254
  docker run --rm -i -e OPENROUTER_API_KEY=sk-or-v1-... \
255
- ghcr.io/stabgan/openrouter-mcp-multimodal:4.8.0
255
+ ghcr.io/stabgan/openrouter-mcp-multimodal:5.0.0
256
256
  ```
257
257
 
258
258
  ```json
@@ -266,7 +266,7 @@ docker run --rm -i -e OPENROUTER_API_KEY=sk-or-v1-... \
266
266
  "-i",
267
267
  "-e",
268
268
  "OPENROUTER_API_KEY=sk-or-v1-...",
269
- "ghcr.io/stabgan/openrouter-mcp-multimodal:4.8.0"
269
+ "ghcr.io/stabgan/openrouter-mcp-multimodal:5.0.0"
270
270
  ]
271
271
  }
272
272
  }
@@ -376,7 +376,7 @@ If still failing, use the full path from `where npx` as the command.
376
376
  | `generate_image` | Text-to-image via chat completions with reference images |
377
377
  | `generate_image_dedicated` | Text-to-image via dedicated `/api/v1/images` (resolution, quality, format) |
378
378
  | `generate_audio` | Text-to-speech / music via chat completions |
379
- | `text_to_speech` | Dedicated TTS (`/api/v1/audio/speech`) — voices, speed, format |
379
+ | `text_to_speech` | Dedicated TTS (`/api/v1/audio/speech`) — free Deepgram default, voices, speed, mp3/pcm |
380
380
  | `speech_to_text` | Dedicated STT (`/api/v1/audio/transcriptions`) — Whisper, GPT-4o |
381
381
  | `generate_video` | Text-to-video (async, resumable) |
382
382
  | `generate_video_from_image` | Image-to-video (narrower schema) |
@@ -491,7 +491,7 @@ Report vulnerabilities: **[SECURITY.md](./SECURITY.md)** (private disclosure —
491
491
  | Variable | Required | Default | Description |
492
492
  | :---------------------------------- | :------: | :------------------------------------ | :---------------------------------- |
493
493
  | `OPENROUTER_API_KEY` | **Yes** | — | OpenRouter API key |
494
- | `OPENROUTER_DEFAULT_MODEL` | No | `nvidia/nemotron-nano-12b-v2-vl:free` | Default when tools omit `model` |
494
+ | `OPENROUTER_DEFAULT_MODEL` | No | `google/gemma-4-26b-a4b-it:free` | Default when tools omit `model` |
495
495
  | `OPENROUTER_OUTPUT_DIR` | No | `cwd` | Sandbox root for `save_path` |
496
496
  | `OPENROUTER_INPUT_DIR` | No | `OUTPUT_DIR` or `cwd` | Sandbox root for local input files |
497
497
  | `OPENROUTER_INLINE_MAX_BYTES` | No | `1048576` (image/audio) | Global inline media ceiling |
@@ -583,7 +583,7 @@ Fixed in 4.5.2+ — see [GHSA-3q7p-736f-x44v](https://github.com/stabgan/openrou
583
583
 
584
584
  ## Compatibility
585
585
 
586
- Works with any MCP client. Protocol: **MCP 2025-06-18**. Node **≥ 20** (Docker image uses Node 22).
586
+ Works with any MCP client. Protocol: **MCP 2025-06-18**. Node **≥ 22** (Docker image uses Node 24).
587
587
 
588
588
  ## License
589
589
 
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { ToolHandlers } from './tool-handlers.js';
8
8
  import { logger } from './logger.js';
9
9
  import { SERVER_VERSION } from './version.js';
10
10
  import { SERVER_ICON } from './tool-icons.js';
11
- const DEFAULT_MODEL = 'nvidia/nemotron-nano-12b-v2-vl:free';
11
+ const DEFAULT_MODEL = 'google/gemma-4-26b-a4b-it:free';
12
12
  // Log whitelisted fields only — avoid leaking auth headers from SDK errors.
13
13
  function logFatal(kind, err) {
14
14
  const e = err;
@@ -147,7 +147,6 @@ export class ModelCache {
147
147
  const safeOffset = clampOffset(offset);
148
148
  const safeLimit = clampLimit(limit);
149
149
  const page = [];
150
- let total = 0;
151
150
  let matchIndex = 0;
152
151
  for (const model of sortedModels(this.models)) {
153
152
  if (!matches(model))
@@ -157,8 +156,7 @@ export class ModelCache {
157
156
  }
158
157
  matchIndex++;
159
158
  }
160
- total = matchIndex;
161
- return { page, total };
159
+ return { page, total: matchIndex };
162
160
  }
163
161
  search(params) {
164
162
  if (params.all) {
@@ -7,7 +7,7 @@ export declare const IMAGE_OUTPUT_FORMATS: readonly ["png", "jpeg", "webp", "svg
7
7
  /** generate_audio handler VALID_FORMATS */
8
8
  export declare const GENERATE_AUDIO_FORMATS: readonly ["wav", "mp3", "flac", "opus", "pcm16"];
9
9
  /** text_to_speech handler VALID_FORMATS */
10
- export declare const TTS_RESPONSE_FORMATS: readonly ["mp3", "opus", "aac", "flac", "wav", "pcm"];
10
+ export declare const TTS_RESPONSE_FORMATS: readonly ["mp3", "pcm"];
11
11
  /** speech_to_text handler VALID_RESPONSE_FORMATS */
12
12
  export declare const STT_RESPONSE_FORMATS: readonly ["json", "text", "srt", "verbose_json", "vtt"];
13
13
  export declare const CHAT_MESSAGE_ROLES: readonly ["system", "user", "assistant"];
@@ -1,4 +1,5 @@
1
1
  import { TOOL_DESCRIPTIONS, TOOL_NAMES } from './tool-descriptions.js';
2
+ import { DEFAULT_TTS_MODEL, DEFAULT_TTS_RESPONSE_FORMAT, DEFAULT_TTS_VOICE, } from './tts-defaults.js';
2
3
  /** Aspect ratios accepted by generate_image and generate_image_dedicated handlers. */
3
4
  export const IMAGE_ASPECT_RATIOS = [
4
5
  '1:1',
@@ -23,7 +24,7 @@ export const IMAGE_OUTPUT_FORMATS = ['png', 'jpeg', 'webp', 'svg'];
23
24
  /** generate_audio handler VALID_FORMATS */
24
25
  export const GENERATE_AUDIO_FORMATS = ['wav', 'mp3', 'flac', 'opus', 'pcm16'];
25
26
  /** text_to_speech handler VALID_FORMATS */
26
- export const TTS_RESPONSE_FORMATS = ['mp3', 'opus', 'aac', 'flac', 'wav', 'pcm'];
27
+ export const TTS_RESPONSE_FORMATS = ['mp3', 'pcm'];
27
28
  /** speech_to_text handler VALID_RESPONSE_FORMATS */
28
29
  export const STT_RESPONSE_FORMATS = ['json', 'text', 'srt', 'verbose_json', 'vtt'];
29
30
  export const CHAT_MESSAGE_ROLES = ['system', 'user', 'assistant'];
@@ -548,16 +549,16 @@ export const TOOL_DEFINITIONS = [
548
549
  },
549
550
  model: {
550
551
  type: 'string',
551
- description: 'TTS model. Default: openai/gpt-4o-mini-tts-2025-12-15. Also: google/gemini-flash-tts, mistral/voxtral-mini-tts.',
552
+ description: `TTS model for OpenRouter POST /audio/speech. Default: ${DEFAULT_TTS_MODEL}. Discover current models with GET /api/v1/models?output_modalities=speech.`,
552
553
  },
553
554
  voice: {
554
555
  type: 'string',
555
- description: 'Voice ID (model-specific). Default: alloy. OpenAI voices: alloy, echo, fable, onyx, nova, shimmer.',
556
+ description: `Voice ID (model-specific). Default: ${DEFAULT_TTS_VOICE} for ${DEFAULT_TTS_MODEL}; pass a voice supported by another model.`,
556
557
  },
557
558
  response_format: {
558
559
  type: 'string',
559
560
  enum: [...TTS_RESPONSE_FORMATS],
560
- description: 'Output audio format. Default: mp3.',
561
+ description: `Output audio format (mp3 or pcm). Default: ${DEFAULT_TTS_RESPONSE_FORMAT}.`,
561
562
  },
562
563
  speed: {
563
564
  type: 'number',
@@ -754,7 +755,7 @@ export const TOOL_DEFINITIONS = [
754
755
  documents: { type: 'array', items: { type: 'string' }, minItems: 1 },
755
756
  model: {
756
757
  type: 'string',
757
- description: 'Reranker model (default: cohere/rerank-english-v3.0).',
758
+ description: 'Reranker model (default: cohere/rerank-v3.5).',
758
759
  },
759
760
  top_n: { type: 'number', minimum: 1, description: 'Return only the top N results.' },
760
761
  return_documents: {
@@ -1,7 +1,3 @@
1
- /**
2
- * MCP tool descriptions with explicit routing, examples, and failure modes.
3
- * See docs/plans/tool-description-improvement.md for the authoring guide.
4
- */
5
1
  export interface ToolDescriptionParts {
6
2
  summary: string;
7
3
  useWhen: string[];
@@ -2,6 +2,7 @@
2
2
  * MCP tool descriptions with explicit routing, examples, and failure modes.
3
3
  * See docs/plans/tool-description-improvement.md for the authoring guide.
4
4
  */
5
+ import { DEFAULT_TTS_MODEL, DEFAULT_TTS_RESPONSE_FORMAT, DEFAULT_TTS_VOICE, } from './tts-defaults.js';
5
6
  function formatBullets(items) {
6
7
  return items.map((item) => `- ${item}`).join('\n');
7
8
  }
@@ -383,11 +384,11 @@ export const TOOL_DESCRIPTIONS = {
383
384
  }),
384
385
  text_to_speech: buildToolDescription({
385
386
  summary: "Convert text to speech via OpenRouter's dedicated TTS endpoint (POST /api/v1/audio/speech). " +
386
- 'Faster and cheaper than chat completions for pure TTS. Models: OpenAI GPT-4o Mini TTS, Google Gemini Flash TTS, Mistral Voxtral.',
387
+ `Default: ${DEFAULT_TTS_MODEL} with ${DEFAULT_TTS_VOICE}; discover current models with GET /api/v1/models?output_modalities=speech. Output formats: mp3 or pcm (default: ${DEFAULT_TTS_RESPONSE_FORMAT}).`,
387
388
  useWhen: [
388
389
  'You need text-to-speech with specific voice control',
389
390
  'You want fast, dedicated TTS without chat overhead',
390
- 'You need a specific audio format (mp3, opus, wav, etc.)',
391
+ 'You need mp3 or pcm audio output',
391
392
  ],
392
393
  notWhen: [
393
394
  'You want to generate music or sound effects → generate_audio',
@@ -396,13 +397,13 @@ export const TOOL_DESCRIPTIONS = {
396
397
  ],
397
398
  goodExamples: [
398
399
  '`{ "input": "Hello, welcome to our app!" }`',
399
- '`{ "input": "...", "voice": "nova", "response_format": "mp3", "save_path": "out/welcome.mp3" }`',
400
+ '`{ "input": "...", "voice": "flux-alexis-en", "response_format": "mp3", "save_path": "out/welcome.mp3" }`',
400
401
  '`{ "input": "...", "instructions": "speak slowly and clearly", "speed": 0.8 }`',
401
402
  ],
402
403
  badExamples: [
403
404
  '`{ "input": "" }` → INVALID_INPUT',
404
405
  '`{ "prompt": "text" }` → wrong key; use `input`',
405
- '`{ "response_format": "mp4" }` → not a valid audio format',
406
+ '`{ "response_format": "wav" }` → only mp3 and pcm are supported',
406
407
  ],
407
408
  failsWhen: [
408
409
  'INVALID_INPUT: empty input, invalid response_format',
@@ -528,7 +529,7 @@ export const TOOL_DESCRIPTIONS = {
528
529
  worksWith: ['generate_video', 'generate_video_from_image'],
529
530
  }),
530
531
  rerank_documents: buildToolDescription({
531
- summary: 'Re-order documents by relevance to a query using an OpenRouter reranker. Default: cohere/rerank-english-v3.0.',
532
+ summary: 'Re-order documents by relevance to a query using an OpenRouter reranker. Default: cohere/rerank-v3.5.',
532
533
  useWhen: [
533
534
  'You have a query and a list of text snippets to sort by relevance',
534
535
  'You will feed top results into chat_completion for grounded answers',
@@ -539,7 +540,7 @@ export const TOOL_DESCRIPTIONS = {
539
540
  ],
540
541
  goodExamples: [
541
542
  '`{ "query": "battery life", "documents": ["Doc A text...", "Doc B text..."] }`',
542
- '`{ "query": "...", "documents": [...], "model": "cohere/rerank-english-v3.0" }`',
543
+ '`{ "query": "...", "documents": [...], "model": "cohere/rerank-v3.5" }`',
543
544
  ],
544
545
  badExamples: [
545
546
  '`{ "documents": [] }` → INVALID_INPUT',
@@ -6,7 +6,7 @@ import { classifyUpstreamError } from './openrouter-errors.js';
6
6
  import { extractCompletionText, detectReasoningCutoff, buildCompletionMeta, } from './completion-utils.js';
7
7
  import { buildCacheHeaders, extractCacheMeta, validateCacheOptions, } from './cache.js';
8
8
  import { awaitCompletionWithHeaders } from './openai-withresponse.js';
9
- const DEFAULT_MODEL = 'nvidia/nemotron-nano-12b-v2-vl:free';
9
+ const DEFAULT_MODEL = 'google/gemma-4-26b-a4b-it:free';
10
10
  export async function handleAnalyzeImage(request, openai, defaultModel) {
11
11
  const args = request.params.arguments ?? { image_path: '' };
12
12
  const { image_path, question, model, cache_input, cache, cache_ttl, cache_clear } = args;
@@ -3,7 +3,7 @@ import type { ChatCompletionMessageParam } from 'openai/resources/chat/completio
3
3
  import { type ToolErrorResult } from '../errors.js';
4
4
  import { type ProviderRoutingOptions } from './provider-routing.js';
5
5
  import { type CacheOptions } from './cache.js';
6
- export declare const DEFAULT_CHAT_MODEL = "nvidia/nemotron-nano-12b-v2-vl:free";
6
+ export declare const DEFAULT_CHAT_MODEL = "google/gemma-4-26b-a4b-it:free";
7
7
  /** Shared request shape for sync and async chat completion tools. */
8
8
  export interface ChatToolRequest extends CacheOptions {
9
9
  model?: string;
@@ -1,7 +1,7 @@
1
1
  import { ErrorCode, toolError } from '../errors.js';
2
2
  import { readProviderDefaults, mergeProviderOptions, buildProviderBody, resolveMaxTokens, } from './provider-routing.js';
3
3
  import { buildCacheHeaders } from './cache.js';
4
- export const DEFAULT_CHAT_MODEL = 'nvidia/nemotron-nano-12b-v2-vl:free';
4
+ export const DEFAULT_CHAT_MODEL = 'google/gemma-4-26b-a4b-it:free';
5
5
  export function readIncludeReasoningDefault() {
6
6
  const raw = (process.env.OPENROUTER_INCLUDE_REASONING ?? '').trim().toLowerCase();
7
7
  return raw === '1' || raw === 'true' || raw === 'yes';
@@ -95,7 +95,6 @@ function expandIPv6(ip) {
95
95
  if (!net.isIPv6(noBrackets))
96
96
  return null;
97
97
  let addr = noBrackets.toLowerCase();
98
- let v4Tail = null;
99
98
  const dotIndex = addr.indexOf('.');
100
99
  if (dotIndex >= 0) {
101
100
  const lastColon = addr.lastIndexOf(':', dotIndex);
@@ -106,10 +105,9 @@ function expandIPv6(ip) {
106
105
  if (parts.length !== 4 || parts.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) {
107
106
  return null;
108
107
  }
109
- v4Tail = [((parts[0] << 8) | parts[1]) & 0xffff, ((parts[2] << 8) | parts[3]) & 0xffff];
110
- const hex6 = v4Tail[0].toString(16);
111
- const hex7 = v4Tail[1].toString(16);
112
- addr = addr.slice(0, lastColon) + ':' + hex6 + ':' + hex7;
108
+ const v4Group6 = ((parts[0] << 8) | parts[1]) & 0xffff;
109
+ const v4Group7 = ((parts[2] << 8) | parts[3]) & 0xffff;
110
+ addr = addr.slice(0, lastColon) + ':' + v4Group6.toString(16) + ':' + v4Group7.toString(16);
113
111
  }
114
112
  const halves = addr.split('::');
115
113
  if (halves.length > 2)
@@ -564,16 +562,7 @@ export async function fetchHttpResource(urlString, opts) {
564
562
  for (let hop = 0; hop <= opts.maxRedirects; hop++) {
565
563
  const { url: validated, addresses } = await validateUrlAndResolveAddresses(current);
566
564
  const target = validated.href;
567
- let res;
568
- try {
569
- res = await pinnedRequestWithFallback(validated, addresses, opts.timeoutMs);
570
- }
571
- catch (err) {
572
- if (err instanceof Error && err.message === 'Fetch timed out') {
573
- throw new Error('Fetch timed out');
574
- }
575
- throw err;
576
- }
565
+ const res = await pinnedRequestWithFallback(validated, addresses, opts.timeoutMs);
577
566
  const statusCode = res.statusCode ?? 0;
578
567
  if (statusCode >= 300 && statusCode < 400) {
579
568
  const loc = headerValue(res.headers, 'location');
@@ -24,7 +24,7 @@ function parseJsonArray(raw, name) {
24
24
  return parsed.length > 0 ? parsed : undefined;
25
25
  }
26
26
  catch (err) {
27
- throw new Error(`${name}: malformed JSON array (${err instanceof Error ? err.message : String(err)})`);
27
+ throw new Error(`${name}: malformed JSON array (${err instanceof Error ? err.message : String(err)})`, { cause: err });
28
28
  }
29
29
  }
30
30
  return parseCsv(trimmed);
@@ -2,7 +2,7 @@ import { ErrorCode, toolError, toolErrorFrom } from '../errors.js';
2
2
  import { classifyUpstreamError } from './openrouter-errors.js';
3
3
  import { buildStructuredResult } from './structured-output.js';
4
4
  import { capResultText } from './completion-utils.js';
5
- const DEFAULT_MODEL = 'cohere/rerank-english-v3.0';
5
+ const DEFAULT_MODEL = 'cohere/rerank-v3.5';
6
6
  function isValidDocumentIndex(index, documentCount) {
7
7
  return (typeof index === 'number' && Number.isInteger(index) && index >= 0 && index < documentCount);
8
8
  }
@@ -1,6 +1,7 @@
1
- /** Dedicated POST /api/v1/audio/speech — OpenAI, Gemini Flash TTS, Voxtral. */
1
+ /** Dedicated POST /api/v1/audio/speech. */
2
2
  import { extname } from 'node:path';
3
3
  import { TTS_RESPONSE_FORMATS } from '../tool-definitions.js';
4
+ import { DEFAULT_TTS_MODEL, DEFAULT_TTS_RESPONSE_FORMAT, DEFAULT_TTS_VOICE, } from '../tts-defaults.js';
4
5
  import { resolveOptionalOutputPath, isToolErrorResult } from './path-safety.js';
5
6
  import { ErrorCode, toolError, toolErrorFrom } from '../errors.js';
6
7
  import { SERVER_VERSION } from '../version.js';
@@ -10,8 +11,6 @@ import { buildBinaryToolResult } from './tool-result-payload.js';
10
11
  import { replaceExtension, writeOutputFile } from './path-utils.js';
11
12
  import { buildCacheHeaders, validateCacheOptions } from './cache.js';
12
13
  import { detectAudioFormat } from './audio-utils.js';
13
- const DEFAULT_MODEL = 'openai/gpt-4o-mini-tts-2025-12-15';
14
- const DEFAULT_VOICE = 'alloy';
15
14
  const MIN_SPEED = 0.25;
16
15
  const MAX_SPEED = 4.0;
17
16
  const VALID_FORMATS = new Set(TTS_RESPONSE_FORMATS);
@@ -30,10 +29,13 @@ export async function handleTextToSpeech(request, apiClient) {
30
29
  const cacheError = validateCacheOptions({ cache, cache_ttl, cache_clear });
31
30
  if (cacheError)
32
31
  return cacheError;
32
+ const effectiveModel = model?.trim() || DEFAULT_TTS_MODEL;
33
+ const effectiveVoice = voice?.trim() || (effectiveModel === DEFAULT_TTS_MODEL ? DEFAULT_TTS_VOICE : undefined);
34
+ const effectiveResponseFormat = response_format || DEFAULT_TTS_RESPONSE_FORMAT;
33
35
  logger.audit('text_to_speech.start', {
34
- model: model || DEFAULT_MODEL,
35
- voice: voice || DEFAULT_VOICE,
36
- response_format: response_format || 'mp3',
36
+ model: effectiveModel,
37
+ voice: effectiveVoice || 'provider default',
38
+ response_format: effectiveResponseFormat,
37
39
  input_preview: input.slice(0, 80),
38
40
  save_path: save_path ? 'provided' : 'none',
39
41
  });
@@ -42,12 +44,12 @@ export async function handleTextToSpeech(request, apiClient) {
42
44
  return savePathResult;
43
45
  const safeSavePath = savePathResult.path;
44
46
  const body = {
45
- model: model || DEFAULT_MODEL,
47
+ model: effectiveModel,
46
48
  input,
47
- voice: voice || DEFAULT_VOICE,
49
+ response_format: effectiveResponseFormat,
48
50
  };
49
- if (response_format)
50
- body.response_format = response_format;
51
+ if (effectiveVoice)
52
+ body.voice = effectiveVoice;
51
53
  if (typeof speed === 'number')
52
54
  body.speed = speed;
53
55
  if (instructions)
@@ -66,11 +68,12 @@ export async function handleTextToSpeech(request, apiClient) {
66
68
  const ext = detected.ext;
67
69
  const baseMeta = {
68
70
  server_version: SERVER_VERSION,
69
- model: model || DEFAULT_MODEL,
71
+ model: effectiveModel,
70
72
  mime: mimeType,
71
73
  size_bytes: buffer.length,
72
- voice: voice || DEFAULT_VOICE,
73
74
  };
75
+ if (effectiveVoice)
76
+ baseMeta.voice = effectiveVoice;
74
77
  if (safeSavePath) {
75
78
  const currentExt = extname(safeSavePath).toLowerCase().slice(1);
76
79
  const actualPath = currentExt === ext ? safeSavePath : replaceExtension(safeSavePath, ext);
@@ -0,0 +1,4 @@
1
+ /** Current free OpenRouter TTS defaults; model and voice can be overridden per call. */
2
+ export declare const DEFAULT_TTS_MODEL = "deepgram/flux-tts:free";
3
+ export declare const DEFAULT_TTS_VOICE = "flux-alexis-en";
4
+ export declare const DEFAULT_TTS_RESPONSE_FORMAT = "mp3";
@@ -0,0 +1,4 @@
1
+ /** Current free OpenRouter TTS defaults; model and voice can be overridden per call. */
2
+ export const DEFAULT_TTS_MODEL = 'deepgram/flux-tts:free';
3
+ export const DEFAULT_TTS_VOICE = 'flux-alexis-en';
4
+ export const DEFAULT_TTS_RESPONSE_FORMAT = 'mp3';
package/dist/version.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- export declare const SERVER_VERSION = "4.8.0";
2
- export declare const MCP_PROTOCOL_VERSION = "2025-06-18";
1
+ export declare const SERVER_VERSION = "5.0.0";
2
+ /** Advertised MCP protocol version — derived from the installed SDK. */
3
+ export declare const MCP_PROTOCOL_VERSION = "2025-11-25";
package/dist/version.js CHANGED
@@ -1,2 +1,4 @@
1
- export const SERVER_VERSION = '4.8.0';
2
- export const MCP_PROTOCOL_VERSION = '2025-06-18';
1
+ import { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';
2
+ export const SERVER_VERSION = '5.0.0';
3
+ /** Advertised MCP protocol version — derived from the installed SDK. */
4
+ export const MCP_PROTOCOL_VERSION = LATEST_PROTOCOL_VERSION;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stabgan/openrouter-mcp-multimodal",
3
- "version": "4.8.0",
3
+ "version": "5.0.0",
4
4
  "mcpName": "io.github.stabgan/openrouter-multimodal",
5
5
  "description": "MCP server for OpenRouter — chat with 300+ LLMs, analyze images/audio/video, generate images (dedicated API), TTS/STT, video generation (Veo 3.1 / Seedance / Wan), async completions, response caching",
6
6
  "type": "module",
@@ -55,23 +55,23 @@
55
55
  "homepage": "https://github.com/stabgan/openrouter-mcp-multimodal#readme",
56
56
  "license": "Apache-2.0",
57
57
  "engines": {
58
- "node": ">=20.0.0"
58
+ "node": ">=22.0.0"
59
59
  },
60
60
  "dependencies": {
61
- "@modelcontextprotocol/sdk": "^1.29.0",
61
+ "@modelcontextprotocol/sdk": "^1.30.0",
62
62
  "dotenv": "^17.4.2",
63
- "openai": "^4.104.0",
64
- "sharp": "^0.35.3"
63
+ "openai": "^7.9.0",
64
+ "sharp": "^0.35.4"
65
65
  },
66
66
  "devDependencies": {
67
- "@eslint/js": "^9.39.2",
68
- "@types/node": "^22.20.0",
69
- "eslint": "^9.39.2",
67
+ "@eslint/js": "^10.0.1",
68
+ "@types/node": "^22.20.1",
69
+ "eslint": "^10.9.1",
70
70
  "eslint-config-prettier": "^10.1.8",
71
- "prettier": "^3.9.4",
71
+ "prettier": "^3.9.6",
72
72
  "shx": "^0.4.0",
73
73
  "typescript": "^5.9.3",
74
- "typescript-eslint": "^8.62.1",
75
- "vitest": "^4.1.9"
74
+ "typescript-eslint": "^8.69.0",
75
+ "vitest": "^4.1.11"
76
76
  }
77
77
  }