@stabgan/openrouter-mcp-multimodal 4.6.2 → 4.8.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 +108 -37
- package/dist/errors.d.ts +5 -20
- package/dist/errors.js +1 -10
- package/dist/index.js +7 -1
- package/dist/logger.js +54 -24
- package/dist/model-cache.d.ts +13 -0
- package/dist/model-cache.js +61 -5
- package/dist/openrouter-api.d.ts +14 -15
- package/dist/openrouter-api.js +68 -22
- package/dist/openrouter-openai-client.d.ts +9 -0
- package/dist/openrouter-openai-client.js +15 -0
- package/dist/tool-definitions.d.ts +24 -0
- package/dist/tool-definitions.js +283 -170
- package/dist/tool-descriptions.js +23 -15
- package/dist/tool-handlers/analyze-audio.js +4 -1
- package/dist/tool-handlers/analyze-image.js +10 -5
- package/dist/tool-handlers/analyze-video.js +9 -5
- package/dist/tool-handlers/async-chat.d.ts +17 -0
- package/dist/tool-handlers/async-chat.js +112 -31
- package/dist/tool-handlers/audio-utils.d.ts +19 -4
- package/dist/tool-handlers/audio-utils.js +170 -16
- package/dist/tool-handlers/cache.d.ts +3 -3
- package/dist/tool-handlers/cache.js +56 -4
- package/dist/tool-handlers/chat-completion.js +16 -7
- package/dist/tool-handlers/chat-request.d.ts +3 -0
- package/dist/tool-handlers/chat-request.js +28 -0
- package/dist/tool-handlers/completion-utils.d.ts +5 -11
- package/dist/tool-handlers/completion-utils.js +76 -47
- package/dist/tool-handlers/fetch-utils.d.ts +14 -0
- package/dist/tool-handlers/fetch-utils.js +331 -68
- package/dist/tool-handlers/generate-audio.d.ts +6 -51
- package/dist/tool-handlers/generate-audio.js +30 -76
- package/dist/tool-handlers/generate-image-dedicated.d.ts +2 -12
- package/dist/tool-handlers/generate-image-dedicated.js +79 -28
- package/dist/tool-handlers/generate-image.d.ts +3 -46
- package/dist/tool-handlers/generate-image.js +26 -43
- package/dist/tool-handlers/generate-video.d.ts +4 -3
- package/dist/tool-handlers/generate-video.js +51 -50
- package/dist/tool-handlers/get-model-info.js +1 -1
- package/dist/tool-handlers/health-check.js +39 -15
- package/dist/tool-handlers/image-utils.js +2 -2
- package/dist/tool-handlers/openrouter-errors.d.ts +2 -0
- package/dist/tool-handlers/openrouter-errors.js +138 -31
- package/dist/tool-handlers/path-safety.d.ts +6 -0
- package/dist/tool-handlers/path-safety.js +77 -14
- package/dist/tool-handlers/path-utils.d.ts +4 -0
- package/dist/tool-handlers/path-utils.js +20 -0
- package/dist/tool-handlers/provider-routing.d.ts +2 -0
- package/dist/tool-handlers/provider-routing.js +10 -0
- package/dist/tool-handlers/rerank.d.ts +1 -4
- package/dist/tool-handlers/rerank.js +43 -14
- package/dist/tool-handlers/search-models.js +3 -3
- package/dist/tool-handlers/speech-to-text.d.ts +1 -0
- package/dist/tool-handlers/speech-to-text.js +23 -56
- package/dist/tool-handlers/text-to-speech.d.ts +2 -12
- package/dist/tool-handlers/text-to-speech.js +29 -22
- package/dist/tool-handlers/tool-result-payload.d.ts +47 -0
- package/dist/tool-handlers/tool-result-payload.js +98 -0
- package/dist/tool-handlers/validate-model.js +1 -1
- package/dist/tool-handlers.d.ts +9 -0
- package/dist/tool-handlers.js +19 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -3
package/dist/openrouter-api.d.ts
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import type { OpenRouterModelRecord } from './model-cache.js';
|
|
2
2
|
declare function parseRetryAfter(headerValue: string | null): number | null;
|
|
3
3
|
declare function backoffWithJitter(attempt: number, retryAfterMs: number | null): number;
|
|
4
|
-
/**
|
|
5
|
-
* fetch() wrapper with retries on 429 / 5xx / network error.
|
|
6
|
-
*
|
|
7
|
-
* A fresh `AbortSignal.timeout(timeoutMs)` is created per attempt so retries
|
|
8
|
-
* each get a full timeout budget. Backoff honors `Retry-After` (seconds or
|
|
9
|
-
* HTTP-date) and applies jitter to avoid thundering-herd synchronization.
|
|
10
|
-
*/
|
|
11
4
|
declare function fetchWithRetry(url: string, init: Omit<RequestInit, 'signal'>, { retries, timeoutMs }?: {
|
|
12
5
|
retries?: number;
|
|
13
6
|
timeoutMs?: number;
|
|
@@ -17,25 +10,25 @@ export declare class OpenRouterAPIClient {
|
|
|
17
10
|
constructor(apiKey: string);
|
|
18
11
|
private authHeaders;
|
|
19
12
|
getModels(): Promise<OpenRouterModelRecord[]>;
|
|
20
|
-
/** Submit a video-generation job.
|
|
13
|
+
/** Submit a video-generation job. */
|
|
21
14
|
submitVideoJob(body: Record<string, unknown>): Promise<VideoJobEnvelope>;
|
|
22
|
-
/** Poll a submitted video-generation job
|
|
15
|
+
/** Poll a submitted video-generation job. */
|
|
23
16
|
pollVideoJob(id: string): Promise<VideoJobStatus>;
|
|
24
|
-
/** Download generated video binary
|
|
17
|
+
/** Download generated video binary. */
|
|
25
18
|
downloadVideoContent(id: string, index?: number, maxBytes?: number): Promise<{
|
|
26
19
|
buffer: Buffer;
|
|
27
20
|
contentType: string | null;
|
|
28
21
|
}>;
|
|
29
|
-
/** POST /images
|
|
22
|
+
/** POST /images. */
|
|
30
23
|
generateImage(body: Record<string, unknown>, headers?: Record<string, string>): Promise<ImageGenerationResponse>;
|
|
31
|
-
/** POST /audio/speech
|
|
24
|
+
/** POST /audio/speech. */
|
|
32
25
|
generateSpeech(body: Record<string, unknown>, headers?: Record<string, string>): Promise<{
|
|
33
26
|
buffer: Buffer;
|
|
34
27
|
contentType: string;
|
|
35
28
|
}>;
|
|
36
|
-
/** POST /audio/transcriptions
|
|
29
|
+
/** POST /audio/transcriptions. */
|
|
37
30
|
transcribeAudio(body: Record<string, unknown>, headers?: Record<string, string>): Promise<TranscriptionResponse>;
|
|
38
|
-
/** POST /rerank
|
|
31
|
+
/** POST /rerank. */
|
|
39
32
|
rerank(params: {
|
|
40
33
|
model: string;
|
|
41
34
|
query: string;
|
|
@@ -90,7 +83,7 @@ export interface RerankResponse {
|
|
|
90
83
|
usage?: Record<string, unknown>;
|
|
91
84
|
[key: string]: unknown;
|
|
92
85
|
}
|
|
93
|
-
export type VideoJobStatusName = 'pending' | 'queued' | 'processing' | 'completed' | 'failed';
|
|
86
|
+
export type VideoJobStatusName = 'pending' | 'queued' | 'processing' | 'completed' | 'failed' | 'cancelled' | 'canceled';
|
|
94
87
|
export interface VideoJobStatus {
|
|
95
88
|
id: string;
|
|
96
89
|
status: VideoJobStatusName | string;
|
|
@@ -103,9 +96,15 @@ export interface VideoJobStatus {
|
|
|
103
96
|
progress?: number;
|
|
104
97
|
[key: string]: unknown;
|
|
105
98
|
}
|
|
99
|
+
declare function extractEmbeddedError(data: unknown): string | undefined;
|
|
100
|
+
declare function readTranscriptionResponse(res: Response, responseFormat: unknown, context: string): Promise<TranscriptionResponse>;
|
|
101
|
+
declare function readJsonOrThrow<T>(res: Response, context: string): Promise<T>;
|
|
106
102
|
export declare const _internals: {
|
|
107
103
|
parseRetryAfter: typeof parseRetryAfter;
|
|
108
104
|
backoffWithJitter: typeof backoffWithJitter;
|
|
109
105
|
fetchWithRetry: typeof fetchWithRetry;
|
|
106
|
+
extractEmbeddedError: typeof extractEmbeddedError;
|
|
107
|
+
readJsonOrThrow: typeof readJsonOrThrow;
|
|
108
|
+
readTranscriptionResponse: typeof readTranscriptionResponse;
|
|
110
109
|
};
|
|
111
110
|
export {};
|
package/dist/openrouter-api.js
CHANGED
|
@@ -24,13 +24,6 @@ function backoffWithJitter(attempt, retryAfterMs) {
|
|
|
24
24
|
const jitter = 0.5 + Math.random(); // 0.5x .. 1.5x
|
|
25
25
|
return Math.round(target * jitter);
|
|
26
26
|
}
|
|
27
|
-
/**
|
|
28
|
-
* fetch() wrapper with retries on 429 / 5xx / network error.
|
|
29
|
-
*
|
|
30
|
-
* A fresh `AbortSignal.timeout(timeoutMs)` is created per attempt so retries
|
|
31
|
-
* each get a full timeout budget. Backoff honors `Retry-After` (seconds or
|
|
32
|
-
* HTTP-date) and applies jitter to avoid thundering-herd synchronization.
|
|
33
|
-
*/
|
|
34
27
|
async function fetchWithRetry(url, init, { retries = 2, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
35
28
|
let lastErr;
|
|
36
29
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
@@ -77,10 +70,10 @@ export class OpenRouterAPIClient {
|
|
|
77
70
|
const res = await fetchWithRetry(`${BASE_URL}/models`, { headers: this.authHeaders() }, { retries: 2, timeoutMs: DEFAULT_TIMEOUT_MS });
|
|
78
71
|
if (!res.ok)
|
|
79
72
|
throw new Error(`Failed to fetch models: HTTP ${res.status}`);
|
|
80
|
-
const data =
|
|
73
|
+
const data = await readJsonOrThrow(res, 'GET /models');
|
|
81
74
|
return data.data ?? [];
|
|
82
75
|
}
|
|
83
|
-
/** Submit a video-generation job.
|
|
76
|
+
/** Submit a video-generation job. */
|
|
84
77
|
async submitVideoJob(body) {
|
|
85
78
|
const res = await fetchWithRetry(`${BASE_URL}/videos`, {
|
|
86
79
|
method: 'POST',
|
|
@@ -91,18 +84,18 @@ export class OpenRouterAPIClient {
|
|
|
91
84
|
const detail = await safeReadText(res);
|
|
92
85
|
throw new Error(`POST /videos failed: HTTP ${res.status}${detail ? ` — ${detail}` : ''}`);
|
|
93
86
|
}
|
|
94
|
-
return (
|
|
87
|
+
return readJsonOrThrow(res, 'POST /videos');
|
|
95
88
|
}
|
|
96
|
-
/** Poll a submitted video-generation job
|
|
89
|
+
/** Poll a submitted video-generation job. */
|
|
97
90
|
async pollVideoJob(id) {
|
|
98
91
|
const res = await fetchWithRetry(`${BASE_URL}/videos/${encodeURIComponent(id)}`, { headers: this.authHeaders() }, { retries: 2, timeoutMs: DEFAULT_TIMEOUT_MS });
|
|
99
92
|
if (!res.ok) {
|
|
100
93
|
const detail = await safeReadText(res);
|
|
101
94
|
throw new Error(`GET /videos/${id} failed: HTTP ${res.status}${detail ? ` — ${detail}` : ''}`);
|
|
102
95
|
}
|
|
103
|
-
return (
|
|
96
|
+
return readJsonOrThrow(res, `GET /videos/${id}`);
|
|
104
97
|
}
|
|
105
|
-
/** Download generated video binary
|
|
98
|
+
/** Download generated video binary. */
|
|
106
99
|
async downloadVideoContent(id, index = 0, maxBytes = 256 * 1024 * 1024) {
|
|
107
100
|
const url = `${BASE_URL}/videos/${encodeURIComponent(id)}/content?index=${index}`;
|
|
108
101
|
const res = await fetchWithRetry(url, { headers: this.authHeaders() }, { retries: 1, timeoutMs: VIDEO_TIMEOUT_MS * 2 });
|
|
@@ -144,7 +137,7 @@ export class OpenRouterAPIClient {
|
|
|
144
137
|
}
|
|
145
138
|
return { buffer: Buffer.concat(chunks), contentType: res.headers.get('content-type') };
|
|
146
139
|
}
|
|
147
|
-
/** POST /images
|
|
140
|
+
/** POST /images. */
|
|
148
141
|
async generateImage(body, headers) {
|
|
149
142
|
const res = await fetchWithRetry(`${BASE_URL}/images`, {
|
|
150
143
|
method: 'POST',
|
|
@@ -155,9 +148,9 @@ export class OpenRouterAPIClient {
|
|
|
155
148
|
const detail = await safeReadText(res);
|
|
156
149
|
throw new Error(`POST /images failed: HTTP ${res.status}${detail ? ` — ${detail}` : ''}`);
|
|
157
150
|
}
|
|
158
|
-
return (
|
|
151
|
+
return readJsonOrThrow(res, 'POST /images');
|
|
159
152
|
}
|
|
160
|
-
/** POST /audio/speech
|
|
153
|
+
/** POST /audio/speech. */
|
|
161
154
|
async generateSpeech(body, headers) {
|
|
162
155
|
const res = await fetchWithRetry(`${BASE_URL}/audio/speech`, {
|
|
163
156
|
method: 'POST',
|
|
@@ -172,7 +165,7 @@ export class OpenRouterAPIClient {
|
|
|
172
165
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
173
166
|
return { buffer: buf, contentType };
|
|
174
167
|
}
|
|
175
|
-
/** POST /audio/transcriptions
|
|
168
|
+
/** POST /audio/transcriptions. */
|
|
176
169
|
async transcribeAudio(body, headers) {
|
|
177
170
|
const res = await fetchWithRetry(`${BASE_URL}/audio/transcriptions`, {
|
|
178
171
|
method: 'POST',
|
|
@@ -183,9 +176,9 @@ export class OpenRouterAPIClient {
|
|
|
183
176
|
const detail = await safeReadText(res);
|
|
184
177
|
throw new Error(`POST /audio/transcriptions failed: HTTP ${res.status}${detail ? ` — ${detail}` : ''}`);
|
|
185
178
|
}
|
|
186
|
-
return (
|
|
179
|
+
return readTranscriptionResponse(res, body.response_format, 'POST /audio/transcriptions');
|
|
187
180
|
}
|
|
188
|
-
/** POST /rerank
|
|
181
|
+
/** POST /rerank. */
|
|
189
182
|
async rerank(params) {
|
|
190
183
|
const body = {
|
|
191
184
|
model: params.model,
|
|
@@ -203,7 +196,7 @@ export class OpenRouterAPIClient {
|
|
|
203
196
|
const detail = await safeReadText(res);
|
|
204
197
|
throw new Error(`POST /rerank failed: HTTP ${res.status}${detail ? ` — ${detail}` : ''}`);
|
|
205
198
|
}
|
|
206
|
-
return (
|
|
199
|
+
return readJsonOrThrow(res, 'POST /rerank');
|
|
207
200
|
}
|
|
208
201
|
}
|
|
209
202
|
async function safeReadText(res) {
|
|
@@ -215,5 +208,58 @@ async function safeReadText(res) {
|
|
|
215
208
|
return '';
|
|
216
209
|
}
|
|
217
210
|
}
|
|
218
|
-
|
|
219
|
-
|
|
211
|
+
function extractEmbeddedError(data) {
|
|
212
|
+
if (!data || typeof data !== 'object')
|
|
213
|
+
return undefined;
|
|
214
|
+
const record = data;
|
|
215
|
+
if (record.type === 'error') {
|
|
216
|
+
const err = record.error;
|
|
217
|
+
if (typeof err === 'string')
|
|
218
|
+
return err;
|
|
219
|
+
if (err &&
|
|
220
|
+
typeof err === 'object' &&
|
|
221
|
+
typeof err.message === 'string') {
|
|
222
|
+
return err.message;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (record.error && typeof record.error === 'object') {
|
|
226
|
+
const err = record.error;
|
|
227
|
+
if (typeof err.message === 'string')
|
|
228
|
+
return err.message;
|
|
229
|
+
}
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
async function readTranscriptionResponse(res, responseFormat, context) {
|
|
233
|
+
const format = typeof responseFormat === 'string' ? responseFormat : 'json';
|
|
234
|
+
const contentType = res.headers.get('content-type') ?? '';
|
|
235
|
+
const plainByFormat = format === 'text' || format === 'srt' || format === 'vtt';
|
|
236
|
+
const plainByContentType = contentType.startsWith('text/') && format !== 'json' && format !== 'verbose_json';
|
|
237
|
+
if (plainByFormat || plainByContentType) {
|
|
238
|
+
const text = await res.text();
|
|
239
|
+
return { text };
|
|
240
|
+
}
|
|
241
|
+
return readJsonOrThrow(res, context);
|
|
242
|
+
}
|
|
243
|
+
async function readJsonOrThrow(res, context) {
|
|
244
|
+
const raw = await res.text();
|
|
245
|
+
let data;
|
|
246
|
+
try {
|
|
247
|
+
data = JSON.parse(raw);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
const detail = raw.length > 500 ? raw.slice(0, 500) + '…' : raw;
|
|
251
|
+
throw new Error(`${context}: non-JSON response${detail ? ` — ${detail}` : ''}`);
|
|
252
|
+
}
|
|
253
|
+
const embedded = extractEmbeddedError(data);
|
|
254
|
+
if (embedded)
|
|
255
|
+
throw new Error(`${context}: ${embedded}`);
|
|
256
|
+
return data;
|
|
257
|
+
}
|
|
258
|
+
export const _internals = {
|
|
259
|
+
parseRetryAfter,
|
|
260
|
+
backoffWithJitter,
|
|
261
|
+
fetchWithRetry,
|
|
262
|
+
extractEmbeddedError,
|
|
263
|
+
readJsonOrThrow,
|
|
264
|
+
readTranscriptionResponse,
|
|
265
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
declare const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
|
3
|
+
declare const OPENROUTER_ATTRIBUTION_HEADERS: {
|
|
4
|
+
readonly 'HTTP-Referer': "https://github.com/stabgan/openrouter-mcp-multimodal";
|
|
5
|
+
readonly 'X-Title': "openrouter-mcp-multimodal";
|
|
6
|
+
};
|
|
7
|
+
/** OpenAI SDK client configured for OpenRouter chat/completions endpoints. */
|
|
8
|
+
export declare function createOpenRouterOpenAIClient(apiKey: string): OpenAI;
|
|
9
|
+
export { OPENROUTER_BASE_URL, OPENROUTER_ATTRIBUTION_HEADERS };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
|
|
3
|
+
const OPENROUTER_ATTRIBUTION_HEADERS = {
|
|
4
|
+
'HTTP-Referer': 'https://github.com/stabgan/openrouter-mcp-multimodal',
|
|
5
|
+
'X-Title': 'openrouter-mcp-multimodal',
|
|
6
|
+
};
|
|
7
|
+
/** OpenAI SDK client configured for OpenRouter chat/completions endpoints. */
|
|
8
|
+
export function createOpenRouterOpenAIClient(apiKey) {
|
|
9
|
+
return new OpenAI({
|
|
10
|
+
apiKey,
|
|
11
|
+
baseURL: OPENROUTER_BASE_URL,
|
|
12
|
+
defaultHeaders: { ...OPENROUTER_ATTRIBUTION_HEADERS },
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export { OPENROUTER_BASE_URL, OPENROUTER_ATTRIBUTION_HEADERS };
|
|
@@ -1 +1,25 @@
|
|
|
1
|
+
/** Aspect ratios accepted by generate_image and generate_image_dedicated handlers. */
|
|
2
|
+
export declare const IMAGE_ASPECT_RATIOS: readonly ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9", "1:4", "4:1", "1:8", "8:1"];
|
|
3
|
+
export declare const IMAGE_SIZES: readonly ["0.5K", "1K", "2K", "4K"];
|
|
4
|
+
export declare const IMAGE_DEDICATED_RESOLUTIONS: readonly ["512", "0.5K", "1K", "2K", "4K"];
|
|
5
|
+
export declare const IMAGE_DEDICATED_QUALITIES: readonly ["auto", "low", "medium", "high"];
|
|
6
|
+
export declare const IMAGE_OUTPUT_FORMATS: readonly ["png", "jpeg", "webp", "svg"];
|
|
7
|
+
/** generate_audio handler VALID_FORMATS */
|
|
8
|
+
export declare const GENERATE_AUDIO_FORMATS: readonly ["wav", "mp3", "flac", "opus", "pcm16"];
|
|
9
|
+
/** text_to_speech handler VALID_FORMATS */
|
|
10
|
+
export declare const TTS_RESPONSE_FORMATS: readonly ["mp3", "opus", "aac", "flac", "wav", "pcm"];
|
|
11
|
+
/** speech_to_text handler VALID_RESPONSE_FORMATS */
|
|
12
|
+
export declare const STT_RESPONSE_FORMATS: readonly ["json", "text", "srt", "verbose_json", "vtt"];
|
|
13
|
+
export declare const CHAT_MESSAGE_ROLES: readonly ["system", "user", "assistant"];
|
|
14
|
+
export declare const PROVIDER_SORT_VALUES: readonly ["price", "throughput", "latency"];
|
|
15
|
+
export declare const PROVIDER_DATA_COLLECTION: readonly ["allow", "deny"];
|
|
16
|
+
/** Tools whose handlers accept save_path (binary artifact output). */
|
|
17
|
+
export declare const TOOLS_WITH_SAVE_PATH: readonly ["generate_image", "generate_image_dedicated", "generate_audio", "text_to_speech", "generate_video", "generate_video_from_image", "get_video_status"];
|
|
18
|
+
/** Shared save_path fragment for binary-output tools. */
|
|
19
|
+
export declare const SAVE_PATH_PROPERTY: {
|
|
20
|
+
readonly type: "string";
|
|
21
|
+
readonly description: string;
|
|
22
|
+
};
|
|
1
23
|
export declare const TOOL_DEFINITIONS: Array<Record<string, unknown>>;
|
|
24
|
+
/** Tool names from definitions — must match TOOL_NAMES in tool-descriptions.ts. */
|
|
25
|
+
export declare const TOOL_DEFINITION_NAMES: unknown[];
|