@tanstack/ai-byteplus 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/adapters/text.d.ts +3 -3
- package/dist/esm/adapters/text.js.map +1 -1
- package/dist/esm/byok.d.ts +3 -0
- package/dist/esm/byok.js +17 -0
- package/dist/esm/byok.js.map +1 -0
- package/package.json +8 -4
- package/src/adapters/text.ts +3 -3
- package/src/byok.ts +14 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { default as OpenAI } from 'openai';
|
|
2
2
|
import { OpenAIBaseChatCompletionsTextAdapter } from '@tanstack/openai-base';
|
|
3
3
|
import { StructuredOutputOptions, StructuredOutputResult } from '@tanstack/ai/adapters';
|
|
4
|
-
import { ContentPart, Modality, ModelMessage,
|
|
4
|
+
import { ContentPart, Modality, ModelMessage, AdapterYieldChunk, TextOptions } from '@tanstack/ai';
|
|
5
5
|
import { ChatCompletionContentPart, ChatCompletionMessageParam } from 'openai/resources/chat/completions/completions';
|
|
6
6
|
import { BYTEPLUS_CHAT_MODELS, BytePlusChatModelToolCapabilitiesByName, ResolveInputModalities, ResolveProviderOptions } from '../model-meta.js';
|
|
7
7
|
import { BytePlusMessageMetadataByModality } from '../message-types.js';
|
|
@@ -76,7 +76,7 @@ export declare class BytePlusTextAdapter<TModel extends (typeof BYTEPLUS_CHAT_MO
|
|
|
76
76
|
threadId: string;
|
|
77
77
|
messageId: string;
|
|
78
78
|
hasEmittedRunStarted: boolean;
|
|
79
|
-
}): AsyncIterable<
|
|
79
|
+
}): AsyncIterable<AdapterYieldChunk>;
|
|
80
80
|
/**
|
|
81
81
|
* Echoes a captured `encrypted_content` blob back on outgoing assistant
|
|
82
82
|
* messages so multi-turn conversations replay it verbatim, as Ark's
|
|
@@ -121,7 +121,7 @@ export declare class BytePlusTextAdapter<TModel extends (typeof BYTEPLUS_CHAT_MO
|
|
|
121
121
|
*/
|
|
122
122
|
supportsCombinedToolsAndSchema(): boolean;
|
|
123
123
|
structuredOutput(options: StructuredOutputOptions<TProviderOptions>): Promise<StructuredOutputResult<unknown>>;
|
|
124
|
-
structuredOutputStream(options: StructuredOutputOptions<TProviderOptions>): AsyncIterable<
|
|
124
|
+
structuredOutputStream(options: StructuredOutputOptions<TProviderOptions>): AsyncIterable<AdapterYieldChunk>;
|
|
125
125
|
/**
|
|
126
126
|
* Explains why structured output is unavailable, or `undefined` when the
|
|
127
127
|
* model supports it. Ark rejects `response_format: json_object` on every
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text.js","names":[],"sources":["../../../src/adapters/text.ts"],"sourcesContent":["import OpenAI from 'openai'\nimport { EventType } from '@tanstack/ai'\nimport { OpenAIBaseChatCompletionsTextAdapter } from '@tanstack/openai-base'\nimport { generateId } from '@tanstack/ai-utils'\nimport {\n BYTEPLUS_STRUCTURED_OUTPUT_CHAT_MODELS,\n emitsEncryptedContent,\n supportsStructuredOutput,\n} from '../model-meta'\nimport {\n getBytePlusArkApiKeyFromEnv,\n withBytePlusArkDefaults,\n} from '../utils/client'\nimport type {\n StructuredOutputOptions,\n StructuredOutputResult,\n} from '@tanstack/ai/adapters'\nimport type {\n ContentPart,\n ContentPartSource,\n Modality,\n ModelMessage,\n StreamChunk,\n TextOptions,\n} from '@tanstack/ai'\nimport type {\n ChatCompletionContentPart,\n ChatCompletionMessageParam,\n} from 'openai/resources/chat/completions/completions'\nimport type {\n BYTEPLUS_CHAT_MODELS,\n BytePlusChatModelToolCapabilitiesByName,\n ResolveInputModalities,\n ResolveProviderOptions,\n} from '../model-meta'\nimport type {\n BytePlusAudioMetadata,\n BytePlusChatContentPart,\n BytePlusEncryptedContentFields,\n BytePlusImageMetadata,\n BytePlusInputAudioContentPart,\n BytePlusMessageMetadataByModality,\n BytePlusStreamDeltaExtras,\n BytePlusVideoMetadata,\n} from '../message-types'\nimport type { BytePlusArkConfig } from '../utils/client'\n\ntype ResolveToolCapabilities<TModel extends string> =\n TModel extends keyof BytePlusChatModelToolCapabilitiesByName\n ? NonNullable<BytePlusChatModelToolCapabilitiesByName[TModel]>\n : readonly []\n\n/**\n * Configuration for the BytePlus text adapter.\n */\nexport interface BytePlusTextConfig extends BytePlusArkConfig {}\n\n/**\n * Re-export of the public provider options type.\n */\nexport type { BytePlusTextProviderOptions } from '../text/text-provider-options'\n\n/**\n * BytePlus ModelArk Text (Chat) Adapter\n *\n * Tree-shakeable adapter for the Seed / GLM / DeepSeek / gpt-oss chat models\n * on BytePlus ModelArk. Ark serves an OpenAI-compatible Chat Completions\n * endpoint, so this drives the OpenAI SDK against Ark's `baseURL` — the same\n * pattern as `ai-groq` and `ai-grok`.\n *\n * Three Ark behaviours are handled on top of the shared base:\n *\n * 1. **`reasoning_content` deltas** — Ark streams reasoning under\n * `delta.reasoning_content` rather than the OpenAI `reasoning` field.\n * 2. **`encrypted_content` round-trip** — thinking-summary models emit an\n * opaque signature over the reasoning trace. See\n * {@link BytePlusTextAdapter.processStreamChunks} and\n * {@link BytePlusTextAdapter.convertMessage}.\n * 3. **Per-model structured-output gating** — only 10 of the 18 shipped chat\n * models honour `response_format: json_schema` (glm-4-7 accepts it and then\n * ignores the schema), and Ark rejects `json_object` everywhere, so there\n * is no JSON-mode fallback.\n */\nexport class BytePlusTextAdapter<\n TModel extends (typeof BYTEPLUS_CHAT_MODELS)[number],\n // `Record<string, any>` (not `unknown`) mirrors the OpenAI/Groq/Grok text\n // adapters: the resolved provider options are an interface with no index\n // signature, assignable to `Record<string, any>` but not to\n // `Record<string, unknown>`. See issue #821.\n TProviderOptions extends Record<string, any> = ResolveProviderOptions<TModel>,\n TInputModalities extends ReadonlyArray<Modality> =\n ResolveInputModalities<TModel>,\n TToolCapabilities extends ReadonlyArray<string> =\n ResolveToolCapabilities<TModel>,\n> extends OpenAIBaseChatCompletionsTextAdapter<\n TModel,\n TProviderOptions,\n TInputModalities,\n BytePlusMessageMetadataByModality,\n TToolCapabilities\n> {\n override readonly kind = 'text' as const\n override readonly name = 'byteplus' as const\n\n constructor(config: BytePlusTextConfig, model: TModel) {\n super(model, 'byteplus', new OpenAI(withBytePlusArkDefaults(config)))\n }\n\n /**\n * Surfaces Ark's reasoning deltas. Thinking-enabled models stream the\n * reasoning trace as `delta.reasoning_content` (the OpenAI chunk shape has\n * no reasoning field); the base routes this hook through both `chatStream`\n * and `structuredOutputStream`.\n */\n protected override extractReasoning(\n chunk: OpenAI.Chat.Completions.ChatCompletionChunk,\n ): { text: string } | undefined {\n const delta = chunk.choices[0]?.delta as\n | BytePlusStreamDeltaExtras\n | undefined\n const raw = delta?.reasoning_content\n if (typeof raw === 'string' && raw.length > 0) {\n return { text: raw }\n }\n return undefined\n }\n\n /**\n * Captures Ark's `encrypted_content` and attaches it to the reasoning\n * step's `STEP_FINISHED` event as its `signature`.\n *\n * On a thinking-summary model Ark streams the whole blob as one dedicated\n * chunk (empty `content` and `reasoning_content`) sitting between the\n * reasoning deltas and the content deltas — so it is always captured before\n * the base closes the reasoning lifecycle at the first content delta.\n *\n * `signature` is the framework's existing provider-signature seam: the chat\n * engine stores it on the `ThinkingPart`, which\n * `buildAssistantMessages` carries into `ModelMessage.thinking[].signature`,\n * which {@link BytePlusTextAdapter.convertMessage} echoes back to Ark on the\n * next turn. No base-class change is needed — this is the same round-trip\n * Anthropic's thinking signatures use.\n *\n * Only `chatStream` is covered: `structuredOutputStream` drives the SDK\n * directly in the base with no per-chunk seam, so a structured-output turn\n * does not capture the blob. Ark accepts a following turn without it, so the\n * consequence is a lost reasoning-cache hit, not a failed request.\n */\n protected override async *processStreamChunks(\n stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>,\n options: TextOptions,\n aguiState: {\n runId: string\n threadId: string\n messageId: string\n hasEmittedRunStarted: boolean\n },\n ): AsyncIterable<StreamChunk> {\n const captured: { encryptedContent?: string } = {}\n\n for await (const event of super.processStreamChunks(\n captureEncryptedContent(stream, captured),\n options,\n aguiState,\n )) {\n if (\n event.type === EventType.STEP_FINISHED &&\n captured.encryptedContent !== undefined &&\n event.signature === undefined\n ) {\n // `delta` is stamped alongside the signature because the two consumers\n // read this event differently. `chat()`'s server agent loop accumulates\n // thinking ONLY from `STEP_FINISHED.delta` and then drops the whole\n // step — signature included — when the accumulated content is empty\n // (`finalizeCurrentThinkingStep`); the OpenAI base emits `content` but\n // never `delta`, so without this the blob never reaches the\n // continuation message. The client `StreamProcessor` can't double-count\n // it: it short-circuits STEP_FINISHED content once\n // `hasSeenReasoningEvents` is set, which the REASONING_MESSAGE_CONTENT\n // events preceding every STEP_FINISHED here always set.\n yield {\n ...event,\n signature: captured.encryptedContent,\n delta: event.delta ?? event.content ?? '',\n }\n continue\n }\n yield event\n }\n }\n\n /**\n * Echoes a captured `encrypted_content` blob back on outgoing assistant\n * messages so multi-turn conversations replay it verbatim, as Ark's\n * thinking-summary docs require.\n *\n * The gate is `emitsEncryptedContent(this.model)` — the model being called\n * now, not the provenance of the history. That guarantees a signature is\n * never sent to a model that has no `encrypted_content` concept. It does\n * NOT identify who produced the signature: `ModelMessage` carries no\n * provider field, so a foreign signature (e.g. an Anthropic thinking\n * signature in replayed cross-provider history) WILL be forwarded when the\n * current model is a thinking-summary model. No shape guard is attempted —\n * the blob is opaque and Ark is the only party that can validate it.\n *\n * Absence is never an error: a live probe confirmed Ark accepts a turn whose\n * assistant message omits `encrypted_content`.\n */\n protected override convertMessage(\n message: ModelMessage,\n ): ChatCompletionMessageParam {\n const converted = super.convertMessage(message)\n if (converted.role !== 'assistant' || !emitsEncryptedContent(this.model)) {\n return converted\n }\n\n const encryptedContent = lastThinkingSignature(message)\n if (encryptedContent === undefined) return converted\n\n // Intersection rather than a cast: `encrypted_content` is an Ark-only\n // field with no slot on the OpenAI message param, and the intersection is\n // still assignable to `ChatCompletionMessageParam`.\n const withEncrypted: typeof converted & BytePlusEncryptedContentFields = {\n ...converted,\n encrypted_content: encryptedContent,\n }\n return withEncrypted\n }\n\n /**\n * Adds the Ark-only content parts on top of the base's text/image handling:\n * `video_url`, URL-addressed `input_audio`, and the extra `image_url`\n * fields (`detail: 'xhigh'`, `image_pixel_limit`).\n */\n protected override convertContentPart(\n part: ContentPart,\n ): ChatCompletionContentPart | null {\n if (part.type === 'image') {\n const metadata = part.metadata as BytePlusImageMetadata | undefined\n return asChatContentPart({\n type: 'image_url',\n image_url: {\n url: toUrlOrDataUri(part.source),\n detail: metadata?.detail ?? 'auto',\n ...(metadata?.image_pixel_limit && {\n image_pixel_limit: metadata.image_pixel_limit,\n }),\n },\n })\n }\n\n if (part.type === 'video') {\n const metadata = part.metadata as BytePlusVideoMetadata | undefined\n return asChatContentPart({\n type: 'video_url',\n video_url: {\n url: toUrlOrDataUri(part.source),\n ...(metadata?.fps !== undefined && { fps: metadata.fps }),\n },\n })\n }\n\n if (part.type === 'audio') {\n const metadata = part.metadata as BytePlusAudioMetadata | undefined\n // Ark takes audio either by URL or as inline base64 with an explicit\n // container format; unlike images there is no data-URI form.\n if (part.source.type === 'url') {\n return asChatContentPart({\n type: 'input_audio',\n input_audio: { url: part.source.value },\n })\n }\n const format = metadata?.format ?? audioFormatFromMimeType(part.source)\n if (format === undefined) {\n throw new Error(\n `Audio content part for ${this.name} has an unrecognised mimeType ` +\n `(${part.source.mimeType || 'none'}). Set the container format ` +\n `explicitly via the part's metadata.format, or supply a URL source.`,\n )\n }\n return asChatContentPart({\n type: 'input_audio',\n input_audio: { data: stripDataUriPrefix(part.source.value), format },\n })\n }\n\n return super.convertContentPart(part)\n }\n\n /**\n * Only the models in {@link BYTEPLUS_STRUCTURED_OUTPUT_CHAT_MODELS} accept\n * `response_format: json_schema`; the rest reject it with a 400.\n *\n * Returning `false` for a rejecting model does not make structured output\n * work — Ark has no `json_object` fallback to downgrade to. What it buys is\n * keeping `response_format` out of the request the engine would otherwise\n * build: with the hook false the engine takes its separate finalization\n * path, and the guard in {@link BytePlusTextAdapter.structuredOutput} /\n * {@link BytePlusTextAdapter.structuredOutputStream} stops that *before*\n * any HTTP call. So a `chat({ outputSchema })` on a rejecting model fails\n * loudly, named, without a schema Ark would 400 on ever leaving the\n * process — rather than 400-ing on every turn, or (worse) parsing prose as\n * if it were JSON.\n *\n * Tools without a schema are unaffected: `tools` alone never involves\n * `response_format`.\n */\n override supportsCombinedToolsAndSchema(): boolean {\n return supportsStructuredOutput(this.model)\n }\n\n override async structuredOutput(\n options: StructuredOutputOptions<TProviderOptions>,\n ): Promise<StructuredOutputResult<unknown>> {\n const unsupported = this.structuredOutputUnsupportedMessage()\n if (unsupported) {\n options.chatOptions.logger.errors(\n `${this.name}.structuredOutput unsupported model`,\n {\n error: { message: unsupported },\n source: `${this.name}.structuredOutput`,\n },\n )\n throw new Error(unsupported)\n }\n return await super.structuredOutput(options)\n }\n\n override async *structuredOutputStream(\n options: StructuredOutputOptions<TProviderOptions>,\n ): AsyncIterable<StreamChunk> {\n const unsupported = this.structuredOutputUnsupportedMessage()\n if (unsupported) {\n // Mirror the base's contract: failures inside structuredOutputStream\n // surface as a RUN_STARTED → RUN_ERROR pair rather than a throw, so\n // consumers keep a single error-handling path.\n const runId = generateId(this.name)\n yield {\n type: EventType.RUN_STARTED,\n runId,\n threadId: options.chatOptions.threadId ?? generateId(this.name),\n model: options.chatOptions.model,\n timestamp: Date.now(),\n parentRunId: options.chatOptions.parentRunId,\n }\n yield {\n type: EventType.RUN_ERROR,\n runId,\n model: options.chatOptions.model,\n timestamp: Date.now(),\n message: unsupported,\n code: 'unsupported-structured-output',\n error: { message: unsupported, code: 'unsupported-structured-output' },\n }\n options.chatOptions.logger.errors(\n `${this.name}.structuredOutputStream unsupported model`,\n {\n error: { message: unsupported },\n source: `${this.name}.structuredOutputStream`,\n },\n )\n return\n }\n yield* super.structuredOutputStream(options)\n }\n\n /**\n * Explains why structured output is unavailable, or `undefined` when the\n * model supports it. Ark rejects `response_format: json_object` on every\n * model, so there is no JSON-mode fallback to degrade to — failing loud\n * here beats a raw upstream 400.\n */\n private structuredOutputUnsupportedMessage(): string | undefined {\n if (supportsStructuredOutput(this.model)) return undefined\n return (\n `BytePlus model ${this.model} does not support structured output — Ark ` +\n `rejects both response_format json_schema and json_object on it. Use ` +\n `one of: ${BYTEPLUS_STRUCTURED_OUTPUT_CHAT_MODELS.join(', ')}.`\n )\n }\n}\n\n/**\n * Passes Ark's chunks through untouched while recording the single\n * `encrypted_content` blob a thinking-summary model emits.\n */\nasync function* captureEncryptedContent(\n stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>,\n captured: { encryptedContent?: string },\n): AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk> {\n for await (const chunk of stream) {\n const delta = chunk.choices[0]?.delta as\n | BytePlusStreamDeltaExtras\n | undefined\n const blob = delta?.encrypted_content\n if (typeof blob === 'string' && blob.length > 0) {\n captured.encryptedContent = blob\n }\n yield chunk\n }\n}\n\n/**\n * The blob to echo back for an assistant message: the last thinking step that\n * carries a signature.\n */\nfunction lastThinkingSignature(message: ModelMessage): string | undefined {\n const thinking = message.thinking\n if (!thinking) return undefined\n for (let i = thinking.length - 1; i >= 0; i--) {\n const signature = thinking[i]?.signature\n if (signature) return signature\n }\n return undefined\n}\n\n/**\n * The one place the Ark content-part dialect meets the OpenAI SDK's request\n * types.\n *\n * Ark's union is a superset of OpenAI's: `video_url` has no OpenAI arm at all,\n * `input_audio` additionally accepts a `url`, and `image_url` carries\n * `detail: 'xhigh'` and `image_pixel_limit`. `ChatCompletionContentPart` is a\n * closed type alias in the SDK, so no interface augmentation can admit those\n * arms and no narrowing can produce them — widening to `object` keeps this to\n * a single downcast rather than spreading one through each branch of\n * {@link BytePlusTextAdapter.convertContentPart}.\n */\nfunction asChatContentPart(\n part: BytePlusChatContentPart,\n): ChatCompletionContentPart {\n const arkPart: object = part\n return arkPart as ChatCompletionContentPart\n}\n\n/**\n * Renders a content source as the URL string Ark expects: URLs pass through,\n * inline base64 becomes a `data:` URI.\n */\nfunction toUrlOrDataUri(source: ContentPartSource): string {\n if (source.type !== 'data' || source.value.startsWith('data:')) {\n return source.value\n }\n // A missing mimeType would interpolate as \"data:undefined;base64,…\" and be\n // rejected, so fall back the same way the OpenAI base does.\n return `data:${source.mimeType || 'application/octet-stream'};base64,${source.value}`\n}\n\n/**\n * Strips a `data:` prefix so inline audio is sent as bare base64.\n */\nfunction stripDataUriPrefix(value: string): string {\n const comma = value.startsWith('data:') ? value.indexOf(',') : -1\n return comma === -1 ? value : value.slice(comma + 1)\n}\n\nconst AUDIO_FORMAT_BY_MIME_SUBTYPE: Record<\n string,\n NonNullable<BytePlusInputAudioContentPart['input_audio']['format']>\n> = {\n mpeg: 'mp3',\n mp3: 'mp3',\n wav: 'wav',\n 'x-wav': 'wav',\n wave: 'wav',\n ogg: 'ogg',\n flac: 'flac',\n 'x-flac': 'flac',\n mp4: 'm4a',\n m4a: 'm4a',\n 'x-m4a': 'm4a',\n aac: 'aac',\n pcm: 'pcm',\n l16: 'pcm',\n}\n\n/**\n * Maps an audio part's mimeType to Ark's container format token.\n */\nfunction audioFormatFromMimeType(\n source: ContentPartSource,\n):\n | NonNullable<BytePlusInputAudioContentPart['input_audio']['format']>\n | undefined {\n const mimeType = source.mimeType\n if (!mimeType) return undefined\n const subtype = mimeType.split(';')[0]?.split('/')[1]?.toLowerCase()\n return subtype ? AUDIO_FORMAT_BY_MIME_SUBTYPE[subtype] : undefined\n}\n\n/**\n * Creates a BytePlus text adapter with an explicit API key.\n *\n * @param model - The chat model id (e.g., `'seed-2-0-lite-260428'`)\n * @param apiKey - Your BytePlus Ark API key\n * @param config - Optional additional configuration\n *\n * @example\n * ```typescript\n * const adapter = createBytePlusText('seed-2-0-lite-260428', 'ark-...')\n * ```\n */\nexport function createBytePlusText<\n TModel extends (typeof BYTEPLUS_CHAT_MODELS)[number],\n>(\n model: TModel,\n apiKey: string,\n config?: Omit<BytePlusTextConfig, 'apiKey'>,\n): BytePlusTextAdapter<TModel> {\n return new BytePlusTextAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a BytePlus text adapter with the API key read from `ARK_API_KEY`.\n *\n * @param model - The chat model id (e.g., `'seed-2-0-lite-260428'`)\n * @param config - Optional configuration (excluding `apiKey`)\n * @throws Error if `ARK_API_KEY` is not set\n *\n * @example\n * ```typescript\n * const adapter = byteplusText('seed-2-0-lite-260428')\n *\n * const stream = chat({\n * adapter,\n * messages: [{ role: 'user', content: 'Hello!' }],\n * })\n * ```\n */\nexport function byteplusText<\n TModel extends (typeof BYTEPLUS_CHAT_MODELS)[number],\n>(\n model: TModel,\n config?: Omit<BytePlusTextConfig, 'apiKey'>,\n): BytePlusTextAdapter<TModel> {\n const apiKey = getBytePlusArkApiKeyFromEnv()\n return createBytePlusText(model, apiKey, config)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"text.js","names":[],"sources":["../../../src/adapters/text.ts"],"sourcesContent":["import OpenAI from 'openai'\nimport { EventType } from '@tanstack/ai'\nimport { OpenAIBaseChatCompletionsTextAdapter } from '@tanstack/openai-base'\nimport { generateId } from '@tanstack/ai-utils'\nimport {\n BYTEPLUS_STRUCTURED_OUTPUT_CHAT_MODELS,\n emitsEncryptedContent,\n supportsStructuredOutput,\n} from '../model-meta'\nimport {\n getBytePlusArkApiKeyFromEnv,\n withBytePlusArkDefaults,\n} from '../utils/client'\nimport type {\n StructuredOutputOptions,\n StructuredOutputResult,\n} from '@tanstack/ai/adapters'\nimport type {\n ContentPart,\n ContentPartSource,\n Modality,\n ModelMessage,\n AdapterYieldChunk,\n TextOptions,\n} from '@tanstack/ai'\nimport type {\n ChatCompletionContentPart,\n ChatCompletionMessageParam,\n} from 'openai/resources/chat/completions/completions'\nimport type {\n BYTEPLUS_CHAT_MODELS,\n BytePlusChatModelToolCapabilitiesByName,\n ResolveInputModalities,\n ResolveProviderOptions,\n} from '../model-meta'\nimport type {\n BytePlusAudioMetadata,\n BytePlusChatContentPart,\n BytePlusEncryptedContentFields,\n BytePlusImageMetadata,\n BytePlusInputAudioContentPart,\n BytePlusMessageMetadataByModality,\n BytePlusStreamDeltaExtras,\n BytePlusVideoMetadata,\n} from '../message-types'\nimport type { BytePlusArkConfig } from '../utils/client'\n\ntype ResolveToolCapabilities<TModel extends string> =\n TModel extends keyof BytePlusChatModelToolCapabilitiesByName\n ? NonNullable<BytePlusChatModelToolCapabilitiesByName[TModel]>\n : readonly []\n\n/**\n * Configuration for the BytePlus text adapter.\n */\nexport interface BytePlusTextConfig extends BytePlusArkConfig {}\n\n/**\n * Re-export of the public provider options type.\n */\nexport type { BytePlusTextProviderOptions } from '../text/text-provider-options'\n\n/**\n * BytePlus ModelArk Text (Chat) Adapter\n *\n * Tree-shakeable adapter for the Seed / GLM / DeepSeek / gpt-oss chat models\n * on BytePlus ModelArk. Ark serves an OpenAI-compatible Chat Completions\n * endpoint, so this drives the OpenAI SDK against Ark's `baseURL` — the same\n * pattern as `ai-groq` and `ai-grok`.\n *\n * Three Ark behaviours are handled on top of the shared base:\n *\n * 1. **`reasoning_content` deltas** — Ark streams reasoning under\n * `delta.reasoning_content` rather than the OpenAI `reasoning` field.\n * 2. **`encrypted_content` round-trip** — thinking-summary models emit an\n * opaque signature over the reasoning trace. See\n * {@link BytePlusTextAdapter.processStreamChunks} and\n * {@link BytePlusTextAdapter.convertMessage}.\n * 3. **Per-model structured-output gating** — only 10 of the 18 shipped chat\n * models honour `response_format: json_schema` (glm-4-7 accepts it and then\n * ignores the schema), and Ark rejects `json_object` everywhere, so there\n * is no JSON-mode fallback.\n */\nexport class BytePlusTextAdapter<\n TModel extends (typeof BYTEPLUS_CHAT_MODELS)[number],\n // `Record<string, any>` (not `unknown`) mirrors the OpenAI/Groq/Grok text\n // adapters: the resolved provider options are an interface with no index\n // signature, assignable to `Record<string, any>` but not to\n // `Record<string, unknown>`. See issue #821.\n TProviderOptions extends Record<string, any> = ResolveProviderOptions<TModel>,\n TInputModalities extends ReadonlyArray<Modality> =\n ResolveInputModalities<TModel>,\n TToolCapabilities extends ReadonlyArray<string> =\n ResolveToolCapabilities<TModel>,\n> extends OpenAIBaseChatCompletionsTextAdapter<\n TModel,\n TProviderOptions,\n TInputModalities,\n BytePlusMessageMetadataByModality,\n TToolCapabilities\n> {\n override readonly kind = 'text' as const\n override readonly name = 'byteplus' as const\n\n constructor(config: BytePlusTextConfig, model: TModel) {\n super(model, 'byteplus', new OpenAI(withBytePlusArkDefaults(config)))\n }\n\n /**\n * Surfaces Ark's reasoning deltas. Thinking-enabled models stream the\n * reasoning trace as `delta.reasoning_content` (the OpenAI chunk shape has\n * no reasoning field); the base routes this hook through both `chatStream`\n * and `structuredOutputStream`.\n */\n protected override extractReasoning(\n chunk: OpenAI.Chat.Completions.ChatCompletionChunk,\n ): { text: string } | undefined {\n const delta = chunk.choices[0]?.delta as\n | BytePlusStreamDeltaExtras\n | undefined\n const raw = delta?.reasoning_content\n if (typeof raw === 'string' && raw.length > 0) {\n return { text: raw }\n }\n return undefined\n }\n\n /**\n * Captures Ark's `encrypted_content` and attaches it to the reasoning\n * step's `STEP_FINISHED` event as its `signature`.\n *\n * On a thinking-summary model Ark streams the whole blob as one dedicated\n * chunk (empty `content` and `reasoning_content`) sitting between the\n * reasoning deltas and the content deltas — so it is always captured before\n * the base closes the reasoning lifecycle at the first content delta.\n *\n * `signature` is the framework's existing provider-signature seam: the chat\n * engine stores it on the `ThinkingPart`, which\n * `buildAssistantMessages` carries into `ModelMessage.thinking[].signature`,\n * which {@link BytePlusTextAdapter.convertMessage} echoes back to Ark on the\n * next turn. No base-class change is needed — this is the same round-trip\n * Anthropic's thinking signatures use.\n *\n * Only `chatStream` is covered: `structuredOutputStream` drives the SDK\n * directly in the base with no per-chunk seam, so a structured-output turn\n * does not capture the blob. Ark accepts a following turn without it, so the\n * consequence is a lost reasoning-cache hit, not a failed request.\n */\n protected override async *processStreamChunks(\n stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>,\n options: TextOptions,\n aguiState: {\n runId: string\n threadId: string\n messageId: string\n hasEmittedRunStarted: boolean\n },\n ): AsyncIterable<AdapterYieldChunk> {\n const captured: { encryptedContent?: string } = {}\n\n for await (const event of super.processStreamChunks(\n captureEncryptedContent(stream, captured),\n options,\n aguiState,\n )) {\n if (\n event.type === EventType.STEP_FINISHED &&\n captured.encryptedContent !== undefined &&\n event.signature === undefined\n ) {\n // `delta` is stamped alongside the signature because the two consumers\n // read this event differently. `chat()`'s server agent loop accumulates\n // thinking ONLY from `STEP_FINISHED.delta` and then drops the whole\n // step — signature included — when the accumulated content is empty\n // (`finalizeCurrentThinkingStep`); the OpenAI base emits `content` but\n // never `delta`, so without this the blob never reaches the\n // continuation message. The client `StreamProcessor` can't double-count\n // it: it short-circuits STEP_FINISHED content once\n // `hasSeenReasoningEvents` is set, which the REASONING_MESSAGE_CONTENT\n // events preceding every STEP_FINISHED here always set.\n yield {\n ...event,\n signature: captured.encryptedContent,\n delta: event.delta ?? event.content ?? '',\n }\n continue\n }\n yield event\n }\n }\n\n /**\n * Echoes a captured `encrypted_content` blob back on outgoing assistant\n * messages so multi-turn conversations replay it verbatim, as Ark's\n * thinking-summary docs require.\n *\n * The gate is `emitsEncryptedContent(this.model)` — the model being called\n * now, not the provenance of the history. That guarantees a signature is\n * never sent to a model that has no `encrypted_content` concept. It does\n * NOT identify who produced the signature: `ModelMessage` carries no\n * provider field, so a foreign signature (e.g. an Anthropic thinking\n * signature in replayed cross-provider history) WILL be forwarded when the\n * current model is a thinking-summary model. No shape guard is attempted —\n * the blob is opaque and Ark is the only party that can validate it.\n *\n * Absence is never an error: a live probe confirmed Ark accepts a turn whose\n * assistant message omits `encrypted_content`.\n */\n protected override convertMessage(\n message: ModelMessage,\n ): ChatCompletionMessageParam {\n const converted = super.convertMessage(message)\n if (converted.role !== 'assistant' || !emitsEncryptedContent(this.model)) {\n return converted\n }\n\n const encryptedContent = lastThinkingSignature(message)\n if (encryptedContent === undefined) return converted\n\n // Intersection rather than a cast: `encrypted_content` is an Ark-only\n // field with no slot on the OpenAI message param, and the intersection is\n // still assignable to `ChatCompletionMessageParam`.\n const withEncrypted: typeof converted & BytePlusEncryptedContentFields = {\n ...converted,\n encrypted_content: encryptedContent,\n }\n return withEncrypted\n }\n\n /**\n * Adds the Ark-only content parts on top of the base's text/image handling:\n * `video_url`, URL-addressed `input_audio`, and the extra `image_url`\n * fields (`detail: 'xhigh'`, `image_pixel_limit`).\n */\n protected override convertContentPart(\n part: ContentPart,\n ): ChatCompletionContentPart | null {\n if (part.type === 'image') {\n const metadata = part.metadata as BytePlusImageMetadata | undefined\n return asChatContentPart({\n type: 'image_url',\n image_url: {\n url: toUrlOrDataUri(part.source),\n detail: metadata?.detail ?? 'auto',\n ...(metadata?.image_pixel_limit && {\n image_pixel_limit: metadata.image_pixel_limit,\n }),\n },\n })\n }\n\n if (part.type === 'video') {\n const metadata = part.metadata as BytePlusVideoMetadata | undefined\n return asChatContentPart({\n type: 'video_url',\n video_url: {\n url: toUrlOrDataUri(part.source),\n ...(metadata?.fps !== undefined && { fps: metadata.fps }),\n },\n })\n }\n\n if (part.type === 'audio') {\n const metadata = part.metadata as BytePlusAudioMetadata | undefined\n // Ark takes audio either by URL or as inline base64 with an explicit\n // container format; unlike images there is no data-URI form.\n if (part.source.type === 'url') {\n return asChatContentPart({\n type: 'input_audio',\n input_audio: { url: part.source.value },\n })\n }\n const format = metadata?.format ?? audioFormatFromMimeType(part.source)\n if (format === undefined) {\n throw new Error(\n `Audio content part for ${this.name} has an unrecognised mimeType ` +\n `(${part.source.mimeType || 'none'}). Set the container format ` +\n `explicitly via the part's metadata.format, or supply a URL source.`,\n )\n }\n return asChatContentPart({\n type: 'input_audio',\n input_audio: { data: stripDataUriPrefix(part.source.value), format },\n })\n }\n\n return super.convertContentPart(part)\n }\n\n /**\n * Only the models in {@link BYTEPLUS_STRUCTURED_OUTPUT_CHAT_MODELS} accept\n * `response_format: json_schema`; the rest reject it with a 400.\n *\n * Returning `false` for a rejecting model does not make structured output\n * work — Ark has no `json_object` fallback to downgrade to. What it buys is\n * keeping `response_format` out of the request the engine would otherwise\n * build: with the hook false the engine takes its separate finalization\n * path, and the guard in {@link BytePlusTextAdapter.structuredOutput} /\n * {@link BytePlusTextAdapter.structuredOutputStream} stops that *before*\n * any HTTP call. So a `chat({ outputSchema })` on a rejecting model fails\n * loudly, named, without a schema Ark would 400 on ever leaving the\n * process — rather than 400-ing on every turn, or (worse) parsing prose as\n * if it were JSON.\n *\n * Tools without a schema are unaffected: `tools` alone never involves\n * `response_format`.\n */\n override supportsCombinedToolsAndSchema(): boolean {\n return supportsStructuredOutput(this.model)\n }\n\n override async structuredOutput(\n options: StructuredOutputOptions<TProviderOptions>,\n ): Promise<StructuredOutputResult<unknown>> {\n const unsupported = this.structuredOutputUnsupportedMessage()\n if (unsupported) {\n options.chatOptions.logger.errors(\n `${this.name}.structuredOutput unsupported model`,\n {\n error: { message: unsupported },\n source: `${this.name}.structuredOutput`,\n },\n )\n throw new Error(unsupported)\n }\n return await super.structuredOutput(options)\n }\n\n override async *structuredOutputStream(\n options: StructuredOutputOptions<TProviderOptions>,\n ): AsyncIterable<AdapterYieldChunk> {\n const unsupported = this.structuredOutputUnsupportedMessage()\n if (unsupported) {\n // Mirror the base's contract: failures inside structuredOutputStream\n // surface as a RUN_STARTED → RUN_ERROR pair rather than a throw, so\n // consumers keep a single error-handling path.\n const runId = generateId(this.name)\n yield {\n type: EventType.RUN_STARTED,\n runId,\n threadId: options.chatOptions.threadId ?? generateId(this.name),\n model: options.chatOptions.model,\n timestamp: Date.now(),\n parentRunId: options.chatOptions.parentRunId,\n }\n yield {\n type: EventType.RUN_ERROR,\n runId,\n model: options.chatOptions.model,\n timestamp: Date.now(),\n message: unsupported,\n code: 'unsupported-structured-output',\n error: { message: unsupported, code: 'unsupported-structured-output' },\n }\n options.chatOptions.logger.errors(\n `${this.name}.structuredOutputStream unsupported model`,\n {\n error: { message: unsupported },\n source: `${this.name}.structuredOutputStream`,\n },\n )\n return\n }\n yield* super.structuredOutputStream(options)\n }\n\n /**\n * Explains why structured output is unavailable, or `undefined` when the\n * model supports it. Ark rejects `response_format: json_object` on every\n * model, so there is no JSON-mode fallback to degrade to — failing loud\n * here beats a raw upstream 400.\n */\n private structuredOutputUnsupportedMessage(): string | undefined {\n if (supportsStructuredOutput(this.model)) return undefined\n return (\n `BytePlus model ${this.model} does not support structured output — Ark ` +\n `rejects both response_format json_schema and json_object on it. Use ` +\n `one of: ${BYTEPLUS_STRUCTURED_OUTPUT_CHAT_MODELS.join(', ')}.`\n )\n }\n}\n\n/**\n * Passes Ark's chunks through untouched while recording the single\n * `encrypted_content` blob a thinking-summary model emits.\n */\nasync function* captureEncryptedContent(\n stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>,\n captured: { encryptedContent?: string },\n): AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk> {\n for await (const chunk of stream) {\n const delta = chunk.choices[0]?.delta as\n | BytePlusStreamDeltaExtras\n | undefined\n const blob = delta?.encrypted_content\n if (typeof blob === 'string' && blob.length > 0) {\n captured.encryptedContent = blob\n }\n yield chunk\n }\n}\n\n/**\n * The blob to echo back for an assistant message: the last thinking step that\n * carries a signature.\n */\nfunction lastThinkingSignature(message: ModelMessage): string | undefined {\n const thinking = message.thinking\n if (!thinking) return undefined\n for (let i = thinking.length - 1; i >= 0; i--) {\n const signature = thinking[i]?.signature\n if (signature) return signature\n }\n return undefined\n}\n\n/**\n * The one place the Ark content-part dialect meets the OpenAI SDK's request\n * types.\n *\n * Ark's union is a superset of OpenAI's: `video_url` has no OpenAI arm at all,\n * `input_audio` additionally accepts a `url`, and `image_url` carries\n * `detail: 'xhigh'` and `image_pixel_limit`. `ChatCompletionContentPart` is a\n * closed type alias in the SDK, so no interface augmentation can admit those\n * arms and no narrowing can produce them — widening to `object` keeps this to\n * a single downcast rather than spreading one through each branch of\n * {@link BytePlusTextAdapter.convertContentPart}.\n */\nfunction asChatContentPart(\n part: BytePlusChatContentPart,\n): ChatCompletionContentPart {\n const arkPart: object = part\n return arkPart as ChatCompletionContentPart\n}\n\n/**\n * Renders a content source as the URL string Ark expects: URLs pass through,\n * inline base64 becomes a `data:` URI.\n */\nfunction toUrlOrDataUri(source: ContentPartSource): string {\n if (source.type !== 'data' || source.value.startsWith('data:')) {\n return source.value\n }\n // A missing mimeType would interpolate as \"data:undefined;base64,…\" and be\n // rejected, so fall back the same way the OpenAI base does.\n return `data:${source.mimeType || 'application/octet-stream'};base64,${source.value}`\n}\n\n/**\n * Strips a `data:` prefix so inline audio is sent as bare base64.\n */\nfunction stripDataUriPrefix(value: string): string {\n const comma = value.startsWith('data:') ? value.indexOf(',') : -1\n return comma === -1 ? value : value.slice(comma + 1)\n}\n\nconst AUDIO_FORMAT_BY_MIME_SUBTYPE: Record<\n string,\n NonNullable<BytePlusInputAudioContentPart['input_audio']['format']>\n> = {\n mpeg: 'mp3',\n mp3: 'mp3',\n wav: 'wav',\n 'x-wav': 'wav',\n wave: 'wav',\n ogg: 'ogg',\n flac: 'flac',\n 'x-flac': 'flac',\n mp4: 'm4a',\n m4a: 'm4a',\n 'x-m4a': 'm4a',\n aac: 'aac',\n pcm: 'pcm',\n l16: 'pcm',\n}\n\n/**\n * Maps an audio part's mimeType to Ark's container format token.\n */\nfunction audioFormatFromMimeType(\n source: ContentPartSource,\n):\n | NonNullable<BytePlusInputAudioContentPart['input_audio']['format']>\n | undefined {\n const mimeType = source.mimeType\n if (!mimeType) return undefined\n const subtype = mimeType.split(';')[0]?.split('/')[1]?.toLowerCase()\n return subtype ? AUDIO_FORMAT_BY_MIME_SUBTYPE[subtype] : undefined\n}\n\n/**\n * Creates a BytePlus text adapter with an explicit API key.\n *\n * @param model - The chat model id (e.g., `'seed-2-0-lite-260428'`)\n * @param apiKey - Your BytePlus Ark API key\n * @param config - Optional additional configuration\n *\n * @example\n * ```typescript\n * const adapter = createBytePlusText('seed-2-0-lite-260428', 'ark-...')\n * ```\n */\nexport function createBytePlusText<\n TModel extends (typeof BYTEPLUS_CHAT_MODELS)[number],\n>(\n model: TModel,\n apiKey: string,\n config?: Omit<BytePlusTextConfig, 'apiKey'>,\n): BytePlusTextAdapter<TModel> {\n return new BytePlusTextAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a BytePlus text adapter with the API key read from `ARK_API_KEY`.\n *\n * @param model - The chat model id (e.g., `'seed-2-0-lite-260428'`)\n * @param config - Optional configuration (excluding `apiKey`)\n * @throws Error if `ARK_API_KEY` is not set\n *\n * @example\n * ```typescript\n * const adapter = byteplusText('seed-2-0-lite-260428')\n *\n * const stream = chat({\n * adapter,\n * messages: [{ role: 'user', content: 'Hello!' }],\n * })\n * ```\n */\nexport function byteplusText<\n TModel extends (typeof BYTEPLUS_CHAT_MODELS)[number],\n>(\n model: TModel,\n config?: Omit<BytePlusTextConfig, 'apiKey'>,\n): BytePlusTextAdapter<TModel> {\n const apiKey = getBytePlusArkApiKeyFromEnv()\n return createBytePlusText(model, apiKey, config)\n}\n"],"mappings":"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"}
|
package/dist/esm/byok.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineByokProvider } from "@tanstack/ai/byok";
|
|
2
|
+
//#region src/byok.ts
|
|
3
|
+
var byteplusByok = defineByokProvider({
|
|
4
|
+
id: "byteplus",
|
|
5
|
+
label: "BytePlus",
|
|
6
|
+
env: ["ARK_API_KEY", "BYTEPLUS_API_KEY"]
|
|
7
|
+
});
|
|
8
|
+
/** Seed Speech TTS/ASR. Different product and key from {@link byteplusByok}. */
|
|
9
|
+
var byteplusVoiceByok = defineByokProvider({
|
|
10
|
+
id: "byteplus-voice",
|
|
11
|
+
label: "BytePlus Seed Speech",
|
|
12
|
+
env: "BYTEPLUS_VOICE_API_KEY"
|
|
13
|
+
});
|
|
14
|
+
//#endregion
|
|
15
|
+
export { byteplusByok, byteplusVoiceByok };
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=byok.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"byok.js","names":[],"sources":["../../src/byok.ts"],"sourcesContent":["import { defineByokProvider } from '@tanstack/ai/byok'\n\nexport const byteplusByok = defineByokProvider({\n id: 'byteplus',\n label: 'BytePlus',\n env: ['ARK_API_KEY', 'BYTEPLUS_API_KEY'],\n})\n\n/** Seed Speech TTS/ASR. Different product and key from {@link byteplusByok}. */\nexport const byteplusVoiceByok = defineByokProvider({\n id: 'byteplus-voice',\n label: 'BytePlus Seed Speech',\n env: 'BYTEPLUS_VOICE_API_KEY',\n})\n"],"mappings":";;AAEA,IAAa,eAAe,mBAAmB;CAC7C,IAAI;CACJ,OAAO;CACP,KAAK,CAAC,eAAe,kBAAkB;AACzC,CAAC;;AAGD,IAAa,oBAAoB,mBAAmB;CAClD,IAAI;CACJ,OAAO;CACP,KAAK;AACP,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-byteplus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "BytePlus ModelArk adapter for TanStack AI: Seed LLM chat, Seedance video, Seedream image, and Seed Speech TTS/ASR.",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
".": {
|
|
25
25
|
"types": "./dist/esm/index.d.ts",
|
|
26
26
|
"import": "./dist/esm/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./byok": {
|
|
29
|
+
"types": "./dist/esm/byok.d.ts",
|
|
30
|
+
"import": "./dist/esm/byok.js"
|
|
27
31
|
}
|
|
28
32
|
},
|
|
29
33
|
"files": [
|
|
@@ -50,16 +54,16 @@
|
|
|
50
54
|
"devDependencies": {
|
|
51
55
|
"@vitest/coverage-v8": "4.1.10",
|
|
52
56
|
"vite": "^8.2.1",
|
|
53
|
-
"@tanstack/ai": "0.
|
|
57
|
+
"@tanstack/ai": "0.49.1"
|
|
54
58
|
},
|
|
55
59
|
"peerDependencies": {
|
|
56
60
|
"zod": "^4.0.0",
|
|
57
|
-
"@tanstack/ai": "^0.
|
|
61
|
+
"@tanstack/ai": "^0.49.1"
|
|
58
62
|
},
|
|
59
63
|
"dependencies": {
|
|
60
64
|
"openai": "^6.41.0",
|
|
61
65
|
"@tanstack/ai-utils": "^0.4.0",
|
|
62
|
-
"@tanstack/openai-base": "^0.10.
|
|
66
|
+
"@tanstack/openai-base": "^0.10.5"
|
|
63
67
|
},
|
|
64
68
|
"scripts": {
|
|
65
69
|
"build": "vite build",
|
package/src/adapters/text.ts
CHANGED
|
@@ -20,7 +20,7 @@ import type {
|
|
|
20
20
|
ContentPartSource,
|
|
21
21
|
Modality,
|
|
22
22
|
ModelMessage,
|
|
23
|
-
|
|
23
|
+
AdapterYieldChunk,
|
|
24
24
|
TextOptions,
|
|
25
25
|
} from '@tanstack/ai'
|
|
26
26
|
import type {
|
|
@@ -155,7 +155,7 @@ export class BytePlusTextAdapter<
|
|
|
155
155
|
messageId: string
|
|
156
156
|
hasEmittedRunStarted: boolean
|
|
157
157
|
},
|
|
158
|
-
): AsyncIterable<
|
|
158
|
+
): AsyncIterable<AdapterYieldChunk> {
|
|
159
159
|
const captured: { encryptedContent?: string } = {}
|
|
160
160
|
|
|
161
161
|
for await (const event of super.processStreamChunks(
|
|
@@ -328,7 +328,7 @@ export class BytePlusTextAdapter<
|
|
|
328
328
|
|
|
329
329
|
override async *structuredOutputStream(
|
|
330
330
|
options: StructuredOutputOptions<TProviderOptions>,
|
|
331
|
-
): AsyncIterable<
|
|
331
|
+
): AsyncIterable<AdapterYieldChunk> {
|
|
332
332
|
const unsupported = this.structuredOutputUnsupportedMessage()
|
|
333
333
|
if (unsupported) {
|
|
334
334
|
// Mirror the base's contract: failures inside structuredOutputStream
|
package/src/byok.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineByokProvider } from '@tanstack/ai/byok'
|
|
2
|
+
|
|
3
|
+
export const byteplusByok = defineByokProvider({
|
|
4
|
+
id: 'byteplus',
|
|
5
|
+
label: 'BytePlus',
|
|
6
|
+
env: ['ARK_API_KEY', 'BYTEPLUS_API_KEY'],
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
/** Seed Speech TTS/ASR. Different product and key from {@link byteplusByok}. */
|
|
10
|
+
export const byteplusVoiceByok = defineByokProvider({
|
|
11
|
+
id: 'byteplus-voice',
|
|
12
|
+
label: 'BytePlus Seed Speech',
|
|
13
|
+
env: 'BYTEPLUS_VOICE_API_KEY',
|
|
14
|
+
})
|