@miragari/providers 0.1.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 (83) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +567 -0
  3. package/dist/builtin.d.ts +9 -0
  4. package/dist/builtin.d.ts.map +1 -0
  5. package/dist/builtin.js +13 -0
  6. package/dist/builtin.js.map +1 -0
  7. package/dist/google/definition.d.ts +3 -0
  8. package/dist/google/definition.d.ts.map +1 -0
  9. package/dist/google/definition.js +69 -0
  10. package/dist/google/definition.js.map +1 -0
  11. package/dist/google/index.d.ts +2 -0
  12. package/dist/google/index.d.ts.map +1 -0
  13. package/dist/google/index.js +2 -0
  14. package/dist/google/index.js.map +1 -0
  15. package/dist/google/provider.d.ts +2 -0
  16. package/dist/google/provider.d.ts.map +1 -0
  17. package/dist/google/provider.js +133 -0
  18. package/dist/google/provider.js.map +1 -0
  19. package/dist/happyhorse/definition.d.ts +3 -0
  20. package/dist/happyhorse/definition.d.ts.map +1 -0
  21. package/dist/happyhorse/definition.js +57 -0
  22. package/dist/happyhorse/definition.js.map +1 -0
  23. package/dist/happyhorse/index.d.ts +2 -0
  24. package/dist/happyhorse/index.d.ts.map +1 -0
  25. package/dist/happyhorse/index.js +2 -0
  26. package/dist/happyhorse/index.js.map +1 -0
  27. package/dist/happyhorse/provider.d.ts +2 -0
  28. package/dist/happyhorse/provider.d.ts.map +1 -0
  29. package/dist/happyhorse/provider.js +253 -0
  30. package/dist/happyhorse/provider.js.map +1 -0
  31. package/dist/http.d.ts +66 -0
  32. package/dist/http.d.ts.map +1 -0
  33. package/dist/http.js +236 -0
  34. package/dist/http.js.map +1 -0
  35. package/dist/index.d.ts +14 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +10 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/media-router.d.ts +35 -0
  40. package/dist/media-router.d.ts.map +1 -0
  41. package/dist/media-router.js +202 -0
  42. package/dist/media-router.js.map +1 -0
  43. package/dist/openai/definition.d.ts +4 -0
  44. package/dist/openai/definition.d.ts.map +1 -0
  45. package/dist/openai/definition.js +92 -0
  46. package/dist/openai/definition.js.map +1 -0
  47. package/dist/openai/index.d.ts +2 -0
  48. package/dist/openai/index.d.ts.map +1 -0
  49. package/dist/openai/index.js +2 -0
  50. package/dist/openai/index.js.map +1 -0
  51. package/dist/openai/provider.d.ts +2 -0
  52. package/dist/openai/provider.d.ts.map +1 -0
  53. package/dist/openai/provider.js +188 -0
  54. package/dist/openai/provider.js.map +1 -0
  55. package/dist/qwen/definition.d.ts +4 -0
  56. package/dist/qwen/definition.d.ts.map +1 -0
  57. package/dist/qwen/definition.js +210 -0
  58. package/dist/qwen/definition.js.map +1 -0
  59. package/dist/qwen/index.d.ts +2 -0
  60. package/dist/qwen/index.d.ts.map +1 -0
  61. package/dist/qwen/index.js +2 -0
  62. package/dist/qwen/index.js.map +1 -0
  63. package/dist/qwen/provider.d.ts +2 -0
  64. package/dist/qwen/provider.d.ts.map +1 -0
  65. package/dist/qwen/provider.js +237 -0
  66. package/dist/qwen/provider.js.map +1 -0
  67. package/dist/toolkit.d.ts +137 -0
  68. package/dist/toolkit.d.ts.map +1 -0
  69. package/dist/toolkit.js +490 -0
  70. package/dist/toolkit.js.map +1 -0
  71. package/dist/volcengine/definition.d.ts +4 -0
  72. package/dist/volcengine/definition.d.ts.map +1 -0
  73. package/dist/volcengine/definition.js +130 -0
  74. package/dist/volcengine/definition.js.map +1 -0
  75. package/dist/volcengine/index.d.ts +2 -0
  76. package/dist/volcengine/index.d.ts.map +1 -0
  77. package/dist/volcengine/index.js +2 -0
  78. package/dist/volcengine/index.js.map +1 -0
  79. package/dist/volcengine/provider.d.ts +2 -0
  80. package/dist/volcengine/provider.d.ts.map +1 -0
  81. package/dist/volcengine/provider.js +232 -0
  82. package/dist/volcengine/provider.js.map +1 -0
  83. package/package.json +51 -0
package/LICENSE ADDED
@@ -0,0 +1 @@
1
+ SEE LICENSE IN ../../LICENSE
package/README.md ADDED
@@ -0,0 +1,567 @@
1
+ # Adding a Provider
2
+
3
+ MediaRouter providers are contributed as provider plugins. A provider plugin is
4
+ the facade between MediaRouter's shared request protocol and one provider's API.
5
+ Core supplies stable types, basic validation, dimension helpers, job lifecycle,
6
+ and normalized outputs. The provider facade decides how to consume the request
7
+ and which provider endpoint to call.
8
+
9
+ Use `defineHttpProvider()` when the provider is a JSON HTTP API with a create
10
+ endpoint and optional polling endpoint. Use request-level `parseResponse`,
11
+ `parseError`, `contentType`, and `serializeBody` hooks when the provider
12
+ returns SSE, text, non-standard error bodies, form requests, multipart, or
13
+ binary payloads. Implement a custom `ProviderPlugin` driver only when the
14
+ provider needs custom signing or a non-standard task lifecycle.
15
+
16
+ By default, plain objects are sent as JSON. `URLSearchParams`, `FormData`,
17
+ `Blob`, `ArrayBuffer`, typed arrays, and strings are sent as-is with a matching
18
+ content type when one is safe to infer.
19
+
20
+ ## Architecture Rules
21
+
22
+ - `context.request.action` is an opaque provider-facing intent string. The SDK
23
+ passes it through and never validates, enumerates, or routes by it.
24
+ - `context.request.input` and `context.request.options` contain shared
25
+ parameters for the request media type. Keep provider-specific switches in
26
+ `providerOptions`.
27
+ - `context.resolved.dimensions` is a helper result. Providers choose which
28
+ fields to use; core does not force a provider request shape.
29
+ - `capabilities` are metadata and coarse validation constraints. They support
30
+ docs, UI, tests, dimensions, count splitting, and simple limits; they do not
31
+ choose endpoints.
32
+ - Unsupported actions or unsupported standard inputs must fail explicitly. Do
33
+ not silently drop `input.images`, `input.mask`, audio, video, or model inputs.
34
+ - Provider outputs should be built with toolkit helpers such as `completed`,
35
+ `pendingProviderJob`, and `polledJob`. Those helpers are provider-facade
36
+ contracts, not HTTP-only utilities.
37
+
38
+ ## Implementation Flow
39
+
40
+ 1. Add `definition.ts` with model `type`, `async`, `capabilities.actions`,
41
+ dimensions, count behavior, and media-input limits.
42
+ 2. Add `provider.ts` with a provider facade that maps MediaRouter requests to
43
+ provider HTTP requests.
44
+ 3. In the facade, branch by `action` and/or input shape only inside provider
45
+ code.
46
+ 4. Use provider toolkit helpers for common input, dimension, asset, job, and
47
+ error handling.
48
+ 5. Add harness tests proving request mapping, unsupported input failures,
49
+ output normalization, and async lifecycle behavior when applicable.
50
+
51
+ ## Minimal HTTP Provider
52
+
53
+ ```ts
54
+ import {
55
+ assetsFromImageData,
56
+ completed,
57
+ describeMediaInput,
58
+ defineHttpProvider,
59
+ getImageInputs,
60
+ requirePrompt,
61
+ unsupportedInput,
62
+ } from "@miragari/providers"
63
+
64
+ export const exampleProvider = defineHttpProvider({
65
+ id: "example",
66
+ displayName: "Example",
67
+ baseURL: "https://api.example.com/v1",
68
+ auth: { type: "bearer" },
69
+ models: {
70
+ "example-image": {
71
+ id: "example-image",
72
+ type: "image",
73
+ async: false,
74
+ capabilities: {
75
+ actions: {
76
+ generate: { consumes: ["input.prompt", "options.width", "options.height"] },
77
+ reference: { consumes: ["input.prompt", "input.images"] },
78
+ },
79
+ count: { supported: true, max: 4, strategy: "native" },
80
+ },
81
+ },
82
+ },
83
+ create: {
84
+ request: {
85
+ method: "POST",
86
+ path: "/images",
87
+ body: (context) => {
88
+ const images = getImageInputs(context.request)
89
+ if (context.request.action === "reference" && !images.length) {
90
+ unsupportedInput(context, "input.images", "reference action requires images")
91
+ }
92
+
93
+ return {
94
+ model: context.request.model,
95
+ prompt: requirePrompt(context),
96
+ n: context.request.options?.count,
97
+ size: context.resolved.dimensions?.size,
98
+ image: images[0] ? mapExampleImage(context, images[0]) : undefined,
99
+ ...context.request.providerOptions,
100
+ }
101
+ },
102
+ },
103
+ output: (response, context) =>
104
+ completed({
105
+ context,
106
+ assets: response.data?.flatMap((item) => item.url ? [item.url] : []) ?? [],
107
+ raw: response,
108
+ }),
109
+ },
110
+ })
111
+
112
+ function mapExampleImage(context, input) {
113
+ const media = describeMediaInput(input)
114
+ if (media.kind === "url") return media.url
115
+ if (media.kind === "base64") {
116
+ return { data: media.data, mime_type: media.mimeType }
117
+ }
118
+ unsupportedInput(context, "input.images", `unsupported media kind: ${media.kind}`)
119
+ }
120
+ ```
121
+
122
+ ## Provider Facade Contract
123
+
124
+ The provider driver is the facade. Core does not route by action, media inputs,
125
+ or model metadata. A provider decides how to consume the shared request
126
+ protocol:
127
+
128
+ ```ts
129
+ function imageBody(context: ProviderCreateContext) {
130
+ const images = getImageInputs(context.request)
131
+
132
+ switch (context.request.action) {
133
+ case "edit":
134
+ if (!context.request.input.mask) {
135
+ unsupportedInput(context, "input.mask", "edit action requires a mask")
136
+ }
137
+ return editImageBody(context)
138
+ case "reference":
139
+ if (!images.length) {
140
+ unsupportedInput(context, "input.images", "reference action requires images")
141
+ }
142
+ return referenceImageBody(context)
143
+ case undefined:
144
+ case "generate":
145
+ return generateImageBody(context)
146
+ default:
147
+ unsupportedAction(context)
148
+ }
149
+ }
150
+ ```
151
+
152
+ Providers may also branch by input shape instead of `action` when that is the
153
+ provider's natural facade:
154
+
155
+ ```ts
156
+ if (context.request.input.mask) return editImageBody(context)
157
+ if (getImageInputs(context.request).length) return referenceImageBody(context)
158
+ return generateImageBody(context)
159
+ ```
160
+
161
+ Both approaches are valid. The important rule is that the decision lives in the
162
+ provider facade, not in core metadata.
163
+
164
+ ## Standard Inputs And Provider Options
165
+
166
+ Each media type has its own shared request shape:
167
+
168
+ - Image: `input.prompt`, `input.negativePrompt`, `input.images`, `input.mask`,
169
+ `options.width`, `options.height`, `options.count`, `options.seed`,
170
+ `options.quality`, `options.outputFormat`.
171
+ - Video: `input.prompt`, `input.image`, `input.firstFrame`, `input.lastFrame`,
172
+ `input.images`, `input.video`, `input.videos`, `input.audio`,
173
+ `input.audios`, `options.width`, `options.height`, `options.duration`,
174
+ `options.fps`, `options.seed`, `options.mode`, `options.quality`,
175
+ `options.audioEnabled`.
176
+ - Audio: `input.prompt`, `input.text`, `input.audio`, `input.audios`,
177
+ `options.duration`, `options.seed`, `options.voice`, `options.format`,
178
+ `options.sampleRate`.
179
+ - Model3D: `input.prompt`, `input.images`, `input.model`, `options.format`,
180
+ `options.quality`, `options.texture`, `options.seed`.
181
+
182
+ Use `providerOptions` for everything that is provider-specific, unstable, or
183
+ not shared across providers. Examples: `watermark`, `enhancePrompt`,
184
+ `cameraFixed`, provider-native response formats, safety settings, custom
185
+ callback URLs, and provider beta flags.
186
+
187
+ Do not read SDK execution controls from the request body. `RunOptions` controls
188
+ router behavior such as `dimensionMode`, wait timeouts, and image batch
189
+ splitting. Provider request mapping should read `context.request` and
190
+ `context.resolved`.
191
+
192
+ Do not add a shared option just because one provider supports it. Add shared
193
+ fields only when they are meaningful across providers and media types.
194
+
195
+ ## Dimensions
196
+
197
+ The client resolves dimensions once and passes them to providers as
198
+ `context.resolved.dimensions`.
199
+
200
+ ```ts
201
+ const dimensions = context.resolved.dimensions
202
+
203
+ return {
204
+ size: dimensions?.providerSize ?? dimensions?.size,
205
+ width: dimensions?.fmtWidth ?? dimensions?.width,
206
+ height: dimensions?.fmtHeight ?? dimensions?.height,
207
+ ratio: dimensions?.aspectRatio,
208
+ resolution: dimensions?.resolutionTier,
209
+ }
210
+ ```
211
+
212
+ Dimension fields:
213
+
214
+ - `width` / `height`: normalized width and height from the request, or mapped
215
+ provider size when `supportedSizes` or video resolution tiers are configured.
216
+ - `fmtWidth` / `fmtHeight`: fixed 16-aligned image dimensions. Providers that
217
+ require dimensions divisible by 16 can opt into these fields.
218
+ - `size`: string form such as `1024x1024`.
219
+ - `providerSize`: provider-facing size when the dimension config maps to a
220
+ native supported size or resolution tier.
221
+ - `aspectRatio`: nearest supported ratio such as `1:1`, `16:9`, or `9:16`.
222
+ - `resolutionTier`: image tiers use equivalent square side
223
+ `sqrt(width * height) / 1024` and nearest `0.5K`, `1K`, `2K`, `3K`, `4K`;
224
+ video tiers use `480p`, `720p`, `1080p`.
225
+
226
+ Provider rules:
227
+
228
+ - If the provider accepts exact `width`/`height`, use `width` and `height`.
229
+ - If the provider requires 16-aligned image dimensions, use
230
+ `fmtWidth`/`fmtHeight`.
231
+ - If the provider accepts a size string, use `size`.
232
+ - If the provider accepts native tiers such as `2K` or `720p`, prefer
233
+ `providerSize` or `resolutionTier`.
234
+ - If the provider has special dimension rules, keep that logic in the provider
235
+ facade and treat these fields as helpers.
236
+
237
+ ## Capabilities Metadata
238
+
239
+ Provider plugins and model definitions expose metadata plus coarse constraints:
240
+
241
+ - `type` states the model's primary output media type.
242
+ - `async` states whether the provider normally returns a task lifecycle.
243
+ - `defaultModels` lets the generic `MediaRouter` infer provider/model defaults
244
+ so users can call typed facades with only their prompt or media input.
245
+ - `capabilities.actions` documents provider-defined action strings and which
246
+ standard fields each facade branch consumes.
247
+ - `capabilities.dimensions.aspectRatios` controls ratio selection. With
248
+ `RunOptions.dimensionMode: "strict"`, unmatched ratios are rejected;
249
+ otherwise the nearest supported ratio is selected.
250
+ - `capabilities.dimensions.image.supportedSizes` controls provider size
251
+ mapping. In nearest mode, SDK dimensions are mapped to the nearest supported
252
+ provider size; in strict mode, unsupported sizes are rejected.
253
+ - `capabilities.dimensions.image.resolutionTiers` and
254
+ `capabilities.dimensions.video.resolutions` bound provider tier mapping.
255
+ - `maxWidth`, `maxHeight`, `maxPixels`, `minAspectRatio`, and
256
+ `maxAspectRatio` are enforced before provider HTTP calls. Use
257
+ `strategy: "clamp"` only when preserving aspect ratio and scaling down to
258
+ provider limits is acceptable.
259
+ - `capabilities.count` states native batch limits. Use `strategy: "split"`
260
+ only when independent single-output requests are valid and safe for that
261
+ model.
262
+ - `maxImages`, `maxVideos`, `maxAudios`, `durations`, `fps`, and
263
+ `supportsSeed` should be set whenever the provider documents those limits.
264
+
265
+ Capabilities do not choose provider endpoints. They support docs, UI display,
266
+ basic validation, and provider conformance tests.
267
+
268
+ ## Provider Toolkit
269
+
270
+ Built-in providers use the same public helpers available to external provider
271
+ PRs. Prefer these helpers before adding provider-local plumbing:
272
+
273
+ - `requestIntent()` for a provider-friendly view of normalized prompt/text,
274
+ action, shared options, provider options, and role-collected media.
275
+ - `isImageRequest()` and `isVideoRequest()` for branching on request type.
276
+ - `collectMediaInputs()` for role-preserving input collection across prompt
277
+ images, reference images, masks, first/last frames, video, and audio.
278
+ - `getImageInputs()`, `firstImageInput()`, `getVideoInputs()`, and
279
+ `getAudioInputs()` for collecting normalized media inputs.
280
+ - `getModel3DInputs()` for collecting standard model input media.
281
+ - `describeMediaInput()` for inspecting URL, base64, bytes, and file
282
+ references before mapping them into provider-specific payloads.
283
+ - `mediaInputToInlineBase64()` for providers that accept inline base64 media.
284
+ - `providerAsset()` and `providerAssets()` for filling the current media type
285
+ onto provider asset shorthand.
286
+ - `assetsFromImageData()` and `assetFromUrl()` for provider response shapes
287
+ that already need custom extraction.
288
+ - `appendPromptFlags()` for providers that encode generation controls as
289
+ prompt flags.
290
+ - `requirePrompt()` for provider branches where prompt text is mandatory.
291
+ - `getProviderOption()` for reading provider-private options without adding
292
+ them to the shared request protocol.
293
+ - `badRequest()`, `unsupportedInput()`, `unsupportedAction()`, and
294
+ `assertNoUnusedMediaInputs()` for explicit provider facade failures.
295
+
296
+ These helpers keep built-in and contributed providers on the same path: a new
297
+ provider should usually define models, map a create request, map create/poll
298
+ responses, and rely on `providerOptions` only for provider-specific controls
299
+ that do not belong in the shared request protocol.
300
+
301
+ ## Provider File Layout
302
+
303
+ Use the same layout as the built-in providers:
304
+
305
+ ```text
306
+ src/<provider>/
307
+ definition.ts # model definitions, status maps, provider-local response types
308
+ provider.ts # defineHttpProvider() call and request/response mapping
309
+ provider.test.ts # harness tests for create, poll, errors, and edge statuses
310
+ index.ts # export { <provider>Provider } from "./provider.js"
311
+ ```
312
+
313
+ Then wire the provider through `src/index.ts`:
314
+
315
+ ```ts
316
+ export { exampleProvider } from "./example/index.js"
317
+
318
+ import { exampleProvider } from "./example/index.js"
319
+
320
+ export const builtinProviderPlugins = {
321
+ // existing providers...
322
+ example: exampleProvider,
323
+ }
324
+ ```
325
+
326
+ Keep provider-local helpers private unless they are clearly useful to multiple
327
+ providers. If a helper would help future provider PRs, move it to `toolkit.ts`
328
+ and use it from the built-in provider too.
329
+
330
+ ## Custom Response Parsing
331
+
332
+ ```ts
333
+ export const streamingJsonProvider = defineHttpProvider({
334
+ id: "streaming-json",
335
+ displayName: "Streaming JSON",
336
+ baseURL: "https://api.example.com/v1",
337
+ models,
338
+ create: {
339
+ request: {
340
+ path: "/generate",
341
+ body: (context) => ({ prompt: context.request.input.prompt }),
342
+ parseResponse: ({ text }) =>
343
+ text
344
+ .split(/\r?\n/)
345
+ .filter((line) => line.startsWith("data:"))
346
+ .map((line) => JSON.parse(line.slice("data:".length).trim()))
347
+ .at(-1),
348
+ parseError: ({ text }) => ({ message: text }),
349
+ },
350
+ output: (response, context) =>
351
+ completed({
352
+ context,
353
+ assets: assetFromUrl("image", response.url),
354
+ raw: response,
355
+ }),
356
+ },
357
+ })
358
+ ```
359
+
360
+ ## Custom Body Serialization
361
+
362
+ ```ts
363
+ export const formProvider = defineHttpProvider({
364
+ id: "form-provider",
365
+ displayName: "Form Provider",
366
+ baseURL: "https://api.example.com/v1",
367
+ models,
368
+ create: {
369
+ request: {
370
+ path: "/generate",
371
+ contentType: "application/x-www-form-urlencoded",
372
+ body: (context) => ({ prompt: context.request.input.prompt }),
373
+ serializeBody: ({ body }) =>
374
+ new URLSearchParams(body as Record<string, string>),
375
+ },
376
+ output: (response, context) =>
377
+ completed({
378
+ context,
379
+ assets: assetsFromImageData(response.data, context),
380
+ raw: response,
381
+ }),
382
+ },
383
+ })
384
+ ```
385
+
386
+ ## Status Mapping
387
+
388
+ Provider-level `statusMap` is available in output helpers:
389
+
390
+ ```ts
391
+ import {
392
+ completed,
393
+ defineHttpProvider,
394
+ pendingProviderJob,
395
+ pendingStatus,
396
+ providerError,
397
+ } from "@miragari/providers"
398
+
399
+ export const taskProvider = defineHttpProvider({
400
+ id: "task-provider",
401
+ displayName: "Task Provider",
402
+ baseURL: "https://api.example.com/v1",
403
+ models,
404
+ statusMap: {
405
+ done: "succeeded",
406
+ processing: "running",
407
+ error: "failed",
408
+ },
409
+ unknownStatus: "throw",
410
+ missingStatus: "throw",
411
+ create: {
412
+ request: { path: "/tasks", body: (context) => context.request.input },
413
+ output: (response, context, helpers) => {
414
+ const status = helpers.statusFrom(response.status, { context })
415
+ if (status === "succeeded") {
416
+ return completed({
417
+ context,
418
+ assets: [{ type: "image", url: response.output_url }],
419
+ raw: response,
420
+ })
421
+ }
422
+ if (status === "failed") {
423
+ throw providerError(response.error ?? response, context.provider, context.request.model)
424
+ }
425
+ return pendingProviderJob({
426
+ context,
427
+ providerJobId: response.id,
428
+ status: pendingStatus(status, "running"),
429
+ })
430
+ },
431
+ },
432
+ })
433
+ ```
434
+
435
+ `completed()` and `polledJob()` enforce terminal-state invariants by default:
436
+ `succeeded` requires at least one consumable output asset (`url` or `base64`),
437
+ and `failed` requires a normalized error. `pendingProviderJob()` only accepts
438
+ `queued` or `running`; if a create response already contains terminal output,
439
+ return `completed()` or throw a normalized provider error instead. Use
440
+ `allowEmptyResult: true` only for providers that intentionally produce no assets,
441
+ and explain that provider behavior in the PR.
442
+
443
+ For providers whose poll lifecycle needs more than a single task id, use
444
+ `providerState` on `pendingProviderJob()` and `polledJob()`:
445
+
446
+ ```ts
447
+ pendingProviderJob({
448
+ context,
449
+ providerJobId: response.id,
450
+ providerState: { pollPath: response.operationUrl },
451
+ })
452
+ ```
453
+
454
+ Poll requests can then read `context.job.providerState` without encoding
455
+ provider-specific state into `raw`. Keep `providerState` JSON-serializable so
456
+ jobs can be persisted and resumed. `polledJob()` shallow-merges new
457
+ `providerState` values with the previous job state.
458
+
459
+ ## HTTP Error Classification
460
+
461
+ `defineHttpProvider()` classifies common HTTP failures by default:
462
+
463
+ - `401` and auth-related `403` responses -> `AUTH_ERROR`
464
+ - `404` -> `NOT_FOUND`
465
+ - `429` -> `RATE_LIMITED`
466
+ - `5xx`, `429`, and common transient statuses are retryable
467
+ - safety/content-policy messages -> `CONTENT_REJECTED`
468
+ - region/location messages -> `REGION_RESTRICTED`
469
+
470
+ ## Cancellation
471
+
472
+ ```ts
473
+ export const cancellableProvider = defineHttpProvider({
474
+ id: "cancellable-provider",
475
+ displayName: "Cancellable Provider",
476
+ baseURL: "https://api.example.com/v1",
477
+ models,
478
+ create,
479
+ poll,
480
+ cancel: {
481
+ request: {
482
+ method: "DELETE",
483
+ path: (context) => `/tasks/${context.job.providerJobId}`,
484
+ },
485
+ },
486
+ })
487
+ ```
488
+
489
+ ## Provider Tests
490
+
491
+ Provider PRs should use the shared in-repo test harness to verify request
492
+ mapping and response mapping with the same runtime shape used by built-in
493
+ providers. The harness is intentionally not exported from
494
+ `@miragari/providers`; import it by relative path from provider tests.
495
+
496
+ ```ts
497
+ import {
498
+ createProviderHarness,
499
+ jsonBody,
500
+ jsonResponse,
501
+ } from "../test-harness.js"
502
+
503
+ const harness = createProviderHarness({
504
+ plugin: exampleProvider,
505
+ provider: "exampleProxy",
506
+ responses: [jsonResponse({ id: "task_1", status: "queued" })],
507
+ })
508
+
509
+ const output = await exampleProvider.driver.create(
510
+ harness.createContext({
511
+ provider: "exampleProxy",
512
+ model: "example-video",
513
+ type: "video",
514
+ input: { prompt: "test" },
515
+ }),
516
+ )
517
+
518
+ expect(harness.calls[0].url).toBe("https://api.example.com/v1/tasks")
519
+ expect(jsonBody(harness.calls[0])).toMatchObject({ prompt: "test" })
520
+ harness.expectAllResponsesUsed()
521
+ ```
522
+
523
+ When using a dynamic `responses` callback, assert the expected call count:
524
+
525
+ ```ts
526
+ const harness = createProviderHarness({
527
+ plugin: exampleProvider,
528
+ responses: (call) => jsonResponse({ ok: call.url.includes("/tasks") }),
529
+ })
530
+
531
+ // ... exercise create/poll/cancel ...
532
+
533
+ harness.expectFetchCount(2)
534
+ ```
535
+
536
+ At minimum, cover create URL/body/headers, result asset mapping, async poll
537
+ mapping, terminal failure mapping, HTTP error mapping, unexpected or missing
538
+ provider status, and cancellation when the provider supports it.
539
+
540
+ ## PR Checklist
541
+
542
+ - Add the provider under `src/<provider>/` with `definition.ts`, `provider.ts`,
543
+ `provider.test.ts`, and `index.ts`.
544
+ - Export one `*Provider` plugin from the provider folder and from
545
+ `packages/providers/src/index.ts`.
546
+ - Add the plugin to `builtinProviderPlugins` only after it uses the same public
547
+ helper path as existing built-ins.
548
+ - Define model `type`, async behavior, `capabilities.actions`, dimensions,
549
+ count behavior, and relevant media-input limits.
550
+ - Keep model capability declarations aligned with provider documentation; do
551
+ not rely on provider HTTP errors for known unsupported sizes, counts,
552
+ durations, or input media counts.
553
+ - Document each provider action by the standard fields it consumes, such as
554
+ `input.images`, `input.mask`, `options.duration`, or `providerOptions.seed`.
555
+ - Ensure unsupported actions or unsupported standard inputs fail explicitly;
556
+ never silently drop media inputs.
557
+ - Use the provider toolkit for common media input, asset, and request-type
558
+ mapping before adding custom local helpers.
559
+ - Preserve provider-specific controls through `providerOptions`.
560
+ - Normalize errors with `createMediaRouterError()` or `MediaRouterException`
561
+ from `@miragari/core`; do not hand-roll error objects.
562
+ - Custom `normalizeError` results that are not branded `MediaRouterError`
563
+ values are treated as `UNKNOWN`.
564
+ - Terminal failed jobs must set `job.error` with `createMediaRouterError()`;
565
+ provider SDK error shapes are not preserved.
566
+ - Explain any use of `allowEmptyResult`.
567
+ - Add harness tests for request mapping, result mapping, status polling, status edge cases, and error mapping.
@@ -0,0 +1,9 @@
1
+ import type { ProviderPlugin } from "@miragari/core";
2
+ export declare const builtinProviderPlugins: {
3
+ openai: ProviderPlugin;
4
+ google: ProviderPlugin;
5
+ qwen: ProviderPlugin;
6
+ happyhorse: ProviderPlugin;
7
+ volcengine: ProviderPlugin;
8
+ };
9
+ //# sourceMappingURL=builtin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builtin.d.ts","sourceRoot":"","sources":["../src/builtin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAOpD,eAAO,MAAM,sBAAsB;;;;;;CAMO,CAAA"}
@@ -0,0 +1,13 @@
1
+ import { googleProvider } from "./google/index.js";
2
+ import { happyhorseProvider } from "./happyhorse/index.js";
3
+ import { openaiProvider } from "./openai/index.js";
4
+ import { qwenProvider } from "./qwen/index.js";
5
+ import { volcengineProvider } from "./volcengine/index.js";
6
+ export const builtinProviderPlugins = {
7
+ openai: openaiProvider,
8
+ google: googleProvider,
9
+ qwen: qwenProvider,
10
+ happyhorse: happyhorseProvider,
11
+ volcengine: volcengineProvider,
12
+ };
13
+ //# sourceMappingURL=builtin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builtin.js","sourceRoot":"","sources":["../src/builtin.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAE1D,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,MAAM,EAAE,cAAc;IACtB,MAAM,EAAE,cAAc;IACtB,IAAI,EAAE,YAAY;IAClB,UAAU,EAAE,kBAAkB;IAC9B,UAAU,EAAE,kBAAkB;CACU,CAAA"}
@@ -0,0 +1,3 @@
1
+ import type { ModelDefinition } from "@miragari/core";
2
+ export declare const googleModels: Record<string, ModelDefinition>;
3
+ //# sourceMappingURL=definition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"definition.d.ts","sourceRoot":"","sources":["../../src/google/definition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAErD,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAmExD,CAAA"}
@@ -0,0 +1,69 @@
1
+ export const googleModels = {
2
+ "gemini-2.5-flash-image": {
3
+ id: "gemini-2.5-flash-image",
4
+ type: "image",
5
+ async: false,
6
+ capabilities: {
7
+ actions: {
8
+ generate: {
9
+ description: "Generate images from prompt text through generateContent.",
10
+ consumes: ["input.prompt", "options.width", "options.height"],
11
+ },
12
+ reference: {
13
+ description: "Generate images using prompt text and reference images.",
14
+ consumes: ["input.prompt", "input.images", "options.width", "options.height"],
15
+ },
16
+ },
17
+ dimensions: {
18
+ aspectRatios: ["1:1", "16:9", "9:16", "4:3", "3:4"],
19
+ image: { resolutionTiers: ["1K", "2K"] },
20
+ },
21
+ count: { supported: false, max: 1, strategy: "split" },
22
+ },
23
+ },
24
+ "gemini-3-pro-image": {
25
+ id: "gemini-3-pro-image",
26
+ type: "image",
27
+ async: false,
28
+ capabilities: {
29
+ actions: {
30
+ generate: {
31
+ description: "Generate images from prompt text through generateContent.",
32
+ consumes: ["input.prompt", "options.width", "options.height"],
33
+ },
34
+ reference: {
35
+ description: "Generate images using prompt text and reference images.",
36
+ consumes: ["input.prompt", "input.images", "options.width", "options.height"],
37
+ },
38
+ },
39
+ dimensions: {
40
+ aspectRatios: ["1:1", "16:9", "9:16", "4:3", "3:4"],
41
+ image: { resolutionTiers: ["1K", "2K", "4K"] },
42
+ },
43
+ count: { supported: false, max: 1, strategy: "split" },
44
+ },
45
+ },
46
+ "gemini-3.1-flash-image": {
47
+ id: "gemini-3.1-flash-image",
48
+ type: "image",
49
+ async: false,
50
+ capabilities: {
51
+ actions: {
52
+ generate: {
53
+ description: "Generate images from prompt text through generateContent.",
54
+ consumes: ["input.prompt", "options.width", "options.height"],
55
+ },
56
+ reference: {
57
+ description: "Generate images using prompt text and reference images.",
58
+ consumes: ["input.prompt", "input.images", "options.width", "options.height"],
59
+ },
60
+ },
61
+ dimensions: {
62
+ aspectRatios: ["1:1", "16:9", "9:16", "4:3", "3:4"],
63
+ image: { resolutionTiers: ["1K", "2K", "4K"] },
64
+ },
65
+ count: { supported: false, max: 1, strategy: "split" },
66
+ },
67
+ },
68
+ };
69
+ //# sourceMappingURL=definition.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"definition.js","sourceRoot":"","sources":["../../src/google/definition.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,YAAY,GAAoC;IAC3D,wBAAwB,EAAE;QACxB,EAAE,EAAE,wBAAwB;QAC5B,IAAI,EAAE,OAAO;QACb,KAAK,EAAE,KAAK;QACZ,YAAY,EAAE;YACZ,OAAO,EAAE;gBACP,QAAQ,EAAE;oBACR,WAAW,EAAE,2DAA2D;oBACxE,QAAQ,EAAE,CAAC,cAAc,EAAE,eAAe,EAAE,gBAAgB,CAAC;iBAC9D;gBACD,SAAS,EAAE;oBACT,WAAW,EAAE,yDAAyD;oBACtE,QAAQ,EAAE,CAAC,cAAc,EAAE,cAAc,EAAE,eAAe,EAAE,gBAAgB,CAAC;iBAC9E;aACF;YACD,UAAU,EAAE;gBACV,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC;gBACnD,KAAK,EAAE,EAAE,eAAe,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE;aACzC;YACD,KAAK,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE;SACvD;KACF;IACD,oBAAoB,EAAE;QACpB,EAAE,EAAE,oBAAoB;QACxB,IAAI,EAAE,OAAO;QACb,KAAK,EAAE,KAAK;QACZ,YAAY,EAAE;YACZ,OAAO,EAAE;gBACP,QAAQ,EAAE;oBACR,WAAW,EAAE,2DAA2D;oBACxE,QAAQ,EAAE,CAAC,cAAc,EAAE,eAAe,EAAE,gBAAgB,CAAC;iBAC9D;gBACD,SAAS,EAAE;oBACT,WAAW,EAAE,yDAAyD;oBACtE,QAAQ,EAAE,CAAC,cAAc,EAAE,cAAc,EAAE,eAAe,EAAE,gBAAgB,CAAC;iBAC9E;aACF;YACD,UAAU,EAAE;gBACV,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC;gBACnD,KAAK,EAAE,EAAE,eAAe,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE;aAC/C;YACD,KAAK,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE;SACvD;KACF;IACD,wBAAwB,EAAE;QACxB,EAAE,EAAE,wBAAwB;QAC5B,IAAI,EAAE,OAAO;QACb,KAAK,EAAE,KAAK;QACZ,YAAY,EAAE;YACZ,OAAO,EAAE;gBACP,QAAQ,EAAE;oBACR,WAAW,EAAE,2DAA2D;oBACxE,QAAQ,EAAE,CAAC,cAAc,EAAE,eAAe,EAAE,gBAAgB,CAAC;iBAC9D;gBACD,SAAS,EAAE;oBACT,WAAW,EAAE,yDAAyD;oBACtE,QAAQ,EAAE,CAAC,cAAc,EAAE,cAAc,EAAE,eAAe,EAAE,gBAAgB,CAAC;iBAC9E;aACF;YACD,UAAU,EAAE;gBACV,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC;gBACnD,KAAK,EAAE,EAAE,eAAe,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE;aAC/C;YACD,KAAK,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE;SACvD;KACF;CACF,CAAA"}
@@ -0,0 +1,2 @@
1
+ export { googleProvider } from "./provider.js";
2
+ //# sourceMappingURL=index.d.ts.map