@tanstack/ai-byteplus 0.0.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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -0
  3. package/dist/esm/adapters/image.d.ts +89 -0
  4. package/dist/esm/adapters/image.js +229 -0
  5. package/dist/esm/adapters/image.js.map +1 -0
  6. package/dist/esm/adapters/text.d.ts +163 -0
  7. package/dist/esm/adapters/text.js +347 -0
  8. package/dist/esm/adapters/text.js.map +1 -0
  9. package/dist/esm/adapters/transcription.d.ts +102 -0
  10. package/dist/esm/adapters/transcription.js +274 -0
  11. package/dist/esm/adapters/transcription.js.map +1 -0
  12. package/dist/esm/adapters/tts.d.ts +143 -0
  13. package/dist/esm/adapters/tts.js +307 -0
  14. package/dist/esm/adapters/tts.js.map +1 -0
  15. package/dist/esm/adapters/video.d.ts +182 -0
  16. package/dist/esm/adapters/video.js +442 -0
  17. package/dist/esm/adapters/video.js.map +1 -0
  18. package/dist/esm/audio/transcription-provider-options.d.ts +46 -0
  19. package/dist/esm/audio/tts-provider-options.d.ts +114 -0
  20. package/dist/esm/audio/wire-types.d.ts +261 -0
  21. package/dist/esm/audio/wire-types.js +28 -0
  22. package/dist/esm/audio/wire-types.js.map +1 -0
  23. package/dist/esm/image/image-provider-options.d.ts +165 -0
  24. package/dist/esm/image/image-provider-options.js +134 -0
  25. package/dist/esm/image/image-provider-options.js.map +1 -0
  26. package/dist/esm/image/wire-types.d.ts +149 -0
  27. package/dist/esm/index.d.ts +25 -0
  28. package/dist/esm/index.js +11 -0
  29. package/dist/esm/message-types.d.ts +154 -0
  30. package/dist/esm/model-meta.d.ts +594 -0
  31. package/dist/esm/model-meta.js +619 -0
  32. package/dist/esm/model-meta.js.map +1 -0
  33. package/dist/esm/text/text-provider-options.d.ts +109 -0
  34. package/dist/esm/utils/client.d.ts +183 -0
  35. package/dist/esm/utils/client.js +253 -0
  36. package/dist/esm/utils/client.js.map +1 -0
  37. package/dist/esm/video/video-provider-options.d.ts +197 -0
  38. package/dist/esm/video/video-provider-options.js +191 -0
  39. package/dist/esm/video/video-provider-options.js.map +1 -0
  40. package/dist/esm/video/wire-types.d.ts +248 -0
  41. package/package.json +77 -0
  42. package/src/adapters/image.ts +409 -0
  43. package/src/adapters/text.ts +539 -0
  44. package/src/adapters/transcription.ts +479 -0
  45. package/src/adapters/tts.ts +447 -0
  46. package/src/adapters/video.ts +732 -0
  47. package/src/audio/transcription-provider-options.ts +46 -0
  48. package/src/audio/tts-provider-options.ts +122 -0
  49. package/src/audio/wire-types.ts +290 -0
  50. package/src/image/image-provider-options.ts +288 -0
  51. package/src/image/wire-types.ts +169 -0
  52. package/src/index.ts +222 -0
  53. package/src/message-types.ts +169 -0
  54. package/src/model-meta.ts +954 -0
  55. package/src/text/text-provider-options.ts +151 -0
  56. package/src/utils/client.ts +377 -0
  57. package/src/video/video-provider-options.ts +361 -0
  58. package/src/video/wire-types.ts +293 -0
@@ -0,0 +1,409 @@
1
+ import { resolveMediaPrompt } from '@tanstack/ai'
2
+ import { BaseImageAdapter } from '@tanstack/ai/adapters'
3
+ import { toRunErrorPayload } from '@tanstack/ai/adapter-internals'
4
+ import { generateId } from '@tanstack/ai-utils'
5
+ import {
6
+ bytePlusArkError,
7
+ bytePlusArkHeaders,
8
+ bytePlusTimeoutSignal,
9
+ describeBody,
10
+ getBytePlusArkApiKeyFromEnv,
11
+ readJsonBody,
12
+ toHeaderRecord,
13
+ withBytePlusArkDefaults,
14
+ } from '../utils/client'
15
+ import {
16
+ resolveBytePlusImageSize,
17
+ resolveBytePlusSequentialImages,
18
+ validateBytePlusImagePrompt,
19
+ validateBytePlusReferenceImages,
20
+ } from '../image/image-provider-options'
21
+ import type {
22
+ GeneratedImage,
23
+ ImageGenerationOptions,
24
+ ImageGenerationResult,
25
+ ImagePart,
26
+ MediaInputMetadata,
27
+ TokenUsage,
28
+ } from '@tanstack/ai'
29
+ import type { InternalLogger } from '@tanstack/ai/adapter-internals'
30
+ import type {
31
+ BytePlusImageErrorObject,
32
+ BytePlusImageGenerationRequest,
33
+ BytePlusImageGenerationResponse,
34
+ BytePlusImageUsage,
35
+ } from '../image/wire-types'
36
+ import type {
37
+ BytePlusImageModelInputModalitiesByName,
38
+ BytePlusImageModelProviderOptionsByName,
39
+ BytePlusImageProviderOptions,
40
+ } from '../image/image-provider-options'
41
+ import type {
42
+ BytePlusImageModel,
43
+ BytePlusImageModelSizeByName,
44
+ } from '../model-meta'
45
+ import type { BytePlusArkConfig } from '../utils/client'
46
+
47
+ /**
48
+ * Configuration for the BytePlus Seedream image adapter.
49
+ */
50
+ export interface BytePlusImageConfig extends BytePlusArkConfig {}
51
+
52
+ /**
53
+ * Roles Seedream can honour. Every input image is a reference — there is no
54
+ * inpainting mask, control-image or frame channel — so this is an allow-list
55
+ * rather than a deny-list: a role added to the core union later (or a
56
+ * video-oriented one like `start_frame`) fails loudly here instead of being
57
+ * silently flattened into a plain reference.
58
+ */
59
+ const SUPPORTED_INPUT_ROLES: ReadonlySet<string> = new Set([
60
+ 'reference',
61
+ 'character',
62
+ ])
63
+
64
+ /**
65
+ * Converts a prompt image part to the string Seedream's `image` field takes:
66
+ * URLs pass through (BytePlus fetches them server-side), data sources become
67
+ * data URIs. BytePlus requires the format in `data:image/<format>;base64,` to
68
+ * be lowercase, so the mime type is lowercased on the way out.
69
+ */
70
+ function imagePartToImageRef(part: ImagePart<MediaInputMetadata>): string {
71
+ const { source } = part
72
+ if (source.type === 'url') return source.value
73
+ if (source.value.startsWith('data:')) return source.value
74
+ return `data:${source.mimeType.toLowerCase()};base64,${source.value}`
75
+ }
76
+
77
+ /**
78
+ * Renders provider error objects as `code: message` pairs for a log line or
79
+ * an error message.
80
+ */
81
+ function describeFailures(
82
+ failures: ReadonlyArray<BytePlusImageErrorObject>,
83
+ ): string {
84
+ return failures
85
+ .map((failure) =>
86
+ [failure.code, failure.message].filter(Boolean).join(': '),
87
+ )
88
+ .filter((text) => text.length > 0)
89
+ .join('; ')
90
+ }
91
+
92
+ /**
93
+ * Maps Seedream's usage block onto `TokenUsage`.
94
+ *
95
+ * BytePlus bills per generated image and does not count input tokens, so
96
+ * `promptTokens` is always 0 and `generated_images` is surfaced as
97
+ * `unitsBilled` — the count the price is applied to.
98
+ */
99
+ function buildBytePlusImageUsage(
100
+ usage: BytePlusImageUsage | undefined,
101
+ ): TokenUsage | undefined {
102
+ if (!usage) return undefined
103
+
104
+ const completionTokens = usage.output_tokens ?? 0
105
+ return {
106
+ promptTokens: 0,
107
+ completionTokens,
108
+ totalTokens: usage.total_tokens ?? completionTokens,
109
+ ...(usage.generated_images !== undefined && {
110
+ unitsBilled: usage.generated_images,
111
+ }),
112
+ }
113
+ }
114
+
115
+ /**
116
+ * BytePlus Seedream image generation adapter.
117
+ *
118
+ * Drives Ark's `POST /images/generations` endpoint directly rather than
119
+ * through the OpenAI SDK: the endpoint takes size tokens (`2K`) as well as
120
+ * pixel sizes, has no `n` parameter, and carries reference images for editing
121
+ * in the generation request instead of a separate edits endpoint.
122
+ *
123
+ * Features:
124
+ * - Text-to-image and image-conditioned generation (editing, multi-reference)
125
+ * from a single call, with per-model reference-count limits enforced.
126
+ * - Size validation across both accepted forms.
127
+ * - `numberOfImages` mapped onto Seedream's group-image mode.
128
+ *
129
+ * @example
130
+ * ```typescript
131
+ * const adapter = byteplusImage('seedream-4-0-250828')
132
+ * const result = await generateImage({
133
+ * adapter,
134
+ * prompt: 'A guitar in a sunlit workshop',
135
+ * size: '2K',
136
+ * modelOptions: { watermark: false },
137
+ * })
138
+ * ```
139
+ */
140
+ export class BytePlusImageAdapter<
141
+ TModel extends BytePlusImageModel,
142
+ > extends BaseImageAdapter<
143
+ TModel,
144
+ BytePlusImageProviderOptions,
145
+ BytePlusImageModelProviderOptionsByName,
146
+ BytePlusImageModelSizeByName,
147
+ BytePlusImageModelInputModalitiesByName
148
+ > {
149
+ override readonly kind = 'image' as const
150
+ readonly name = 'byteplus' as const
151
+
152
+ /** Config with the Ark base URL resolved and trailing slashes trimmed. */
153
+ private readonly clientConfig: Omit<BytePlusImageConfig, 'baseURL'> & {
154
+ baseURL: string
155
+ }
156
+
157
+ constructor(model: TModel, config: BytePlusImageConfig) {
158
+ super(model, {})
159
+ this.clientConfig = withBytePlusArkDefaults(config)
160
+ }
161
+
162
+ async generateImages(
163
+ options: ImageGenerationOptions<
164
+ BytePlusImageProviderOptions,
165
+ BytePlusImageModelSizeByName[TModel]
166
+ >,
167
+ ): Promise<ImageGenerationResult> {
168
+ const { numberOfImages, size, modelOptions, logger } = options
169
+ const model = this.model
170
+
171
+ const resolved = resolveMediaPrompt(options.prompt)
172
+
173
+ if (resolved.videos.length > 0 || resolved.audios.length > 0) {
174
+ throw new Error(
175
+ `byteplus.generateImages does not support video / audio prompt parts on model ${model}.`,
176
+ )
177
+ }
178
+
179
+ const unsupportedRole = resolved.images.find(
180
+ (part) =>
181
+ part.metadata?.role !== undefined &&
182
+ !SUPPORTED_INPUT_ROLES.has(part.metadata.role),
183
+ )
184
+ if (unsupportedRole) {
185
+ throw new Error(
186
+ `byteplus: Seedream has no ${unsupportedRole.metadata?.role} input; ` +
187
+ `it accepts reference images only (${[...SUPPORTED_INPUT_ROLES].join(', ')}).`,
188
+ )
189
+ }
190
+
191
+ validateBytePlusImagePrompt(model, resolved.text)
192
+ validateBytePlusReferenceImages(model, resolved.images.length)
193
+
194
+ const imageRefs = resolved.images.map(imagePartToImageRef)
195
+ const request: BytePlusImageGenerationRequest = {
196
+ ...(imageRefs.length > 0 && { image: imageRefs }),
197
+ ...(size !== undefined && {
198
+ size: resolveBytePlusImageSize(size),
199
+ }),
200
+ ...resolveBytePlusSequentialImages(model, numberOfImages),
201
+ // Explicit provider options win over the values derived from the
202
+ // generic options above (e.g. forcing `sequential_image_generation`).
203
+ ...modelOptions,
204
+ model,
205
+ prompt: resolved.text,
206
+ }
207
+
208
+ try {
209
+ logger.request(
210
+ `activity=image provider=${this.name} model=${model} size=${request.size ?? 'default'} refs=${imageRefs.length}`,
211
+ { provider: this.name, model },
212
+ )
213
+
214
+ const fetchImpl = this.clientConfig.fetch ?? fetch
215
+ const signal = bytePlusTimeoutSignal(this.clientConfig.timeout)
216
+ const response = await fetchImpl(
217
+ `${this.clientConfig.baseURL}/images/generations`,
218
+ {
219
+ method: 'POST',
220
+ ...(signal && { signal }),
221
+ headers: bytePlusArkHeaders(
222
+ this.clientConfig.apiKey,
223
+ toHeaderRecord(this.clientConfig.defaultHeaders),
224
+ ),
225
+ body: JSON.stringify(request),
226
+ },
227
+ )
228
+
229
+ const body = await readJsonBody(response)
230
+ if (!response.ok) {
231
+ throw bytePlusArkError(response.status, body, 'image generation')
232
+ }
233
+
234
+ return this.transformResponse(body, logger, numberOfImages)
235
+ } catch (error: unknown) {
236
+ logger.errors(`${this.name}.generateImages fatal`, {
237
+ error: toRunErrorPayload(error, `${this.name}.generateImages failed`),
238
+ source: `${this.name}.generateImages`,
239
+ })
240
+ throw error
241
+ }
242
+ }
243
+
244
+ private transformResponse(
245
+ body: unknown,
246
+ logger: InternalLogger,
247
+ numberOfImages: number | undefined,
248
+ ): ImageGenerationResult {
249
+ // Shape pinned by a live seedream-4-0-250828 call and the Ark OpenAPI
250
+ // document. Validate rather than cast: `readJsonBody` returns `undefined`
251
+ // for an empty body and the raw text for a non-JSON one (an HTML error
252
+ // page from a proxy in front of the API), and casting either would report
253
+ // "returned no images" with the body — the only evidence of what actually
254
+ // happened — thrown away.
255
+ if (typeof body !== 'object' || body === null) {
256
+ throw bytePlusArkError(
257
+ 200,
258
+ body,
259
+ 'image generation returned a non-object body',
260
+ )
261
+ }
262
+ const payload = body as BytePlusImageGenerationResponse
263
+
264
+ const images: Array<GeneratedImage> = []
265
+ const failures: Array<BytePlusImageErrorObject> = []
266
+ // Items matching none of the three known shapes. Ark's OpenAPI document
267
+ // describes a second, nested item form, so this is a live possibility
268
+ // rather than a defensive branch — and an unrecognized item that is
269
+ // neither counted nor reported turns provider drift into an
270
+ // "returned no images" with no attribution at all.
271
+ let unrecognized = 0
272
+ for (const item of payload.data ?? []) {
273
+ if (item.b64_json) {
274
+ images.push({ b64Json: item.b64_json })
275
+ } else if (item.url) {
276
+ images.push({ url: item.url })
277
+ } else if (item.error) {
278
+ // Group-image mode reports per-image failures alongside successes;
279
+ // dropping them silently would make a short result look complete.
280
+ failures.push(item.error)
281
+ } else {
282
+ unrecognized += 1
283
+ }
284
+ }
285
+ if (payload.error) failures.push(payload.error)
286
+
287
+ if (unrecognized > 0) {
288
+ logger.errors(
289
+ `${this.name}.generateImages: ${unrecognized} response item(s) matched ` +
290
+ `none of b64_json / url / error — the response shape may have changed.`,
291
+ {
292
+ source: `${this.name}.generateImages`,
293
+ provider: this.name,
294
+ model: this.model,
295
+ body,
296
+ },
297
+ )
298
+ }
299
+
300
+ if (images.length === 0) {
301
+ const detail =
302
+ describeFailures(failures) ||
303
+ (unrecognized > 0
304
+ ? `${unrecognized} unrecognized response item(s): ${describeBody(body) ?? ''}`
305
+ : '')
306
+ throw new Error(
307
+ `byteplus: image generation returned no images` +
308
+ (detail ? `: ${detail}` : '.'),
309
+ )
310
+ }
311
+
312
+ if (failures.length > 0) {
313
+ logger.errors(
314
+ `${this.name}.generateImages dropped ${failures.length} failed image(s): ${describeFailures(failures)}`,
315
+ {
316
+ source: `${this.name}.generateImages`,
317
+ provider: this.name,
318
+ model: this.model,
319
+ failures,
320
+ },
321
+ )
322
+ // The caller asked for a group and is getting a short array. Warn
323
+ // unconditionally: the `numberOfImages` warning below only fires when
324
+ // the count was set explicitly, so a partial failure would otherwise
325
+ // return successfully with no signal at all.
326
+ logger.warn(
327
+ `byteplus: ${failures.length} of ${failures.length + images.length} ` +
328
+ `images failed to generate; returning ${images.length}.`,
329
+ { provider: this.name, model: this.model },
330
+ )
331
+ }
332
+
333
+ if (numberOfImages !== undefined && images.length < numberOfImages) {
334
+ logger.warn(
335
+ `byteplus: requested ${numberOfImages} images, received ${images.length}. ` +
336
+ `Seedream has no exact count — sequential_image_generation.max_images is ` +
337
+ `an upper bound and the model decides how many the prompt warrants.`,
338
+ { provider: this.name, model: this.model },
339
+ )
340
+ }
341
+
342
+ const usage = buildBytePlusImageUsage(payload.usage)
343
+
344
+ return {
345
+ id: generateId(this.name),
346
+ model: this.model,
347
+ images,
348
+ ...(usage ? { usage } : {}),
349
+ }
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Creates a BytePlus Seedream image adapter with an explicit API key.
355
+ * Type resolution happens here at the call site.
356
+ *
357
+ * @param model - The model name (e.g., 'seedream-4-0-250828')
358
+ * @param apiKey - Your BytePlus Ark API key
359
+ * @param config - Optional additional configuration
360
+ * @returns Configured BytePlus image adapter instance with resolved types
361
+ *
362
+ * @example
363
+ * ```typescript
364
+ * const adapter = createBytePlusImage('seedream-5-0-260128', 'ark-...')
365
+ *
366
+ * const result = await generateImage({
367
+ * adapter,
368
+ * prompt: 'A cute baby sea otter',
369
+ * size: '2K',
370
+ * })
371
+ * ```
372
+ */
373
+ export function createBytePlusImage<TModel extends BytePlusImageModel>(
374
+ model: TModel,
375
+ apiKey: string,
376
+ config?: Omit<BytePlusImageConfig, 'apiKey'>,
377
+ ): BytePlusImageAdapter<TModel> {
378
+ return new BytePlusImageAdapter(model, { apiKey, ...config })
379
+ }
380
+
381
+ /**
382
+ * Creates a BytePlus Seedream image adapter, reading `ARK_API_KEY` from the
383
+ * environment. Type resolution happens here at the call site.
384
+ *
385
+ * Note that Ark keys are region-isolated: a key issued for `ap-southeast`
386
+ * does not work against the EU host.
387
+ *
388
+ * @param model - The model name (e.g., 'seedream-4-0-250828')
389
+ * @param config - Optional configuration (excluding apiKey, auto-detected)
390
+ * @returns Configured BytePlus image adapter instance with resolved types
391
+ * @throws Error if ARK_API_KEY is not found in environment
392
+ *
393
+ * @example
394
+ * ```typescript
395
+ * const adapter = byteplusImage('seedream-4-0-250828')
396
+ *
397
+ * const result = await generateImage({
398
+ * adapter,
399
+ * prompt: 'A beautiful sunset over mountains',
400
+ * modelOptions: { watermark: false },
401
+ * })
402
+ * ```
403
+ */
404
+ export function byteplusImage<TModel extends BytePlusImageModel>(
405
+ model: TModel,
406
+ config?: Omit<BytePlusImageConfig, 'apiKey'>,
407
+ ): BytePlusImageAdapter<TModel> {
408
+ return createBytePlusImage(model, getBytePlusArkApiKeyFromEnv(), config)
409
+ }