@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,539 @@
1
+ import OpenAI from 'openai'
2
+ import { EventType } from '@tanstack/ai'
3
+ import { OpenAIBaseChatCompletionsTextAdapter } from '@tanstack/openai-base'
4
+ import { generateId } from '@tanstack/ai-utils'
5
+ import {
6
+ BYTEPLUS_STRUCTURED_OUTPUT_CHAT_MODELS,
7
+ emitsEncryptedContent,
8
+ supportsStructuredOutput,
9
+ } from '../model-meta'
10
+ import {
11
+ getBytePlusArkApiKeyFromEnv,
12
+ withBytePlusArkDefaults,
13
+ } from '../utils/client'
14
+ import type {
15
+ StructuredOutputOptions,
16
+ StructuredOutputResult,
17
+ } from '@tanstack/ai/adapters'
18
+ import type {
19
+ ContentPart,
20
+ ContentPartSource,
21
+ Modality,
22
+ ModelMessage,
23
+ StreamChunk,
24
+ TextOptions,
25
+ } from '@tanstack/ai'
26
+ import type {
27
+ ChatCompletionContentPart,
28
+ ChatCompletionMessageParam,
29
+ } from 'openai/resources/chat/completions/completions'
30
+ import type {
31
+ BYTEPLUS_CHAT_MODELS,
32
+ BytePlusChatModelToolCapabilitiesByName,
33
+ ResolveInputModalities,
34
+ ResolveProviderOptions,
35
+ } from '../model-meta'
36
+ import type {
37
+ BytePlusAudioMetadata,
38
+ BytePlusChatContentPart,
39
+ BytePlusEncryptedContentFields,
40
+ BytePlusImageMetadata,
41
+ BytePlusInputAudioContentPart,
42
+ BytePlusMessageMetadataByModality,
43
+ BytePlusStreamDeltaExtras,
44
+ BytePlusVideoMetadata,
45
+ } from '../message-types'
46
+ import type { BytePlusArkConfig } from '../utils/client'
47
+
48
+ type ResolveToolCapabilities<TModel extends string> =
49
+ TModel extends keyof BytePlusChatModelToolCapabilitiesByName
50
+ ? NonNullable<BytePlusChatModelToolCapabilitiesByName[TModel]>
51
+ : readonly []
52
+
53
+ /**
54
+ * Configuration for the BytePlus text adapter.
55
+ */
56
+ export interface BytePlusTextConfig extends BytePlusArkConfig {}
57
+
58
+ /**
59
+ * Re-export of the public provider options type.
60
+ */
61
+ export type { BytePlusTextProviderOptions } from '../text/text-provider-options'
62
+
63
+ /**
64
+ * BytePlus ModelArk Text (Chat) Adapter
65
+ *
66
+ * Tree-shakeable adapter for the Seed / GLM / DeepSeek / gpt-oss chat models
67
+ * on BytePlus ModelArk. Ark serves an OpenAI-compatible Chat Completions
68
+ * endpoint, so this drives the OpenAI SDK against Ark's `baseURL` — the same
69
+ * pattern as `ai-groq` and `ai-grok`.
70
+ *
71
+ * Three Ark behaviours are handled on top of the shared base:
72
+ *
73
+ * 1. **`reasoning_content` deltas** — Ark streams reasoning under
74
+ * `delta.reasoning_content` rather than the OpenAI `reasoning` field.
75
+ * 2. **`encrypted_content` round-trip** — thinking-summary models emit an
76
+ * opaque signature over the reasoning trace. See
77
+ * {@link BytePlusTextAdapter.processStreamChunks} and
78
+ * {@link BytePlusTextAdapter.convertMessage}.
79
+ * 3. **Per-model structured-output gating** — only 10 of the 18 shipped chat
80
+ * models honour `response_format: json_schema` (glm-4-7 accepts it and then
81
+ * ignores the schema), and Ark rejects `json_object` everywhere, so there
82
+ * is no JSON-mode fallback.
83
+ */
84
+ export class BytePlusTextAdapter<
85
+ TModel extends (typeof BYTEPLUS_CHAT_MODELS)[number],
86
+ // `Record<string, any>` (not `unknown`) mirrors the OpenAI/Groq/Grok text
87
+ // adapters: the resolved provider options are an interface with no index
88
+ // signature, assignable to `Record<string, any>` but not to
89
+ // `Record<string, unknown>`. See issue #821.
90
+ TProviderOptions extends Record<string, any> = ResolveProviderOptions<TModel>,
91
+ TInputModalities extends ReadonlyArray<Modality> =
92
+ ResolveInputModalities<TModel>,
93
+ TToolCapabilities extends ReadonlyArray<string> =
94
+ ResolveToolCapabilities<TModel>,
95
+ > extends OpenAIBaseChatCompletionsTextAdapter<
96
+ TModel,
97
+ TProviderOptions,
98
+ TInputModalities,
99
+ BytePlusMessageMetadataByModality,
100
+ TToolCapabilities
101
+ > {
102
+ override readonly kind = 'text' as const
103
+ override readonly name = 'byteplus' as const
104
+
105
+ constructor(config: BytePlusTextConfig, model: TModel) {
106
+ super(model, 'byteplus', new OpenAI(withBytePlusArkDefaults(config)))
107
+ }
108
+
109
+ /**
110
+ * Surfaces Ark's reasoning deltas. Thinking-enabled models stream the
111
+ * reasoning trace as `delta.reasoning_content` (the OpenAI chunk shape has
112
+ * no reasoning field); the base routes this hook through both `chatStream`
113
+ * and `structuredOutputStream`.
114
+ */
115
+ protected override extractReasoning(
116
+ chunk: OpenAI.Chat.Completions.ChatCompletionChunk,
117
+ ): { text: string } | undefined {
118
+ const delta = chunk.choices[0]?.delta as
119
+ | BytePlusStreamDeltaExtras
120
+ | undefined
121
+ const raw = delta?.reasoning_content
122
+ if (typeof raw === 'string' && raw.length > 0) {
123
+ return { text: raw }
124
+ }
125
+ return undefined
126
+ }
127
+
128
+ /**
129
+ * Captures Ark's `encrypted_content` and attaches it to the reasoning
130
+ * step's `STEP_FINISHED` event as its `signature`.
131
+ *
132
+ * On a thinking-summary model Ark streams the whole blob as one dedicated
133
+ * chunk (empty `content` and `reasoning_content`) sitting between the
134
+ * reasoning deltas and the content deltas — so it is always captured before
135
+ * the base closes the reasoning lifecycle at the first content delta.
136
+ *
137
+ * `signature` is the framework's existing provider-signature seam: the chat
138
+ * engine stores it on the `ThinkingPart`, which
139
+ * `buildAssistantMessages` carries into `ModelMessage.thinking[].signature`,
140
+ * which {@link BytePlusTextAdapter.convertMessage} echoes back to Ark on the
141
+ * next turn. No base-class change is needed — this is the same round-trip
142
+ * Anthropic's thinking signatures use.
143
+ *
144
+ * Only `chatStream` is covered: `structuredOutputStream` drives the SDK
145
+ * directly in the base with no per-chunk seam, so a structured-output turn
146
+ * does not capture the blob. Ark accepts a following turn without it, so the
147
+ * consequence is a lost reasoning-cache hit, not a failed request.
148
+ */
149
+ protected override async *processStreamChunks(
150
+ stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>,
151
+ options: TextOptions,
152
+ aguiState: {
153
+ runId: string
154
+ threadId: string
155
+ messageId: string
156
+ hasEmittedRunStarted: boolean
157
+ },
158
+ ): AsyncIterable<StreamChunk> {
159
+ const captured: { encryptedContent?: string } = {}
160
+
161
+ for await (const event of super.processStreamChunks(
162
+ captureEncryptedContent(stream, captured),
163
+ options,
164
+ aguiState,
165
+ )) {
166
+ if (
167
+ event.type === EventType.STEP_FINISHED &&
168
+ captured.encryptedContent !== undefined &&
169
+ event.signature === undefined
170
+ ) {
171
+ // `delta` is stamped alongside the signature because the two consumers
172
+ // read this event differently. `chat()`'s server agent loop accumulates
173
+ // thinking ONLY from `STEP_FINISHED.delta` and then drops the whole
174
+ // step — signature included — when the accumulated content is empty
175
+ // (`finalizeCurrentThinkingStep`); the OpenAI base emits `content` but
176
+ // never `delta`, so without this the blob never reaches the
177
+ // continuation message. The client `StreamProcessor` can't double-count
178
+ // it: it short-circuits STEP_FINISHED content once
179
+ // `hasSeenReasoningEvents` is set, which the REASONING_MESSAGE_CONTENT
180
+ // events preceding every STEP_FINISHED here always set.
181
+ yield {
182
+ ...event,
183
+ signature: captured.encryptedContent,
184
+ delta: event.delta ?? event.content ?? '',
185
+ }
186
+ continue
187
+ }
188
+ yield event
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Echoes a captured `encrypted_content` blob back on outgoing assistant
194
+ * messages so multi-turn conversations replay it verbatim, as Ark's
195
+ * thinking-summary docs require.
196
+ *
197
+ * The gate is `emitsEncryptedContent(this.model)` — the model being called
198
+ * now, not the provenance of the history. That guarantees a signature is
199
+ * never sent to a model that has no `encrypted_content` concept. It does
200
+ * NOT identify who produced the signature: `ModelMessage` carries no
201
+ * provider field, so a foreign signature (e.g. an Anthropic thinking
202
+ * signature in replayed cross-provider history) WILL be forwarded when the
203
+ * current model is a thinking-summary model. No shape guard is attempted —
204
+ * the blob is opaque and Ark is the only party that can validate it.
205
+ *
206
+ * Absence is never an error: a live probe confirmed Ark accepts a turn whose
207
+ * assistant message omits `encrypted_content`.
208
+ */
209
+ protected override convertMessage(
210
+ message: ModelMessage,
211
+ ): ChatCompletionMessageParam {
212
+ const converted = super.convertMessage(message)
213
+ if (converted.role !== 'assistant' || !emitsEncryptedContent(this.model)) {
214
+ return converted
215
+ }
216
+
217
+ const encryptedContent = lastThinkingSignature(message)
218
+ if (encryptedContent === undefined) return converted
219
+
220
+ // Intersection rather than a cast: `encrypted_content` is an Ark-only
221
+ // field with no slot on the OpenAI message param, and the intersection is
222
+ // still assignable to `ChatCompletionMessageParam`.
223
+ const withEncrypted: typeof converted & BytePlusEncryptedContentFields = {
224
+ ...converted,
225
+ encrypted_content: encryptedContent,
226
+ }
227
+ return withEncrypted
228
+ }
229
+
230
+ /**
231
+ * Adds the Ark-only content parts on top of the base's text/image handling:
232
+ * `video_url`, URL-addressed `input_audio`, and the extra `image_url`
233
+ * fields (`detail: 'xhigh'`, `image_pixel_limit`).
234
+ */
235
+ protected override convertContentPart(
236
+ part: ContentPart,
237
+ ): ChatCompletionContentPart | null {
238
+ if (part.type === 'image') {
239
+ const metadata = part.metadata as BytePlusImageMetadata | undefined
240
+ return asChatContentPart({
241
+ type: 'image_url',
242
+ image_url: {
243
+ url: toUrlOrDataUri(part.source),
244
+ detail: metadata?.detail ?? 'auto',
245
+ ...(metadata?.image_pixel_limit && {
246
+ image_pixel_limit: metadata.image_pixel_limit,
247
+ }),
248
+ },
249
+ })
250
+ }
251
+
252
+ if (part.type === 'video') {
253
+ const metadata = part.metadata as BytePlusVideoMetadata | undefined
254
+ return asChatContentPart({
255
+ type: 'video_url',
256
+ video_url: {
257
+ url: toUrlOrDataUri(part.source),
258
+ ...(metadata?.fps !== undefined && { fps: metadata.fps }),
259
+ },
260
+ })
261
+ }
262
+
263
+ if (part.type === 'audio') {
264
+ const metadata = part.metadata as BytePlusAudioMetadata | undefined
265
+ // Ark takes audio either by URL or as inline base64 with an explicit
266
+ // container format; unlike images there is no data-URI form.
267
+ if (part.source.type === 'url') {
268
+ return asChatContentPart({
269
+ type: 'input_audio',
270
+ input_audio: { url: part.source.value },
271
+ })
272
+ }
273
+ const format = metadata?.format ?? audioFormatFromMimeType(part.source)
274
+ if (format === undefined) {
275
+ throw new Error(
276
+ `Audio content part for ${this.name} has an unrecognised mimeType ` +
277
+ `(${part.source.mimeType || 'none'}). Set the container format ` +
278
+ `explicitly via the part's metadata.format, or supply a URL source.`,
279
+ )
280
+ }
281
+ return asChatContentPart({
282
+ type: 'input_audio',
283
+ input_audio: { data: stripDataUriPrefix(part.source.value), format },
284
+ })
285
+ }
286
+
287
+ return super.convertContentPart(part)
288
+ }
289
+
290
+ /**
291
+ * Only the models in {@link BYTEPLUS_STRUCTURED_OUTPUT_CHAT_MODELS} accept
292
+ * `response_format: json_schema`; the rest reject it with a 400.
293
+ *
294
+ * Returning `false` for a rejecting model does not make structured output
295
+ * work — Ark has no `json_object` fallback to downgrade to. What it buys is
296
+ * keeping `response_format` out of the request the engine would otherwise
297
+ * build: with the hook false the engine takes its separate finalization
298
+ * path, and the guard in {@link BytePlusTextAdapter.structuredOutput} /
299
+ * {@link BytePlusTextAdapter.structuredOutputStream} stops that *before*
300
+ * any HTTP call. So a `chat({ outputSchema })` on a rejecting model fails
301
+ * loudly, named, without a schema Ark would 400 on ever leaving the
302
+ * process — rather than 400-ing on every turn, or (worse) parsing prose as
303
+ * if it were JSON.
304
+ *
305
+ * Tools without a schema are unaffected: `tools` alone never involves
306
+ * `response_format`.
307
+ */
308
+ override supportsCombinedToolsAndSchema(): boolean {
309
+ return supportsStructuredOutput(this.model)
310
+ }
311
+
312
+ override async structuredOutput(
313
+ options: StructuredOutputOptions<TProviderOptions>,
314
+ ): Promise<StructuredOutputResult<unknown>> {
315
+ const unsupported = this.structuredOutputUnsupportedMessage()
316
+ if (unsupported) {
317
+ options.chatOptions.logger.errors(
318
+ `${this.name}.structuredOutput unsupported model`,
319
+ {
320
+ error: { message: unsupported },
321
+ source: `${this.name}.structuredOutput`,
322
+ },
323
+ )
324
+ throw new Error(unsupported)
325
+ }
326
+ return await super.structuredOutput(options)
327
+ }
328
+
329
+ override async *structuredOutputStream(
330
+ options: StructuredOutputOptions<TProviderOptions>,
331
+ ): AsyncIterable<StreamChunk> {
332
+ const unsupported = this.structuredOutputUnsupportedMessage()
333
+ if (unsupported) {
334
+ // Mirror the base's contract: failures inside structuredOutputStream
335
+ // surface as a RUN_STARTED → RUN_ERROR pair rather than a throw, so
336
+ // consumers keep a single error-handling path.
337
+ const timestamp = Date.now()
338
+ const runId = generateId(this.name)
339
+ yield {
340
+ type: EventType.RUN_STARTED,
341
+ runId,
342
+ threadId: options.chatOptions.threadId ?? generateId(this.name),
343
+ model: options.chatOptions.model,
344
+ timestamp,
345
+ parentRunId: options.chatOptions.parentRunId,
346
+ }
347
+ yield {
348
+ type: EventType.RUN_ERROR,
349
+ runId,
350
+ model: options.chatOptions.model,
351
+ timestamp,
352
+ message: unsupported,
353
+ code: 'unsupported-structured-output',
354
+ error: { message: unsupported, code: 'unsupported-structured-output' },
355
+ }
356
+ options.chatOptions.logger.errors(
357
+ `${this.name}.structuredOutputStream unsupported model`,
358
+ {
359
+ error: { message: unsupported },
360
+ source: `${this.name}.structuredOutputStream`,
361
+ },
362
+ )
363
+ return
364
+ }
365
+ yield* super.structuredOutputStream(options)
366
+ }
367
+
368
+ /**
369
+ * Explains why structured output is unavailable, or `undefined` when the
370
+ * model supports it. Ark rejects `response_format: json_object` on every
371
+ * model, so there is no JSON-mode fallback to degrade to — failing loud
372
+ * here beats a raw upstream 400.
373
+ */
374
+ private structuredOutputUnsupportedMessage(): string | undefined {
375
+ if (supportsStructuredOutput(this.model)) return undefined
376
+ return (
377
+ `BytePlus model ${this.model} does not support structured output — Ark ` +
378
+ `rejects both response_format json_schema and json_object on it. Use ` +
379
+ `one of: ${BYTEPLUS_STRUCTURED_OUTPUT_CHAT_MODELS.join(', ')}.`
380
+ )
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Passes Ark's chunks through untouched while recording the single
386
+ * `encrypted_content` blob a thinking-summary model emits.
387
+ */
388
+ async function* captureEncryptedContent(
389
+ stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>,
390
+ captured: { encryptedContent?: string },
391
+ ): AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk> {
392
+ for await (const chunk of stream) {
393
+ const delta = chunk.choices[0]?.delta as
394
+ | BytePlusStreamDeltaExtras
395
+ | undefined
396
+ const blob = delta?.encrypted_content
397
+ if (typeof blob === 'string' && blob.length > 0) {
398
+ captured.encryptedContent = blob
399
+ }
400
+ yield chunk
401
+ }
402
+ }
403
+
404
+ /**
405
+ * The blob to echo back for an assistant message: the last thinking step that
406
+ * carries a signature.
407
+ */
408
+ function lastThinkingSignature(message: ModelMessage): string | undefined {
409
+ const thinking = message.thinking
410
+ if (!thinking) return undefined
411
+ for (let i = thinking.length - 1; i >= 0; i--) {
412
+ const signature = thinking[i]?.signature
413
+ if (signature) return signature
414
+ }
415
+ return undefined
416
+ }
417
+
418
+ /**
419
+ * The one place the Ark content-part dialect meets the OpenAI SDK's request
420
+ * types.
421
+ *
422
+ * Ark's union is a superset of OpenAI's: `video_url` has no OpenAI arm at all,
423
+ * `input_audio` additionally accepts a `url`, and `image_url` carries
424
+ * `detail: 'xhigh'` and `image_pixel_limit`. `ChatCompletionContentPart` is a
425
+ * closed type alias in the SDK, so no interface augmentation can admit those
426
+ * arms and no narrowing can produce them — widening to `object` keeps this to
427
+ * a single downcast rather than spreading one through each branch of
428
+ * {@link BytePlusTextAdapter.convertContentPart}.
429
+ */
430
+ function asChatContentPart(
431
+ part: BytePlusChatContentPart,
432
+ ): ChatCompletionContentPart {
433
+ const arkPart: object = part
434
+ return arkPart as ChatCompletionContentPart
435
+ }
436
+
437
+ /**
438
+ * Renders a content source as the URL string Ark expects: URLs pass through,
439
+ * inline base64 becomes a `data:` URI.
440
+ */
441
+ function toUrlOrDataUri(source: ContentPartSource): string {
442
+ if (source.type !== 'data' || source.value.startsWith('data:')) {
443
+ return source.value
444
+ }
445
+ // A missing mimeType would interpolate as "data:undefined;base64,…" and be
446
+ // rejected, so fall back the same way the OpenAI base does.
447
+ return `data:${source.mimeType || 'application/octet-stream'};base64,${source.value}`
448
+ }
449
+
450
+ /**
451
+ * Strips a `data:` prefix so inline audio is sent as bare base64.
452
+ */
453
+ function stripDataUriPrefix(value: string): string {
454
+ const comma = value.startsWith('data:') ? value.indexOf(',') : -1
455
+ return comma === -1 ? value : value.slice(comma + 1)
456
+ }
457
+
458
+ const AUDIO_FORMAT_BY_MIME_SUBTYPE: Record<
459
+ string,
460
+ NonNullable<BytePlusInputAudioContentPart['input_audio']['format']>
461
+ > = {
462
+ mpeg: 'mp3',
463
+ mp3: 'mp3',
464
+ wav: 'wav',
465
+ 'x-wav': 'wav',
466
+ wave: 'wav',
467
+ ogg: 'ogg',
468
+ flac: 'flac',
469
+ 'x-flac': 'flac',
470
+ mp4: 'm4a',
471
+ m4a: 'm4a',
472
+ 'x-m4a': 'm4a',
473
+ aac: 'aac',
474
+ pcm: 'pcm',
475
+ l16: 'pcm',
476
+ }
477
+
478
+ /**
479
+ * Maps an audio part's mimeType to Ark's container format token.
480
+ */
481
+ function audioFormatFromMimeType(
482
+ source: ContentPartSource,
483
+ ):
484
+ | NonNullable<BytePlusInputAudioContentPart['input_audio']['format']>
485
+ | undefined {
486
+ const mimeType = source.mimeType
487
+ if (!mimeType) return undefined
488
+ const subtype = mimeType.split(';')[0]?.split('/')[1]?.toLowerCase()
489
+ return subtype ? AUDIO_FORMAT_BY_MIME_SUBTYPE[subtype] : undefined
490
+ }
491
+
492
+ /**
493
+ * Creates a BytePlus text adapter with an explicit API key.
494
+ *
495
+ * @param model - The chat model id (e.g., `'seed-2-0-lite-260428'`)
496
+ * @param apiKey - Your BytePlus Ark API key
497
+ * @param config - Optional additional configuration
498
+ *
499
+ * @example
500
+ * ```typescript
501
+ * const adapter = createBytePlusText('seed-2-0-lite-260428', 'ark-...')
502
+ * ```
503
+ */
504
+ export function createBytePlusText<
505
+ TModel extends (typeof BYTEPLUS_CHAT_MODELS)[number],
506
+ >(
507
+ model: TModel,
508
+ apiKey: string,
509
+ config?: Omit<BytePlusTextConfig, 'apiKey'>,
510
+ ): BytePlusTextAdapter<TModel> {
511
+ return new BytePlusTextAdapter({ apiKey, ...config }, model)
512
+ }
513
+
514
+ /**
515
+ * Creates a BytePlus text adapter with the API key read from `ARK_API_KEY`.
516
+ *
517
+ * @param model - The chat model id (e.g., `'seed-2-0-lite-260428'`)
518
+ * @param config - Optional configuration (excluding `apiKey`)
519
+ * @throws Error if `ARK_API_KEY` is not set
520
+ *
521
+ * @example
522
+ * ```typescript
523
+ * const adapter = byteplusText('seed-2-0-lite-260428')
524
+ *
525
+ * const stream = chat({
526
+ * adapter,
527
+ * messages: [{ role: 'user', content: 'Hello!' }],
528
+ * })
529
+ * ```
530
+ */
531
+ export function byteplusText<
532
+ TModel extends (typeof BYTEPLUS_CHAT_MODELS)[number],
533
+ >(
534
+ model: TModel,
535
+ config?: Omit<BytePlusTextConfig, 'apiKey'>,
536
+ ): BytePlusTextAdapter<TModel> {
537
+ const apiKey = getBytePlusArkApiKeyFromEnv()
538
+ return createBytePlusText(model, apiKey, config)
539
+ }