@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.
- package/LICENSE +1 -0
- package/README.md +567 -0
- package/dist/builtin.d.ts +9 -0
- package/dist/builtin.d.ts.map +1 -0
- package/dist/builtin.js +13 -0
- package/dist/builtin.js.map +1 -0
- package/dist/google/definition.d.ts +3 -0
- package/dist/google/definition.d.ts.map +1 -0
- package/dist/google/definition.js +69 -0
- package/dist/google/definition.js.map +1 -0
- package/dist/google/index.d.ts +2 -0
- package/dist/google/index.d.ts.map +1 -0
- package/dist/google/index.js +2 -0
- package/dist/google/index.js.map +1 -0
- package/dist/google/provider.d.ts +2 -0
- package/dist/google/provider.d.ts.map +1 -0
- package/dist/google/provider.js +133 -0
- package/dist/google/provider.js.map +1 -0
- package/dist/happyhorse/definition.d.ts +3 -0
- package/dist/happyhorse/definition.d.ts.map +1 -0
- package/dist/happyhorse/definition.js +57 -0
- package/dist/happyhorse/definition.js.map +1 -0
- package/dist/happyhorse/index.d.ts +2 -0
- package/dist/happyhorse/index.d.ts.map +1 -0
- package/dist/happyhorse/index.js +2 -0
- package/dist/happyhorse/index.js.map +1 -0
- package/dist/happyhorse/provider.d.ts +2 -0
- package/dist/happyhorse/provider.d.ts.map +1 -0
- package/dist/happyhorse/provider.js +253 -0
- package/dist/happyhorse/provider.js.map +1 -0
- package/dist/http.d.ts +66 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +236 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/media-router.d.ts +35 -0
- package/dist/media-router.d.ts.map +1 -0
- package/dist/media-router.js +202 -0
- package/dist/media-router.js.map +1 -0
- package/dist/openai/definition.d.ts +4 -0
- package/dist/openai/definition.d.ts.map +1 -0
- package/dist/openai/definition.js +92 -0
- package/dist/openai/definition.js.map +1 -0
- package/dist/openai/index.d.ts +2 -0
- package/dist/openai/index.d.ts.map +1 -0
- package/dist/openai/index.js +2 -0
- package/dist/openai/index.js.map +1 -0
- package/dist/openai/provider.d.ts +2 -0
- package/dist/openai/provider.d.ts.map +1 -0
- package/dist/openai/provider.js +188 -0
- package/dist/openai/provider.js.map +1 -0
- package/dist/qwen/definition.d.ts +4 -0
- package/dist/qwen/definition.d.ts.map +1 -0
- package/dist/qwen/definition.js +210 -0
- package/dist/qwen/definition.js.map +1 -0
- package/dist/qwen/index.d.ts +2 -0
- package/dist/qwen/index.d.ts.map +1 -0
- package/dist/qwen/index.js +2 -0
- package/dist/qwen/index.js.map +1 -0
- package/dist/qwen/provider.d.ts +2 -0
- package/dist/qwen/provider.d.ts.map +1 -0
- package/dist/qwen/provider.js +237 -0
- package/dist/qwen/provider.js.map +1 -0
- package/dist/toolkit.d.ts +137 -0
- package/dist/toolkit.d.ts.map +1 -0
- package/dist/toolkit.js +490 -0
- package/dist/toolkit.js.map +1 -0
- package/dist/volcengine/definition.d.ts +4 -0
- package/dist/volcengine/definition.d.ts.map +1 -0
- package/dist/volcengine/definition.js +130 -0
- package/dist/volcengine/definition.js.map +1 -0
- package/dist/volcengine/index.d.ts +2 -0
- package/dist/volcengine/index.d.ts.map +1 -0
- package/dist/volcengine/index.js +2 -0
- package/dist/volcengine/index.js.map +1 -0
- package/dist/volcengine/provider.d.ts +2 -0
- package/dist/volcengine/provider.d.ts.map +1 -0
- package/dist/volcengine/provider.js +232 -0
- package/dist/volcengine/provider.js.map +1 -0
- 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"}
|
package/dist/builtin.js
ADDED
|
@@ -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 @@
|
|
|
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"}
|