@reaatech/media-pipeline-mcp-provider-core 0.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Media Pipeline MCP Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,476 @@
1
+ # @reaatech/media-pipeline-mcp-provider-core
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@reaatech/media-pipeline-mcp-provider-core.svg)](https://www.npmjs.com/package/@reaatech/media-pipeline-mcp-provider-core)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/reaatech/media-pipeline-mcp/blob/main/LICENSE)
5
+ [![CI](https://img.shields.io/github/actions/workflow/status/reaatech/media-pipeline-mcp/ci.yml?branch=main&label=CI)](https://github.com/reaatech/media-pipeline-mcp/actions/workflows/ci.yml)
6
+
7
+ > **Status:** Pre-1.0 — APIs may change in minor versions. Pin to a specific version in production.
8
+
9
+ Abstract base class and shared interfaces for all media provider implementations. Defines the provider contract, deterministic caching (F2), retry with exponential backoff, cost estimation, and a multi-strategy router for provider selection across health, budget, and latency constraints.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install @reaatech/media-pipeline-mcp-provider-core
15
+ # or
16
+ pnpm add @reaatech/media-pipeline-mcp-provider-core
17
+ ```
18
+
19
+ ## Feature Overview
20
+
21
+ - **Abstract `MediaProvider` class** — standardizes the interface for all media backends with required `name`, `supportedOperations`, `execute`, `estimateCost`, and optional `healthCheck`
22
+ - **Deterministic caching (F2)** — SHA-256 cache keys from provider::model::version::scopeTag::deterministic inputs; supports `use`, `refresh`, and `skip` modes with 30-day default TTL
23
+ - **Execute-with-retry** — exponential backoff with configurable `maxRetries`, `baseDelay`, `maxDelay`, and automatic non-retryable error detection
24
+ - **Cost estimation (F4/F5)** — `estimateCost` contract returning `{ costUsd, currency, breakdown, estimatedDurationMs }` per operation
25
+ - **Multi-strategy router (F8)** — `first-success` (sequential failover), `cheapest-acceptable` (cost-ordered with health/budget/queue gate), and `fastest` (race-to-first-complete with <5s duration cap)
26
+ - **Storage integration** — `setStorage` / `storeArtifact` helpers for persisting provider outputs
27
+ - **Webhook support (F7)** — `supportsWebhooks`, `webhookSignatureKey`, and `parseWebhookPayload` contracts for async provider callbacks
28
+ - **3D mesh generation types (F21)** — `MeshGenInput`, `MeshOutput`, `TextureConfig`, `MeshFormat` for 3D asset generation workflows
29
+ - **Canonical JSON normalization** — sorted-key, no-whitespace, no-trailing-zero serialization for deterministic cache keys
30
+
31
+ ## Quick Start
32
+
33
+ ```typescript
34
+ import { MediaProvider, defineProvider, type ProviderInput, type ProviderOutput } from "@reaatech/media-pipeline-mcp-provider-core";
35
+
36
+ class MyCustomProvider extends MediaProvider {
37
+ readonly name = "my-custom-provider";
38
+ readonly supportedOperations = ["image.generate", "image.upscale"];
39
+
40
+ constructor(private apiKey: string) {
41
+ super();
42
+ }
43
+
44
+ async healthCheck() {
45
+ try {
46
+ const response = await fetch("https://api.example.com/health");
47
+ return { healthy: response.ok, latency: 120 };
48
+ } catch {
49
+ return { healthy: false, error: "Connection refused" };
50
+ }
51
+ }
52
+
53
+ async estimateCost(input: ProviderInput) {
54
+ return { costUsd: 0.007, currency: "USD", estimatedDurationMs: 2000 };
55
+ }
56
+
57
+ async execute(input: ProviderInput): Promise<ProviderOutput> {
58
+ const response = await fetch("https://api.example.com/generate", {
59
+ method: "POST",
60
+ headers: {
61
+ Authorization: `Bearer ${this.apiKey}`,
62
+ "Content-Type": "application/json",
63
+ },
64
+ body: JSON.stringify({ prompt: input.params.prompt, width: 1024, height: 1024 }),
65
+ });
66
+
67
+ const buffer = Buffer.from(await response.arrayBuffer());
68
+
69
+ return {
70
+ data: buffer,
71
+ mimeType: "image/png",
72
+ metadata: { width: 1024, height: 1024, model: "v2" },
73
+ costUsd: 0.007,
74
+ durationMs: 1500,
75
+ };
76
+ }
77
+ }
78
+ ```
79
+
80
+ ## API Reference
81
+
82
+ ### `MediaProvider` (abstract class)
83
+
84
+ Base class that all provider implementations must extend. Provides caching, retry, and storage utilities.
85
+
86
+ ```typescript
87
+ abstract class MediaProvider {
88
+ abstract readonly name: string;
89
+ abstract readonly supportedOperations: string[];
90
+
91
+ abstract estimateCost(input: ProviderInput): Promise<CostEstimate>;
92
+ abstract execute(input: ProviderInput): Promise<ProviderOutput>;
93
+
94
+ // Optional lifecycle
95
+ healthCheck?(): Promise<ProviderHealth>;
96
+
97
+ // Built-in helpers
98
+ setStorage(storage: ArtifactStore): void;
99
+ executeWithRetry(input: ProviderInput): Promise<ProviderOutput>;
100
+ executeWithCache(input: ProviderInput, cacheConfig?: CacheConfig): Promise<ProviderOutput>;
101
+ generateArtifactId(): string;
102
+ storeArtifact(data: Buffer | ReadableStream, type: ArtifactType, mimeType: string, metadata: Record<string, unknown>, sourceStep?: string): Promise<string>;
103
+
104
+ // Caching internals
105
+ protected computeCacheKey(input: ProviderInput, cacheConfig?: CacheConfig): string;
106
+ protected defaultCacheConfigForOperation(input: ProviderInput): CacheConfig;
107
+ protected canonicalJson(obj: unknown): string;
108
+ protected isNonRetryableError(error: unknown): boolean;
109
+
110
+ // Caching configuration
111
+ static cacheConfig: ProviderCacheConfig;
112
+ }
113
+ ```
114
+
115
+ ### `defineProvider`
116
+
117
+ Identity helper that passes through a provider class for registration.
118
+
119
+ ```typescript
120
+ function defineProvider<T extends MediaProvider>(
121
+ providerClass: new (...args: unknown[]) => T,
122
+ ): new (...args: unknown[]) => T;
123
+ ```
124
+
125
+ ### `ProviderInput`
126
+
127
+ ```typescript
128
+ interface ProviderInput {
129
+ operation: string;
130
+ params: Record<string, unknown>;
131
+ config: Record<string, unknown>;
132
+ }
133
+ ```
134
+
135
+ ### `ProviderOutput`
136
+
137
+ ```typescript
138
+ interface ProviderOutput {
139
+ data: Buffer | ReadableStream;
140
+ mimeType: string;
141
+ metadata: Record<string, unknown>;
142
+ costUsd?: number;
143
+ durationMs?: number;
144
+ }
145
+ ```
146
+
147
+ ### `ProviderHealth`
148
+
149
+ ```typescript
150
+ interface ProviderHealth {
151
+ healthy: boolean;
152
+ latency?: number;
153
+ error?: string;
154
+ }
155
+ ```
156
+
157
+ ### Cost Estimation Types
158
+
159
+ ```typescript
160
+ interface CostEstimate {
161
+ costUsd: number;
162
+ currency: string;
163
+ breakdown?: Array<{ component: string; costUsd: number }>;
164
+ estimatedDurationMs?: number;
165
+ }
166
+ ```
167
+
168
+ ### Caching (F2)
169
+
170
+ #### `CacheConfig`
171
+
172
+ | Property | Type | Default | Description |
173
+ |----------|------|---------|-------------|
174
+ | `mode` | `"use" \| "refresh" \| "skip"` | — | Cache mode: read-first, always-write, or bypass |
175
+ | `ttlSeconds` | `number` | `2592000` | Cache entry lifespan (30 days) |
176
+ | `scope` | `"global" \| "tenant"` | `"global"` | Key scoping: global or per-tenant |
177
+
178
+ #### `ProviderCacheConfig`
179
+
180
+ Per-provider static caching configuration controlling which params participate in cache keys.
181
+
182
+ ```typescript
183
+ interface ProviderCacheConfig {
184
+ deterministicParams: string[]; // Only these params drive the cache key
185
+ nonDeterministicParams: string[]; // These params are excluded from the key
186
+ normalize: (inputs: Record<string, unknown>) => Record<string, unknown>;
187
+ }
188
+ ```
189
+
190
+ #### Cache Key Formula
191
+
192
+ ```
193
+ sha256(provider :: modelId :: modelVersion :: scopeTag :: operation :: canonicalInputs)
194
+ ```
195
+
196
+ - `modelId`: from `input.params.model` or `input.config.model`
197
+ - `modelVersion`: from `input.params.model_version` or `input.params.modelVersion`
198
+ - `scopeTag`: `"global"` or `"tenant:<tenantId>"`
199
+ - `canonicalInputs`: sorted-key JSON of deterministic params only
200
+
201
+ ### Execute with Cache
202
+
203
+ ```typescript
204
+ class MyProvider extends MediaProvider {
205
+ async safeExecute(operation: string, input: ProviderInput) {
206
+ return this.executeWithCache(input, {
207
+ mode: "use",
208
+ ttlSeconds: 3600,
209
+ });
210
+ }
211
+ }
212
+ ```
213
+
214
+ Cache modes:
215
+ - **`skip`** — bypass cache entirely, no read or write
216
+ - **`use`** — read from cache first; on miss, execute and store; cache hit rebates `costUsd` to 0
217
+ - **`refresh`** — always execute and overwrite the cache entry
218
+
219
+ ### Execute with Retry
220
+
221
+ ```typescript
222
+ class MyProvider extends MediaProvider {
223
+ async robustExecute(input: ProviderInput) {
224
+ return this.executeWithRetry(input);
225
+ }
226
+ }
227
+ ```
228
+
229
+ Built-in retry behavior:
230
+ - Up to 3 retries (configurable via `retryConfig`)
231
+ - Exponential backoff: `baseDelay × 2^(attempt-1)` capped at `maxDelay`
232
+ - Non-retryable errors detected by message patterns: `"authentication"`, `"unauthorized"`, `"validation"`, `"invalid api key"`
233
+
234
+ ### Router (F8)
235
+
236
+ Multi-strategy provider routing for selecting among multiple provider/model candidates.
237
+
238
+ ```typescript
239
+ class Router {
240
+ constructor(ctx: RouterContext);
241
+ route(config: RouteConfig, inputs: ProviderInput): Promise<{ decision: RouteDecision; output: ProviderOutput }>;
242
+ }
243
+ ```
244
+
245
+ #### `RouterContext`
246
+
247
+ Injection interface the router uses to interact with the host.
248
+
249
+ ```typescript
250
+ interface RouterContext {
251
+ estimateCost(candidate: RouteCandidate, inputs: ProviderInput): Promise<CostEstimate>;
252
+ health(candidate: RouteCandidate): Promise<{ healthy: boolean; latencyMs?: number; queueDepth?: number }>;
253
+ execute(candidate: RouteCandidate, inputs: ProviderInput, signal: AbortSignal): Promise<ProviderOutput>;
254
+ expectedDurationMs?(candidate: RouteCandidate, inputs: ProviderInput): number | undefined;
255
+ queueMs?(candidate: RouteCandidate): Promise<number | undefined>;
256
+ }
257
+ ```
258
+
259
+ #### `RouteConfig`
260
+
261
+ | Property | Type | Default | Description |
262
+ |----------|------|---------|-------------|
263
+ | `strategy` | `"first-success" \| "cheapest-acceptable" \| "fastest"` | — | Routing strategy |
264
+ | `candidates` | `RouteCandidate[]` | — | Provider/model candidates to route across |
265
+ | `timeoutMs` | `number` | — | Global timeout for the route attempt |
266
+ | `healthTtlMs` | `number` | `30000` | Health probe cache TTL |
267
+
268
+ #### `RouteCandidate`
269
+
270
+ | Property | Type | Description |
271
+ |----------|------|-------------|
272
+ | `provider` | `string` | Provider name |
273
+ | `model` | `string` | Model identifier |
274
+ | `maxQueueMs` | `number` | Maximum acceptable queue depth in ms |
275
+ | `maxUsd` | `number` | Maximum acceptable cost per call |
276
+ | `inputOverrides` | `Record<string, unknown>` | Per-candidate parameter overrides |
277
+ | `weight` | `number` | Tiebreaker weight for equal-cost candidates (default: 1) |
278
+
279
+ #### Routing Strategies
280
+
281
+ | Strategy | Behavior |
282
+ |----------|----------|
283
+ | `first-success` | Try candidates in order; return first successful result |
284
+ | `cheapest-acceptable` | Parallel health/estimate/queue probes; pick the cheapest healthy candidate under budget |
285
+ | `fastest` | Race all candidates simultaneously; return first to complete (all must be <5s expected duration) |
286
+
287
+ #### Router Errors
288
+
289
+ ```typescript
290
+ class RouterNoCandidatesError extends Error {
291
+ readonly code = "ROUTER_NO_CANDIDATES";
292
+ }
293
+
294
+ class RouterAllCandidatesFailedError extends Error {
295
+ readonly code = "ROUTER_ALL_CANDIDATES_FAILED";
296
+ readonly rejections: RouteRejection[];
297
+ }
298
+
299
+ class RouterFastestIneligibleError extends Error {
300
+ readonly code = "ROUTER_FASTEST_INELIGIBLE";
301
+ readonly ineligibleCandidates: RouteCandidate[];
302
+ }
303
+ ```
304
+
305
+ #### Rejection Reasons
306
+
307
+ | Reason | Trigger |
308
+ |--------|---------|
309
+ | `over-budget` | Candidate cost estimate exceeds `maxUsd` |
310
+ | `unhealthy` | Health check returned unhealthy |
311
+ | `queue-full` | Queue depth exceeds `maxQueueMs` |
312
+ | `error` | Execution error |
313
+ | `cancelled` | AbortSignal triggered (timeout or cancellation) |
314
+ | `fastest-ineligible` | Candidate exceeds 5s expected duration cap |
315
+
316
+ ### Webhook Types (F7)
317
+
318
+ ```typescript
319
+ interface WebhookPayload {
320
+ jobId: string;
321
+ status: "completed" | "failed" | "progress";
322
+ output?: unknown;
323
+ pct?: number;
324
+ error?: { code: string; message: string };
325
+ }
326
+ ```
327
+
328
+ ### Mesh Generation Types (F21)
329
+
330
+ ```typescript
331
+ type MeshFormat = "glb" | "fbx" | "obj" | "usdz" | "ply";
332
+
333
+ interface MeshGenInput {
334
+ prompt?: string;
335
+ sourceArtifactId?: string;
336
+ format: MeshFormat;
337
+ polyBudget?: number;
338
+ topology?: "quads" | "tris";
339
+ texture?: TextureConfig;
340
+ animated?: boolean;
341
+ }
342
+
343
+ interface TextureConfig {
344
+ enabled: boolean;
345
+ pbr?: boolean;
346
+ resolution?: 512 | 1024 | 2048 | 4096;
347
+ unwrap?: "auto" | "preserve-source";
348
+ }
349
+
350
+ interface MeshOutput {
351
+ artifactId: string;
352
+ format: MeshFormat;
353
+ polyCount: number;
354
+ hasTextures: boolean;
355
+ hasAnimation: boolean;
356
+ bboxMeters?: { x: number; y: number; z: number };
357
+ }
358
+ ```
359
+
360
+ ### `MediaProviderLike`
361
+
362
+ Lightweight interface for provider shape checking and tooling.
363
+
364
+ ```typescript
365
+ interface MediaProviderLike {
366
+ readonly name: string;
367
+ readonly supportedOperations: string[];
368
+ estimateCost(input: ProviderInput): Promise<CostEstimate>;
369
+ execute(input: ProviderInput): Promise<ProviderOutput>;
370
+ healthCheck?(): Promise<ProviderHealth>;
371
+ supportsStreaming?: ReadonlySet<string>;
372
+ supportsWebhooks?: boolean;
373
+ webhookSignatureKey?(): Promise<string>;
374
+ parseWebhookPayload?(headers: Record<string, string>, body: string): Promise<WebhookPayload>;
375
+ }
376
+ ```
377
+
378
+ ## Usage Patterns
379
+
380
+ ### Implementing a Full Provider
381
+
382
+ Every provider must extend `MediaProvider` and implement:
383
+
384
+ 1. `name` — unique provider identifier
385
+ 2. `supportedOperations` — list of operations (e.g. `["image.generate", "image.upscale"]`)
386
+ 3. `estimateCost(input)` → `CostEstimate` — per-operation cost from public pricing
387
+ 4. `execute(input)` → `ProviderOutput` — main execution path with data, mimeType, metadata
388
+ 5. `healthCheck()` → `ProviderHealth` — optional, used by router for health gating
389
+
390
+ ### Customizing Cache Behavior
391
+
392
+ ```typescript
393
+ class OpenAIImageProvider extends MediaProvider {
394
+ readonly name = "openai-image";
395
+ readonly supportedOperations = ["image.generate"];
396
+
397
+ static cacheConfig: ProviderCacheConfig = {
398
+ deterministicParams: ["prompt", "size", "n"],
399
+ nonDeterministicParams: ["seed"],
400
+ normalize: (inputs) => {
401
+ const out: Record<string, unknown> = {};
402
+ for (const [k, v] of Object.entries(inputs)) {
403
+ out[k] = typeof v === "string" ? v.trim().replace(/\s+/g, " ") : v;
404
+ }
405
+ return out;
406
+ },
407
+ };
408
+
409
+ async estimateCost(input: ProviderInput) {
410
+ return { costUsd: 0.04, currency: "USD" };
411
+ }
412
+
413
+ async execute(input: ProviderInput): Promise<ProviderOutput> {
414
+ // ... implementation
415
+ }
416
+ }
417
+ ```
418
+
419
+ ### Router with Cheapest-Acceptable Strategy
420
+
421
+ ```typescript
422
+ const router = new Router({
423
+ estimateCost: async (candidate, inputs) => {
424
+ return { costUsd: candidate.provider === "stability" ? 0.007 : 0.04, currency: "USD" };
425
+ },
426
+ health: async (candidate) => ({
427
+ healthy: true,
428
+ latencyMs: candidate.model === "sd3" ? 1200 : 2000,
429
+ }),
430
+ execute: async (candidate, inputs, signal) => {
431
+ const provider = getProvider(candidate.provider);
432
+ return provider.execute(inputs);
433
+ },
434
+ });
435
+
436
+ const { decision, output } = await router.route(
437
+ {
438
+ strategy: "cheapest-acceptable",
439
+ candidates: [
440
+ { provider: "stability", model: "sd3", maxUsd: 0.05 },
441
+ { provider: "openai", model: "dall-e-3", maxUsd: 0.08 },
442
+ ],
443
+ timeoutMs: 30000,
444
+ },
445
+ { operation: "image.generate", params: { prompt: "sunset" }, config: {} },
446
+ );
447
+
448
+ console.log(decision.selected.provider); // "stability"
449
+ console.log(decision.reason); // "cheapest-acceptable: lowest cost among healthy candidates"
450
+ ```
451
+
452
+ ### Router with Fastest Strategy
453
+
454
+ ```typescript
455
+ const { decision, output } = await router.route(
456
+ {
457
+ strategy: "fastest",
458
+ candidates: [
459
+ { provider: "fal", model: "flux-pro-1.1" },
460
+ { provider: "replicate", model: "sdxl" },
461
+ ],
462
+ },
463
+ { operation: "image.generate", params: { prompt: "cat" }, config: {} },
464
+ );
465
+ // Both candidates are raced; the first to complete wins
466
+ ```
467
+
468
+ ## Related Packages
469
+
470
+ - [`@reaatech/media-pipeline-mcp-core`](https://www.npmjs.com/package/@reaatech/media-pipeline-mcp-core) — Core pipeline types consumed by providers
471
+ - [`@reaatech/media-pipeline-mcp-storage`](https://www.npmjs.com/package/@reaatech/media-pipeline-mcp-storage) — Artifact persistence used by `storeArtifact`
472
+ - [`@reaatech/media-pipeline-mcp-pipeline`](https://www.npmjs.com/package/@reaatech/media-pipeline-mcp-pipeline) — Pipeline templates and operations
473
+
474
+ ## License
475
+
476
+ [MIT](https://github.com/reaatech/media-pipeline-mcp/blob/main/LICENSE)