@stabgan/openrouter-mcp-multimodal 4.8.0 → 5.0.1
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 +15 -15
- package/dist/index.js +1 -1
- package/dist/model-cache.js +1 -3
- package/dist/tool-definitions.d.ts +1 -1
- package/dist/tool-definitions.js +6 -5
- package/dist/tool-descriptions.d.ts +0 -4
- package/dist/tool-descriptions.js +7 -6
- package/dist/tool-handlers/analyze-image.js +1 -1
- package/dist/tool-handlers/chat-request.d.ts +1 -1
- package/dist/tool-handlers/chat-request.js +1 -1
- package/dist/tool-handlers/fetch-utils.js +4 -15
- package/dist/tool-handlers/provider-routing.js +1 -1
- package/dist/tool-handlers/rerank.js +1 -1
- package/dist/tool-handlers/text-to-speech.js +15 -12
- package/dist/tts-defaults.d.ts +4 -0
- package/dist/tts-defaults.js +4 -0
- package/dist/version.d.ts +3 -2
- package/dist/version.js +4 -2
- package/package.json +11 -11
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%
|
|
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 (
|
|
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
|
|
89
|
-
| **[uvx / pipx](#manual-config)** | Python 3.10+ **and** Node.js
|
|
90
|
-
| **[npm global](#manual-config)** | Node.js
|
|
91
|
-
| **[node (local)](#manual-config)** | Node.js
|
|
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@
|
|
143
|
+
Pin a release: `"args": ["-y", "@stabgan/openrouter-mcp-multimodal@5.0.1"]`
|
|
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
|
|
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=
|
|
155
|
+
# pin npm version: OPENROUTER_MCP_NPM_VERSION=5.0.1 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=
|
|
174
|
+
Optional: `OPENROUTER_MCP_NPM_VERSION=5.0.1` 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:
|
|
255
|
+
ghcr.io/stabgan/openrouter-mcp-multimodal:5.0.1
|
|
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:
|
|
269
|
+
"ghcr.io/stabgan/openrouter-mcp-multimodal:5.0.1"
|
|
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,
|
|
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 | `
|
|
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 **≥
|
|
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 = '
|
|
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;
|
package/dist/model-cache.js
CHANGED
|
@@ -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
|
|
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", "
|
|
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"];
|
package/dist/tool-definitions.js
CHANGED
|
@@ -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', '
|
|
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:
|
|
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:
|
|
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:
|
|
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-
|
|
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: {
|
|
@@ -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
|
-
|
|
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
|
|
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": "
|
|
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": "
|
|
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-
|
|
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-
|
|
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 = '
|
|
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 = "
|
|
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 = '
|
|
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
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
|
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:
|
|
35
|
-
voice:
|
|
36
|
-
response_format:
|
|
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:
|
|
47
|
+
model: effectiveModel,
|
|
46
48
|
input,
|
|
47
|
-
|
|
49
|
+
response_format: effectiveResponseFormat,
|
|
48
50
|
};
|
|
49
|
-
if (
|
|
50
|
-
body.
|
|
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:
|
|
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";
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export declare const SERVER_VERSION = "
|
|
2
|
-
|
|
1
|
+
export declare const SERVER_VERSION = "5.0.1";
|
|
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
|
-
|
|
2
|
-
export const
|
|
1
|
+
import { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
export const SERVER_VERSION = '5.0.1';
|
|
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": "
|
|
3
|
+
"version": "5.0.1",
|
|
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": ">=
|
|
58
|
+
"node": ">=22.0.0"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
61
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
62
62
|
"dotenv": "^17.4.2",
|
|
63
|
-
"openai": "^
|
|
64
|
-
"sharp": "^0.35.
|
|
63
|
+
"openai": "^7.9.0",
|
|
64
|
+
"sharp": "^0.35.4"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
|
-
"@eslint/js": "^
|
|
68
|
-
"@types/node": "^22.20.
|
|
69
|
-
"eslint": "^9.
|
|
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.
|
|
71
|
+
"prettier": "^3.9.6",
|
|
72
72
|
"shx": "^0.4.0",
|
|
73
73
|
"typescript": "^5.9.3",
|
|
74
|
-
"typescript-eslint": "^8.
|
|
75
|
-
"vitest": "^4.1.
|
|
74
|
+
"typescript-eslint": "^8.69.0",
|
|
75
|
+
"vitest": "^4.1.11"
|
|
76
76
|
}
|
|
77
77
|
}
|