@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.
- package/README.md +60 -52
- package/dist/errors.d.ts +3 -12
- package/dist/errors.js +2 -6
- package/dist/index.js +11 -11
- package/dist/logger.d.ts +1 -18
- package/dist/logger.js +0 -7
- package/dist/model-cache.d.ts +1 -20
- package/dist/model-cache.js +1 -20
- package/dist/openrouter-api.d.ts +4 -18
- package/dist/openrouter-api.js +4 -20
- package/dist/tool-definitions.d.ts +1 -0
- package/dist/tool-definitions.js +708 -0
- package/dist/tool-descriptions.js +6 -8
- package/dist/tool-handlers/analyze-audio.d.ts +0 -5
- package/dist/tool-handlers/analyze-image.d.ts +0 -6
- package/dist/tool-handlers/analyze-image.js +1 -8
- package/dist/tool-handlers/analyze-video.d.ts +0 -5
- package/dist/tool-handlers/analyze-video.js +0 -7
- package/dist/tool-handlers/async-chat.d.ts +17 -13
- package/dist/tool-handlers/async-chat.js +43 -66
- package/dist/tool-handlers/audio-utils.d.ts +1 -4
- package/dist/tool-handlers/audio-utils.js +4 -14
- package/dist/tool-handlers/cache.d.ts +2 -18
- package/dist/tool-handlers/cache.js +1 -19
- package/dist/tool-handlers/chat-completion.d.ts +2 -30
- package/dist/tool-handlers/chat-completion.js +13 -33
- package/dist/tool-handlers/chat-request.d.ts +25 -0
- package/dist/tool-handlers/chat-request.js +38 -0
- package/dist/tool-handlers/completion-utils.d.ts +1 -18
- package/dist/tool-handlers/completion-utils.js +0 -2
- package/dist/tool-handlers/fetch-utils.d.ts +2 -18
- package/dist/tool-handlers/fetch-utils.js +3 -51
- package/dist/tool-handlers/generate-audio.js +11 -21
- package/dist/tool-handlers/generate-image-dedicated.js +17 -65
- package/dist/tool-handlers/generate-image-input.d.ts +0 -1
- package/dist/tool-handlers/generate-image-input.js +2 -18
- package/dist/tool-handlers/generate-image.js +8 -15
- package/dist/tool-handlers/generate-video.d.ts +1 -7
- package/dist/tool-handlers/generate-video.js +24 -109
- package/dist/tool-handlers/health-check.d.ts +1 -9
- package/dist/tool-handlers/health-check.js +1 -12
- package/dist/tool-handlers/image-source.d.ts +14 -0
- package/dist/tool-handlers/image-source.js +23 -0
- package/dist/tool-handlers/image-utils.d.ts +5 -0
- package/dist/tool-handlers/image-utils.js +23 -0
- package/dist/tool-handlers/openai-withresponse.d.ts +1 -10
- package/dist/tool-handlers/openai-withresponse.js +0 -7
- package/dist/tool-handlers/openrouter-errors.d.ts +2 -18
- package/dist/tool-handlers/openrouter-errors.js +2 -34
- package/dist/tool-handlers/path-safety.d.ts +10 -15
- package/dist/tool-handlers/path-safety.js +30 -55
- package/dist/tool-handlers/provider-routing.d.ts +0 -9
- package/dist/tool-handlers/provider-routing.js +1 -14
- package/dist/tool-handlers/rerank.js +0 -2
- package/dist/tool-handlers/search-models.d.ts +0 -6
- package/dist/tool-handlers/speech-to-text.js +19 -29
- package/dist/tool-handlers/structured-output.d.ts +1 -4
- package/dist/tool-handlers/structured-output.js +2 -16
- package/dist/tool-handlers/text-to-speech.js +8 -26
- package/dist/tool-handlers/video-utils.d.ts +1 -6
- package/dist/tool-handlers/video-utils.js +2 -17
- package/dist/tool-handlers.js +9 -730
- package/dist/tool-icons.d.ts +9 -0
- package/dist/tool-icons.js +52 -0
- package/dist/version.d.ts +1 -15
- package/dist/version.js +1 -15
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="assets/logo.
|
|
2
|
+
<img src="assets/logo.png" alt="OpenRouter MCP Multimodal — MCP server for chat, vision, audio, and video AI tools" width="128" height="128" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<h1 align="center">OpenRouter MCP Multimodal</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<strong>The MCP server for multimodal AI agents.</strong><br/>
|
|
9
|
-
One install ·
|
|
9
|
+
One install · 19 tools · 300+ OpenRouter models · text, vision, audio & video — analysis and generation.
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
@@ -43,15 +43,15 @@
|
|
|
43
43
|
|
|
44
44
|
Unlike text-only MCP servers, one install covers the **full multimodal surface**:
|
|
45
45
|
|
|
46
|
-
| Capability | Tools | Highlights
|
|
47
|
-
| :---------- | :-------------------------------------------------------------------------------------- |
|
|
48
|
-
| **Chat** | `chat_completion`
|
|
49
|
-
| **Vision** | `analyze_image`, `generate_image`
|
|
50
|
-
| **Audio** | `analyze_audio`, `generate_audio`
|
|
51
|
-
| **Video** | `analyze_video`, `generate_video`, `generate_video_from_image`, `get_video_status` | Clip understanding, Veo
|
|
52
|
-
| **Catalog** | `search_models`, `get_model_info`, `validate_model`, `rerank_documents`, `health_check` | Model discovery, validation, reranking, ops health
|
|
46
|
+
| Capability | Tools | Highlights |
|
|
47
|
+
| :---------- | :-------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
48
|
+
| **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 |
|
|
49
|
+
| **Vision** | `analyze_image`, `generate_image`, `generate_image_dedicated` | OCR, captioning, VQA, image generation with reference inputs, dedicated Image API with resolution/quality/format control |
|
|
50
|
+
| **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) |
|
|
51
|
+
| **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 |
|
|
52
|
+
| **Catalog** | `search_models`, `get_model_info`, `validate_model`, `rerank_documents`, `health_check` | Model discovery, validation, reranking, ops health |
|
|
53
53
|
|
|
54
|
-
**Production hardening:** input/output path sandboxes (including analyze\_\* local files as of v4.5.2), SSRF guards, structured errors with `_meta.code`, MCP 2025-06-18 structured outputs, async video progress notifications, and **
|
|
54
|
+
**Production hardening:** input/output path sandboxes (including analyze\_\* local files as of v4.5.2), SSRF guards, structured errors with `_meta.code`, MCP 2025-06-18 structured outputs, tool icons (2025-11-25), async video progress notifications, and **770+** automated tests (unit, mock, regression, and live integration).
|
|
55
55
|
|
|
56
56
|
## Quick start
|
|
57
57
|
|
|
@@ -72,23 +72,23 @@ npx -y @stabgan/openrouter-mcp-multimodal
|
|
|
72
72
|
|
|
73
73
|
MCP servers are distributed through several packaging models. **This server is implemented in Node.js/TypeScript**; the table below maps each ecosystem method to how you run it here.
|
|
74
74
|
|
|
75
|
-
| Method
|
|
76
|
-
|
|
|
77
|
-
| **[npx](#npx-recommended)**
|
|
78
|
-
| **[uvx / pipx](#uvx--pipx-python-launcher)** | Python 3.10+ **and** Node.js 20+ | Python-first workflows, same pattern as PyPI MCP servers
|
|
79
|
-
| **[npm global](#npm-global)**
|
|
80
|
-
| **[node (local)](#node-local-clone)**
|
|
81
|
-
| **[Docker Hub](#docker)**
|
|
82
|
-
| **[GHCR](#ghcr-github-container-registry)**
|
|
83
|
-
| **[Smithery CLI](#smithery)**
|
|
84
|
-
| **[MCP Registry](#mcp-registry)**
|
|
85
|
-
| **[One-click deeplinks](#one-click)**
|
|
86
|
-
| **[Claude Code CLI](#claude-code-cli)**
|
|
87
|
-
| **[MCP Inspector](#mcp-inspector)**
|
|
88
|
-
| **Windows `cmd /c npx`**
|
|
89
|
-
| pip / uv (direct)
|
|
90
|
-
| DXT desktop extensions
|
|
91
|
-
| Remote HTTP / SSE
|
|
75
|
+
| Method | Runtime | Best for | This server |
|
|
76
|
+
| :------------------------------------------- | :------------------------------- | :------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------- |
|
|
77
|
+
| **[npx](#npx-recommended)** | Node.js 20+ | Most MCP clients (default) | ✅ `@stabgan/openrouter-mcp-multimodal` |
|
|
78
|
+
| **[uvx / pipx](#uvx--pipx-python-launcher)** | 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/) |
|
|
79
|
+
| **[npm global](#npm-global)** | Node.js 20+ | Pin a version without re-downloading | ✅ |
|
|
80
|
+
| **[node (local)](#node-local-clone)** | Node.js 20+ | Contributors / air-gapped builds | ✅ |
|
|
81
|
+
| **[Docker Hub](#docker)** | Docker | Isolation, no Node on host | ✅ `stabgan/openrouter-mcp-multimodal` |
|
|
82
|
+
| **[GHCR](#ghcr-github-container-registry)** | Docker | GitHub-native OCI pulls | ✅ `ghcr.io/stabgan/openrouter-mcp-multimodal` |
|
|
83
|
+
| **[Smithery CLI](#smithery)** | Node.js (via installer) | Interactive install into Claude/Cursor/etc. | ✅ |
|
|
84
|
+
| **[MCP Registry](#mcp-registry)** | npm or OCI | Official discovery (`io.github.stabgan/openrouter-multimodal`) | ✅ [listing](https://registry.modelcontextprotocol.io/servers/io.github.stabgan/openrouter-multimodal) |
|
|
85
|
+
| **[One-click deeplinks](#one-click)** | Node.js | Cursor, VS Code, Kiro | ✅ |
|
|
86
|
+
| **[Claude Code CLI](#claude-code-cli)** | Node.js | Terminal-first Claude Code users | ✅ |
|
|
87
|
+
| **[MCP Inspector](#mcp-inspector)** | Node.js | Debug / list tools locally | ✅ |
|
|
88
|
+
| **Windows `cmd /c npx`** | Node.js | Claude Desktop / Cursor when `npx` not on GUI PATH | ✅ [see below](#windows-npx) |
|
|
89
|
+
| pip / uv (direct) | — | Native Python MCP servers only | — use **uvx** row above |
|
|
90
|
+
| DXT desktop extensions | — | Bundled Claude Desktop `.dxt` | not yet |
|
|
91
|
+
| Remote HTTP / SSE | — | Hosted Smithery / Cloudflare endpoints | via [Smithery](https://smithery.ai/server/@stabgan/openrouter-mcp-multimodal) |
|
|
92
92
|
|
|
93
93
|
> **uvx vs npx:** In the MCP ecosystem, **`npx` runs npm (Node) packages** and **`uvx` runs PyPI (Python) packages**. Because this server is Node-based, `uvx` uses a thin [Python launcher](./python/) that execs `npx -y @stabgan/openrouter-mcp-multimodal` — you still need Node installed.
|
|
94
94
|
|
|
@@ -129,7 +129,7 @@ npx -y @stabgan/openrouter-mcp-multimodal
|
|
|
129
129
|
}
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
-
Pin a release: `"args": ["-y", "@stabgan/openrouter-mcp-multimodal@4.
|
|
132
|
+
Pin a release: `"args": ["-y", "@stabgan/openrouter-mcp-multimodal@4.6.2"]`
|
|
133
133
|
|
|
134
134
|
</details>
|
|
135
135
|
|
|
@@ -141,7 +141,7 @@ Install [uv](https://docs.astral.sh/uv/getting-started/installation/) (includes
|
|
|
141
141
|
```bash
|
|
142
142
|
export OPENROUTER_API_KEY=sk-or-v1-...
|
|
143
143
|
uvx mcp-server-openrouter-multimodal
|
|
144
|
-
# pin npm version: OPENROUTER_MCP_NPM_VERSION=4.
|
|
144
|
+
# pin npm version: OPENROUTER_MCP_NPM_VERSION=4.6.2 uvx mcp-server-openrouter-multimodal
|
|
145
145
|
```
|
|
146
146
|
|
|
147
147
|
```json
|
|
@@ -160,7 +160,7 @@ uvx mcp-server-openrouter-multimodal
|
|
|
160
160
|
|
|
161
161
|
**pipx equivalent:** `pipx run mcp-server-openrouter-multimodal`
|
|
162
162
|
|
|
163
|
-
Optional: `OPENROUTER_MCP_NPM_VERSION=4.
|
|
163
|
+
Optional: `OPENROUTER_MCP_NPM_VERSION=4.6.2` pins the underlying npm package.
|
|
164
164
|
|
|
165
165
|
</details>
|
|
166
166
|
|
|
@@ -241,7 +241,7 @@ Use `-i` (interactive stdio). Avoid `-t` (TTY corrupts MCP framing on some hosts
|
|
|
241
241
|
|
|
242
242
|
```bash
|
|
243
243
|
docker run --rm -i -e OPENROUTER_API_KEY=sk-or-v1-... \
|
|
244
|
-
ghcr.io/stabgan/openrouter-mcp-multimodal:4.
|
|
244
|
+
ghcr.io/stabgan/openrouter-mcp-multimodal:4.6.2
|
|
245
245
|
```
|
|
246
246
|
|
|
247
247
|
```json
|
|
@@ -250,8 +250,11 @@ docker run --rm -i -e OPENROUTER_API_KEY=sk-or-v1-... \
|
|
|
250
250
|
"openrouter": {
|
|
251
251
|
"command": "docker",
|
|
252
252
|
"args": [
|
|
253
|
-
"run",
|
|
254
|
-
"
|
|
253
|
+
"run",
|
|
254
|
+
"--rm",
|
|
255
|
+
"-i",
|
|
256
|
+
"-e",
|
|
257
|
+
"OPENROUTER_API_KEY=sk-or-v1-...",
|
|
255
258
|
"ghcr.io/stabgan/openrouter-mcp-multimodal:latest"
|
|
256
259
|
]
|
|
257
260
|
}
|
|
@@ -349,24 +352,29 @@ If still failing, use the full path from `where npx` as the command.
|
|
|
349
352
|
|
|
350
353
|
## Tools
|
|
351
354
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
| Tool
|
|
355
|
-
|
|
|
356
|
-
| `chat_completion`
|
|
357
|
-
| `
|
|
358
|
-
| `
|
|
359
|
-
| `
|
|
360
|
-
| `
|
|
361
|
-
| `
|
|
362
|
-
| `
|
|
363
|
-
| `
|
|
364
|
-
| `
|
|
365
|
-
| `
|
|
366
|
-
| `
|
|
367
|
-
| `
|
|
368
|
-
| `
|
|
369
|
-
| `
|
|
355
|
+
19 MCP tools. Each description includes **Use when**, **Good/Bad examples**, **Fails when**, and **Works with** so agents pick the right tool and recover from errors.
|
|
356
|
+
|
|
357
|
+
| Tool | Purpose |
|
|
358
|
+
| :--------------------------- | :------------------------------------------------------------------------- |
|
|
359
|
+
| `chat_completion` | Text chat, web search, provider routing, caching, reasoning |
|
|
360
|
+
| `start_chat_completion` | Async background job for long-running reasoning models |
|
|
361
|
+
| `get_chat_completion_status` | Poll / retrieve async completion results |
|
|
362
|
+
| `analyze_image` | Vision — local path, URL, or data URL + `question` |
|
|
363
|
+
| `analyze_audio` | Transcribe / analyze audio files |
|
|
364
|
+
| `analyze_video` | Describe / Q&A over video files |
|
|
365
|
+
| `generate_image` | Text-to-image via chat completions with reference images |
|
|
366
|
+
| `generate_image_dedicated` | Text-to-image via dedicated `/api/v1/images` (resolution, quality, format) |
|
|
367
|
+
| `generate_audio` | Text-to-speech / music via chat completions |
|
|
368
|
+
| `text_to_speech` | Dedicated TTS (`/api/v1/audio/speech`) — voices, speed, format |
|
|
369
|
+
| `speech_to_text` | Dedicated STT (`/api/v1/audio/transcriptions`) — Whisper, GPT-4o |
|
|
370
|
+
| `generate_video` | Text-to-video (async, resumable) |
|
|
371
|
+
| `generate_video_from_image` | Image-to-video (narrower schema) |
|
|
372
|
+
| `get_video_status` | Poll / resume video jobs |
|
|
373
|
+
| `search_models` | Paginated model catalog search |
|
|
374
|
+
| `get_model_info` | Pricing, context, modalities |
|
|
375
|
+
| `validate_model` | Cheap model ID existence check |
|
|
376
|
+
| `rerank_documents` | Relevance ranking for RAG |
|
|
377
|
+
| `health_check` | API key + reachability probe |
|
|
370
378
|
|
|
371
379
|
Errors use a closed `_meta.code` taxonomy: `INVALID_INPUT` · `UNSAFE_PATH` · `UPSTREAM_*` · `MODEL_NOT_FOUND` · `JOB_STILL_RUNNING` · and more.
|
|
372
380
|
|
|
@@ -471,7 +479,7 @@ npm run build
|
|
|
471
479
|
|
|
472
480
|
| Command | What it runs |
|
|
473
481
|
| :------------------------- | :--------------------------------------------------------- |
|
|
474
|
-
| `npm test` | **
|
|
482
|
+
| `npm test` | **773** unit + mock tests (no API key, <20s) |
|
|
475
483
|
| `npm run test:regression` | Security + schema regression guards |
|
|
476
484
|
| `npm run test:integration` | **16** live OpenRouter scenarios (**requires** `.env` key) |
|
|
477
485
|
| `npm run test:e2e` | Full MCP stdio smoke (`scripts/live-e2e.mjs`) |
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Closed error-code taxonomy for MCP tool responses.
|
|
3
|
-
* `toolError(
|
|
4
|
-
* clients can switch on `_meta.code` without regex-parsing free text.
|
|
5
|
-
*
|
|
6
|
-
* Adding a new code requires a design.md note — this set is intentionally
|
|
7
|
-
* small and stable.
|
|
2
|
+
* Closed error-code taxonomy for MCP tool responses.
|
|
3
|
+
* Every handler uses `toolError()` so clients can switch on `_meta.code`.
|
|
8
4
|
*/
|
|
9
5
|
export declare const ErrorCode: {
|
|
10
6
|
readonly INVALID_INPUT: "INVALID_INPUT";
|
|
@@ -24,12 +20,7 @@ export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
|
24
20
|
export interface ToolErrorMeta {
|
|
25
21
|
code: ErrorCode;
|
|
26
22
|
details?: Record<string, unknown>;
|
|
27
|
-
/**
|
|
28
|
-
* Optional human-readable next steps the agent can take. Inspired by
|
|
29
|
-
* the Apigene "structured errors" best practice: rather than a raw
|
|
30
|
-
* string for the agent to interpret, list concrete options like
|
|
31
|
-
* "Wait and retry" or "Lower resolution to 480p".
|
|
32
|
-
*/
|
|
23
|
+
/** Optional next steps for the agent (e.g. "Wait and retry"). */
|
|
33
24
|
suggestions?: string[];
|
|
34
25
|
/**
|
|
35
26
|
* For rate-limit / backoff errors, the number of seconds the caller
|
package/dist/errors.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Closed error-code taxonomy for MCP tool responses.
|
|
3
|
-
* `toolError(
|
|
4
|
-
* clients can switch on `_meta.code` without regex-parsing free text.
|
|
5
|
-
*
|
|
6
|
-
* Adding a new code requires a design.md note — this set is intentionally
|
|
7
|
-
* small and stable.
|
|
2
|
+
* Closed error-code taxonomy for MCP tool responses.
|
|
3
|
+
* Every handler uses `toolError()` so clients can switch on `_meta.code`.
|
|
8
4
|
*/
|
|
9
5
|
export const ErrorCode = {
|
|
10
6
|
INVALID_INPUT: 'INVALID_INPUT',
|
package/dist/index.js
CHANGED
|
@@ -7,19 +7,15 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
7
7
|
import { ToolHandlers } from './tool-handlers.js';
|
|
8
8
|
import { logger } from './logger.js';
|
|
9
9
|
import { SERVER_VERSION } from './version.js';
|
|
10
|
+
import { SERVER_ICON } from './tool-icons.js';
|
|
10
11
|
const DEFAULT_MODEL = 'nvidia/nemotron-nano-12b-v2-vl:free';
|
|
11
|
-
//
|
|
12
|
-
// We log an explicit whitelist of fields rather than the raw error object
|
|
13
|
-
// to avoid ever echoing sensitive SDK internals (request bodies, auth
|
|
14
|
-
// headers) in a future version. Defense-in-depth against a changed
|
|
15
|
-
// APIError.toString() in openai-node.
|
|
12
|
+
// Log whitelisted fields only — avoid leaking auth headers from SDK errors.
|
|
16
13
|
function logFatal(kind, err) {
|
|
17
14
|
const e = err;
|
|
18
15
|
logger.error('fatal', {
|
|
19
16
|
kind,
|
|
20
17
|
name: e?.name ?? 'unknown',
|
|
21
18
|
msg: e?.message ?? String(err),
|
|
22
|
-
// Stack traces are developer-only — trim to avoid unbounded log lines.
|
|
23
19
|
stack: e?.stack?.split('\n').slice(0, 10).join('\n'),
|
|
24
20
|
});
|
|
25
21
|
}
|
|
@@ -37,17 +33,21 @@ if (!apiKey) {
|
|
|
37
33
|
process.exit(1);
|
|
38
34
|
}
|
|
39
35
|
const defaultModel = process.env.OPENROUTER_DEFAULT_MODEL || process.env.DEFAULT_MODEL || DEFAULT_MODEL;
|
|
40
|
-
const server = new Server({
|
|
36
|
+
const server = new Server({
|
|
37
|
+
name: 'openrouter-multimodal-server',
|
|
38
|
+
version: SERVER_VERSION,
|
|
39
|
+
title: 'OpenRouter MCP Multimodal',
|
|
40
|
+
description: 'MCP server for OpenRouter — chat with 300+ LLMs, analyze/generate images, audio, and video.',
|
|
41
|
+
websiteUrl: 'https://github.com/stabgan/openrouter-mcp-multimodal',
|
|
42
|
+
icons: SERVER_ICON,
|
|
43
|
+
}, { capabilities: { tools: {} } });
|
|
41
44
|
server.onerror = (error) => logFatal('mcpError', error);
|
|
42
45
|
new ToolHandlers(server, apiKey, defaultModel);
|
|
43
46
|
process.on('SIGINT', async () => {
|
|
44
47
|
await server.close();
|
|
45
48
|
process.exit(0);
|
|
46
49
|
});
|
|
47
|
-
//
|
|
48
|
-
// encoding on the stdin pipe, which causes the MCP SDK's ReadBuffer to receive
|
|
49
|
-
// strings instead of Buffers. ReadBuffer.readMessage() calls subarray() which
|
|
50
|
-
// doesn't exist on strings, triggering an infinite error loop.
|
|
50
|
+
// Stdin may arrive as strings on some MCP hosts; re-wrap as raw Buffers for the SDK.
|
|
51
51
|
const stdinStream = process.stdin;
|
|
52
52
|
stdinStream.setEncoding?.(undefined);
|
|
53
53
|
const safeStdin = new Readable({
|
package/dist/logger.d.ts
CHANGED
|
@@ -1,16 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Stderr-bound JSON line logger. stdout is
|
|
3
|
-
* go to stderr or the client will choke. Output is one JSON object per line:
|
|
4
|
-
*
|
|
5
|
-
* {"ts":"2026-04-20T14:03:10.123Z","level":"info","msg":"job_submitted",
|
|
6
|
-
* "ctx":{"model":"google/veo-3.1","id":"vid_abc"}}
|
|
7
|
-
*
|
|
8
|
-
* Level is filtered by OPENROUTER_LOG_LEVEL (error|warn|info|debug,
|
|
9
|
-
* default info). Unknown values fall through to info.
|
|
10
|
-
*
|
|
11
|
-
* `audit` is a special level that ALWAYS writes (bypasses the level filter),
|
|
12
|
-
* intended for cost-incurring / destructive operations so operators can
|
|
13
|
-
* trace them after the fact.
|
|
2
|
+
* Stderr-bound JSON line logger. stdout is reserved for MCP transport.
|
|
14
3
|
*/
|
|
15
4
|
export type LogLevel = 'error' | 'warn' | 'info' | 'debug';
|
|
16
5
|
/** Low-level write hook, replaceable in tests. */
|
|
@@ -23,11 +12,5 @@ export declare const logger: {
|
|
|
23
12
|
warn: (msg: string, ctx?: Record<string, unknown>) => void;
|
|
24
13
|
info: (msg: string, ctx?: Record<string, unknown>) => void;
|
|
25
14
|
debug: (msg: string, ctx?: Record<string, unknown>) => void;
|
|
26
|
-
/**
|
|
27
|
-
* Always-on audit line. Bypasses OPENROUTER_LOG_LEVEL. Use for paid or
|
|
28
|
-
* destructive operations (generate_video, generate_audio, generate_image)
|
|
29
|
-
* so operators can trace unintended spend via `docker logs` or a log
|
|
30
|
-
* aggregator.
|
|
31
|
-
*/
|
|
32
15
|
audit(msg: string, ctx?: Record<string, unknown>): void;
|
|
33
16
|
};
|
package/dist/logger.js
CHANGED
|
@@ -30,7 +30,6 @@ export function log(level, msg, ctx) {
|
|
|
30
30
|
_sink.write(JSON.stringify(record));
|
|
31
31
|
}
|
|
32
32
|
catch {
|
|
33
|
-
// Fall back to a short-form record if `ctx` contains something unserializable.
|
|
34
33
|
_sink.write(JSON.stringify({
|
|
35
34
|
ts: new Date().toISOString(),
|
|
36
35
|
level,
|
|
@@ -44,12 +43,6 @@ export const logger = {
|
|
|
44
43
|
warn: (msg, ctx) => log('warn', msg, ctx),
|
|
45
44
|
info: (msg, ctx) => log('info', msg, ctx),
|
|
46
45
|
debug: (msg, ctx) => log('debug', msg, ctx),
|
|
47
|
-
/**
|
|
48
|
-
* Always-on audit line. Bypasses OPENROUTER_LOG_LEVEL. Use for paid or
|
|
49
|
-
* destructive operations (generate_video, generate_audio, generate_image)
|
|
50
|
-
* so operators can trace unintended spend via `docker logs` or a log
|
|
51
|
-
* aggregator.
|
|
52
|
-
*/
|
|
53
46
|
audit(msg, ctx) {
|
|
54
47
|
const record = {
|
|
55
48
|
ts: new Date().toISOString(),
|
package/dist/model-cache.d.ts
CHANGED
|
@@ -25,38 +25,19 @@ export declare class ModelCache {
|
|
|
25
25
|
private static instance;
|
|
26
26
|
private models;
|
|
27
27
|
private fetchedAt;
|
|
28
|
-
/**
|
|
29
|
-
* Separate from `fetchedAt`: set whenever we successfully CALL the
|
|
30
|
-
* fetcher (even if the response happens to be empty). Used by
|
|
31
|
-
* `isValid()` so a successful-but-empty fetch still counts as "fresh"
|
|
32
|
-
* and we don't hot-loop re-fetching the upstream.
|
|
33
|
-
*/
|
|
34
28
|
private populatedAt;
|
|
35
29
|
private inflight;
|
|
36
30
|
static getInstance(): ModelCache;
|
|
37
31
|
isValid(): boolean;
|
|
38
32
|
setModels(models: OpenRouterModelRecord[]): void;
|
|
39
|
-
/**
|
|
40
|
-
* Force the cache back into an uninitialized state. Used by tests that
|
|
41
|
-
* need to assert `ensureFresh()` actually calls the fetcher. Also useful
|
|
42
|
-
* for ops (`health_check --reset`) if we ever expose such a knob.
|
|
43
|
-
*/
|
|
33
|
+
/** Reset cache state (tests). */
|
|
44
34
|
reset(): void;
|
|
45
|
-
/**
|
|
46
|
-
* Populate the cache using `fetcher` if stale, coalescing concurrent callers
|
|
47
|
-
* so only one request hits the upstream API per stale window. Callers that
|
|
48
|
-
* arrive while a populate is in flight await the same promise.
|
|
49
|
-
*/
|
|
50
35
|
ensureFresh(fetcher: () => Promise<OpenRouterModelRecord[]>): Promise<void>;
|
|
51
36
|
getAll(): OpenRouterModelRecord[];
|
|
52
37
|
/** Number of models currently cached. Used by health_check. */
|
|
53
38
|
size(): number;
|
|
54
39
|
get(id: string): OpenRouterModelRecord | null;
|
|
55
40
|
has(id: string): boolean;
|
|
56
|
-
/**
|
|
57
|
-
* Single-pass paginated search: O(n) time, O(limit) extra space for the page.
|
|
58
|
-
* Avoids materializing the full filtered array when only one page is needed.
|
|
59
|
-
*/
|
|
60
41
|
searchPaginated(params: ModelSearchParams, offset: number, limit: number): {
|
|
61
42
|
page: OpenRouterModelRecord[];
|
|
62
43
|
total: number;
|
package/dist/model-cache.js
CHANGED
|
@@ -36,12 +36,6 @@ export class ModelCache {
|
|
|
36
36
|
static instance;
|
|
37
37
|
models = {};
|
|
38
38
|
fetchedAt = 0;
|
|
39
|
-
/**
|
|
40
|
-
* Separate from `fetchedAt`: set whenever we successfully CALL the
|
|
41
|
-
* fetcher (even if the response happens to be empty). Used by
|
|
42
|
-
* `isValid()` so a successful-but-empty fetch still counts as "fresh"
|
|
43
|
-
* and we don't hot-loop re-fetching the upstream.
|
|
44
|
-
*/
|
|
45
39
|
populatedAt = 0;
|
|
46
40
|
inflight = null;
|
|
47
41
|
static getInstance() {
|
|
@@ -56,22 +50,13 @@ export class ModelCache {
|
|
|
56
50
|
this.fetchedAt = Date.now();
|
|
57
51
|
this.populatedAt = this.fetchedAt;
|
|
58
52
|
}
|
|
59
|
-
/**
|
|
60
|
-
* Force the cache back into an uninitialized state. Used by tests that
|
|
61
|
-
* need to assert `ensureFresh()` actually calls the fetcher. Also useful
|
|
62
|
-
* for ops (`health_check --reset`) if we ever expose such a knob.
|
|
63
|
-
*/
|
|
53
|
+
/** Reset cache state (tests). */
|
|
64
54
|
reset() {
|
|
65
55
|
this.models = {};
|
|
66
56
|
this.fetchedAt = 0;
|
|
67
57
|
this.populatedAt = 0;
|
|
68
58
|
this.inflight = null;
|
|
69
59
|
}
|
|
70
|
-
/**
|
|
71
|
-
* Populate the cache using `fetcher` if stale, coalescing concurrent callers
|
|
72
|
-
* so only one request hits the upstream API per stale window. Callers that
|
|
73
|
-
* arrive while a populate is in flight await the same promise.
|
|
74
|
-
*/
|
|
75
60
|
async ensureFresh(fetcher) {
|
|
76
61
|
if (this.isValid())
|
|
77
62
|
return;
|
|
@@ -101,10 +86,6 @@ export class ModelCache {
|
|
|
101
86
|
has(id) {
|
|
102
87
|
return id in this.models;
|
|
103
88
|
}
|
|
104
|
-
/**
|
|
105
|
-
* Single-pass paginated search: O(n) time, O(limit) extra space for the page.
|
|
106
|
-
* Avoids materializing the full filtered array when only one page is needed.
|
|
107
|
-
*/
|
|
108
89
|
searchPaginated(params, offset, limit) {
|
|
109
90
|
const matches = buildMatcher(params);
|
|
110
91
|
const safeOffset = Math.max(0, offset);
|
package/dist/openrouter-api.d.ts
CHANGED
|
@@ -21,33 +21,19 @@ export declare class OpenRouterAPIClient {
|
|
|
21
21
|
submitVideoJob(body: Record<string, unknown>): Promise<VideoJobEnvelope>;
|
|
22
22
|
/** Poll a submitted video-generation job by id. */
|
|
23
23
|
pollVideoJob(id: string): Promise<VideoJobStatus>;
|
|
24
|
-
/** Download
|
|
25
|
-
* This intentionally does NOT go through our SSRF-guarded `fetchHttpResource`
|
|
26
|
-
* because the URL is always OpenRouter itself (trusted origin) — and it can
|
|
27
|
-
* return arbitrarily large bodies that the caller bounds via
|
|
28
|
-
* `OPENROUTER_VIDEO_MAX_DOWNLOAD_BYTES`.
|
|
29
|
-
*/
|
|
24
|
+
/** Download generated video binary (trusted OpenRouter origin). */
|
|
30
25
|
downloadVideoContent(id: string, index?: number, maxBytes?: number): Promise<{
|
|
31
26
|
buffer: Buffer;
|
|
32
27
|
contentType: string | null;
|
|
33
28
|
}>;
|
|
34
|
-
/**
|
|
35
|
-
* POST /images — dedicated image generation endpoint.
|
|
36
|
-
* Returns structured response with base64 image data.
|
|
37
|
-
*/
|
|
29
|
+
/** POST /images — dedicated image generation. */
|
|
38
30
|
generateImage(body: Record<string, unknown>, headers?: Record<string, string>): Promise<ImageGenerationResponse>;
|
|
39
|
-
/**
|
|
40
|
-
* POST /audio/speech — dedicated text-to-speech endpoint.
|
|
41
|
-
* Returns raw audio bytes.
|
|
42
|
-
*/
|
|
31
|
+
/** POST /audio/speech — text-to-speech. */
|
|
43
32
|
generateSpeech(body: Record<string, unknown>, headers?: Record<string, string>): Promise<{
|
|
44
33
|
buffer: Buffer;
|
|
45
34
|
contentType: string;
|
|
46
35
|
}>;
|
|
47
|
-
/**
|
|
48
|
-
* POST /audio/transcriptions — dedicated speech-to-text endpoint.
|
|
49
|
-
* Accepts base64-encoded audio and returns transcription text.
|
|
50
|
-
*/
|
|
36
|
+
/** POST /audio/transcriptions — speech-to-text. */
|
|
51
37
|
transcribeAudio(body: Record<string, unknown>, headers?: Record<string, string>): Promise<TranscriptionResponse>;
|
|
52
38
|
/** POST /rerank — re-order documents by relevance to a query. */
|
|
53
39
|
rerank(params: {
|
package/dist/openrouter-api.js
CHANGED
|
@@ -39,8 +39,6 @@ async function fetchWithRetry(url, init, { retries = 2, timeoutMs = DEFAULT_TIME
|
|
|
39
39
|
if (res.status === 429 || res.status >= 500) {
|
|
40
40
|
if (attempt < retries) {
|
|
41
41
|
const retryAfter = parseRetryAfter(res.headers.get('retry-after'));
|
|
42
|
-
// Release the connection before retrying so undici/pool doesn't
|
|
43
|
-
// keep it open while we sleep.
|
|
44
42
|
try {
|
|
45
43
|
await res.body?.cancel();
|
|
46
44
|
}
|
|
@@ -104,12 +102,7 @@ export class OpenRouterAPIClient {
|
|
|
104
102
|
}
|
|
105
103
|
return (await res.json());
|
|
106
104
|
}
|
|
107
|
-
/** Download
|
|
108
|
-
* This intentionally does NOT go through our SSRF-guarded `fetchHttpResource`
|
|
109
|
-
* because the URL is always OpenRouter itself (trusted origin) — and it can
|
|
110
|
-
* return arbitrarily large bodies that the caller bounds via
|
|
111
|
-
* `OPENROUTER_VIDEO_MAX_DOWNLOAD_BYTES`.
|
|
112
|
-
*/
|
|
105
|
+
/** Download generated video binary (trusted OpenRouter origin). */
|
|
113
106
|
async downloadVideoContent(id, index = 0, maxBytes = 256 * 1024 * 1024) {
|
|
114
107
|
const url = `${BASE_URL}/videos/${encodeURIComponent(id)}/content?index=${index}`;
|
|
115
108
|
const res = await fetchWithRetry(url, { headers: this.authHeaders() }, { retries: 1, timeoutMs: VIDEO_TIMEOUT_MS * 2 });
|
|
@@ -151,10 +144,7 @@ export class OpenRouterAPIClient {
|
|
|
151
144
|
}
|
|
152
145
|
return { buffer: Buffer.concat(chunks), contentType: res.headers.get('content-type') };
|
|
153
146
|
}
|
|
154
|
-
/**
|
|
155
|
-
* POST /images — dedicated image generation endpoint.
|
|
156
|
-
* Returns structured response with base64 image data.
|
|
157
|
-
*/
|
|
147
|
+
/** POST /images — dedicated image generation. */
|
|
158
148
|
async generateImage(body, headers) {
|
|
159
149
|
const res = await fetchWithRetry(`${BASE_URL}/images`, {
|
|
160
150
|
method: 'POST',
|
|
@@ -167,10 +157,7 @@ export class OpenRouterAPIClient {
|
|
|
167
157
|
}
|
|
168
158
|
return (await res.json());
|
|
169
159
|
}
|
|
170
|
-
/**
|
|
171
|
-
* POST /audio/speech — dedicated text-to-speech endpoint.
|
|
172
|
-
* Returns raw audio bytes.
|
|
173
|
-
*/
|
|
160
|
+
/** POST /audio/speech — text-to-speech. */
|
|
174
161
|
async generateSpeech(body, headers) {
|
|
175
162
|
const res = await fetchWithRetry(`${BASE_URL}/audio/speech`, {
|
|
176
163
|
method: 'POST',
|
|
@@ -185,10 +172,7 @@ export class OpenRouterAPIClient {
|
|
|
185
172
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
186
173
|
return { buffer: buf, contentType };
|
|
187
174
|
}
|
|
188
|
-
/**
|
|
189
|
-
* POST /audio/transcriptions — dedicated speech-to-text endpoint.
|
|
190
|
-
* Accepts base64-encoded audio and returns transcription text.
|
|
191
|
-
*/
|
|
175
|
+
/** POST /audio/transcriptions — speech-to-text. */
|
|
192
176
|
async transcribeAudio(body, headers) {
|
|
193
177
|
const res = await fetchWithRetry(`${BASE_URL}/audio/transcriptions`, {
|
|
194
178
|
method: 'POST',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const TOOL_DEFINITIONS: Array<Record<string, unknown>>;
|