@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.
Files changed (64) hide show
  1. package/README.md +108 -37
  2. package/dist/errors.d.ts +5 -20
  3. package/dist/errors.js +1 -10
  4. package/dist/index.js +7 -1
  5. package/dist/logger.js +54 -24
  6. package/dist/model-cache.d.ts +13 -0
  7. package/dist/model-cache.js +61 -5
  8. package/dist/openrouter-api.d.ts +14 -15
  9. package/dist/openrouter-api.js +68 -22
  10. package/dist/openrouter-openai-client.d.ts +9 -0
  11. package/dist/openrouter-openai-client.js +15 -0
  12. package/dist/tool-definitions.d.ts +24 -0
  13. package/dist/tool-definitions.js +283 -170
  14. package/dist/tool-descriptions.js +23 -15
  15. package/dist/tool-handlers/analyze-audio.js +4 -1
  16. package/dist/tool-handlers/analyze-image.js +10 -5
  17. package/dist/tool-handlers/analyze-video.js +9 -5
  18. package/dist/tool-handlers/async-chat.d.ts +17 -0
  19. package/dist/tool-handlers/async-chat.js +112 -31
  20. package/dist/tool-handlers/audio-utils.d.ts +19 -4
  21. package/dist/tool-handlers/audio-utils.js +170 -16
  22. package/dist/tool-handlers/cache.d.ts +3 -3
  23. package/dist/tool-handlers/cache.js +56 -4
  24. package/dist/tool-handlers/chat-completion.js +16 -7
  25. package/dist/tool-handlers/chat-request.d.ts +3 -0
  26. package/dist/tool-handlers/chat-request.js +28 -0
  27. package/dist/tool-handlers/completion-utils.d.ts +5 -11
  28. package/dist/tool-handlers/completion-utils.js +76 -47
  29. package/dist/tool-handlers/fetch-utils.d.ts +14 -0
  30. package/dist/tool-handlers/fetch-utils.js +331 -68
  31. package/dist/tool-handlers/generate-audio.d.ts +6 -51
  32. package/dist/tool-handlers/generate-audio.js +30 -76
  33. package/dist/tool-handlers/generate-image-dedicated.d.ts +2 -12
  34. package/dist/tool-handlers/generate-image-dedicated.js +79 -28
  35. package/dist/tool-handlers/generate-image.d.ts +3 -46
  36. package/dist/tool-handlers/generate-image.js +26 -43
  37. package/dist/tool-handlers/generate-video.d.ts +4 -3
  38. package/dist/tool-handlers/generate-video.js +51 -50
  39. package/dist/tool-handlers/get-model-info.js +1 -1
  40. package/dist/tool-handlers/health-check.js +39 -15
  41. package/dist/tool-handlers/image-utils.js +2 -2
  42. package/dist/tool-handlers/openrouter-errors.d.ts +2 -0
  43. package/dist/tool-handlers/openrouter-errors.js +138 -31
  44. package/dist/tool-handlers/path-safety.d.ts +6 -0
  45. package/dist/tool-handlers/path-safety.js +77 -14
  46. package/dist/tool-handlers/path-utils.d.ts +4 -0
  47. package/dist/tool-handlers/path-utils.js +20 -0
  48. package/dist/tool-handlers/provider-routing.d.ts +2 -0
  49. package/dist/tool-handlers/provider-routing.js +10 -0
  50. package/dist/tool-handlers/rerank.d.ts +1 -4
  51. package/dist/tool-handlers/rerank.js +43 -14
  52. package/dist/tool-handlers/search-models.js +3 -3
  53. package/dist/tool-handlers/speech-to-text.d.ts +1 -0
  54. package/dist/tool-handlers/speech-to-text.js +23 -56
  55. package/dist/tool-handlers/text-to-speech.d.ts +2 -12
  56. package/dist/tool-handlers/text-to-speech.js +29 -22
  57. package/dist/tool-handlers/tool-result-payload.d.ts +47 -0
  58. package/dist/tool-handlers/tool-result-payload.js +98 -0
  59. package/dist/tool-handlers/validate-model.js +1 -1
  60. package/dist/tool-handlers.d.ts +9 -0
  61. package/dist/tool-handlers.js +19 -10
  62. package/dist/version.d.ts +1 -1
  63. package/dist/version.js +1 -1
  64. package/package.json +1 -3
@@ -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. Returns the `{ id, polling_url, status }` envelope. */
13
+ /** Submit a video-generation job. */
21
14
  submitVideoJob(body: Record<string, unknown>): Promise<VideoJobEnvelope>;
22
- /** Poll a submitted video-generation job by id. */
15
+ /** Poll a submitted video-generation job. */
23
16
  pollVideoJob(id: string): Promise<VideoJobStatus>;
24
- /** Download generated video binary (trusted OpenRouter origin). */
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 — dedicated image generation. */
22
+ /** POST /images. */
30
23
  generateImage(body: Record<string, unknown>, headers?: Record<string, string>): Promise<ImageGenerationResponse>;
31
- /** POST /audio/speech — text-to-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 — speech-to-text. */
29
+ /** POST /audio/transcriptions. */
37
30
  transcribeAudio(body: Record<string, unknown>, headers?: Record<string, string>): Promise<TranscriptionResponse>;
38
- /** POST /rerank — re-order documents by relevance to a query. */
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 {};
@@ -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 = (await res.json());
73
+ const data = await readJsonOrThrow(res, 'GET /models');
81
74
  return data.data ?? [];
82
75
  }
83
- /** Submit a video-generation job. Returns the `{ id, polling_url, status }` envelope. */
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 (await res.json());
87
+ return readJsonOrThrow(res, 'POST /videos');
95
88
  }
96
- /** Poll a submitted video-generation job by id. */
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 (await res.json());
96
+ return readJsonOrThrow(res, `GET /videos/${id}`);
104
97
  }
105
- /** Download generated video binary (trusted OpenRouter origin). */
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 — dedicated image generation. */
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 (await res.json());
151
+ return readJsonOrThrow(res, 'POST /images');
159
152
  }
160
- /** POST /audio/speech — text-to-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 — speech-to-text. */
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 (await res.json());
179
+ return readTranscriptionResponse(res, body.response_format, 'POST /audio/transcriptions');
187
180
  }
188
- /** POST /rerank — re-order documents by relevance to a query. */
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 (await res.json());
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
- // Exported for tests.
219
- export const _internals = { parseRetryAfter, backoffWithJitter, fetchWithRetry };
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[];