@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.
- package/LICENSE +21 -0
- package/README.md +202 -0
- package/dist/esm/adapters/image.d.ts +89 -0
- package/dist/esm/adapters/image.js +229 -0
- package/dist/esm/adapters/image.js.map +1 -0
- package/dist/esm/adapters/text.d.ts +163 -0
- package/dist/esm/adapters/text.js +347 -0
- package/dist/esm/adapters/text.js.map +1 -0
- package/dist/esm/adapters/transcription.d.ts +102 -0
- package/dist/esm/adapters/transcription.js +274 -0
- package/dist/esm/adapters/transcription.js.map +1 -0
- package/dist/esm/adapters/tts.d.ts +143 -0
- package/dist/esm/adapters/tts.js +307 -0
- package/dist/esm/adapters/tts.js.map +1 -0
- package/dist/esm/adapters/video.d.ts +182 -0
- package/dist/esm/adapters/video.js +442 -0
- package/dist/esm/adapters/video.js.map +1 -0
- package/dist/esm/audio/transcription-provider-options.d.ts +46 -0
- package/dist/esm/audio/tts-provider-options.d.ts +114 -0
- package/dist/esm/audio/wire-types.d.ts +261 -0
- package/dist/esm/audio/wire-types.js +28 -0
- package/dist/esm/audio/wire-types.js.map +1 -0
- package/dist/esm/image/image-provider-options.d.ts +165 -0
- package/dist/esm/image/image-provider-options.js +134 -0
- package/dist/esm/image/image-provider-options.js.map +1 -0
- package/dist/esm/image/wire-types.d.ts +149 -0
- package/dist/esm/index.d.ts +25 -0
- package/dist/esm/index.js +11 -0
- package/dist/esm/message-types.d.ts +154 -0
- package/dist/esm/model-meta.d.ts +594 -0
- package/dist/esm/model-meta.js +619 -0
- package/dist/esm/model-meta.js.map +1 -0
- package/dist/esm/text/text-provider-options.d.ts +109 -0
- package/dist/esm/utils/client.d.ts +183 -0
- package/dist/esm/utils/client.js +253 -0
- package/dist/esm/utils/client.js.map +1 -0
- package/dist/esm/video/video-provider-options.d.ts +197 -0
- package/dist/esm/video/video-provider-options.js +191 -0
- package/dist/esm/video/video-provider-options.js.map +1 -0
- package/dist/esm/video/wire-types.d.ts +248 -0
- package/package.json +77 -0
- package/src/adapters/image.ts +409 -0
- package/src/adapters/text.ts +539 -0
- package/src/adapters/transcription.ts +479 -0
- package/src/adapters/tts.ts +447 -0
- package/src/adapters/video.ts +732 -0
- package/src/audio/transcription-provider-options.ts +46 -0
- package/src/audio/tts-provider-options.ts +122 -0
- package/src/audio/wire-types.ts +290 -0
- package/src/image/image-provider-options.ts +288 -0
- package/src/image/wire-types.ts +169 -0
- package/src/index.ts +222 -0
- package/src/message-types.ts +169 -0
- package/src/model-meta.ts +954 -0
- package/src/text/text-provider-options.ts +151 -0
- package/src/utils/client.ts +377 -0
- package/src/video/video-provider-options.ts +361 -0
- package/src/video/wire-types.ts +293 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BytePlus ModelArk chat provider options.
|
|
3
|
+
*
|
|
4
|
+
* Ark's `/chat/completions` is OpenAI-compatible, so most of this is the
|
|
5
|
+
* familiar sampling surface; `thinking`, `reasoning_effort`,
|
|
6
|
+
* `repetition_penalty` and `service_tier` are the Ark-only additions.
|
|
7
|
+
*
|
|
8
|
+
* Every field below was accepted by a live request against
|
|
9
|
+
* `https://ark.ap-southeast.bytepluses.com/api/v3` on 2026-07-31. Two probe
|
|
10
|
+
* results are encoded as TSDoc warnings rather than types because they are
|
|
11
|
+
* cross-field constraints TypeScript can't express: `max_tokens` and
|
|
12
|
+
* `max_completion_tokens` are mutually exclusive, and `reasoning_effort`
|
|
13
|
+
* combined with `thinking: {type: 'disabled'}` is a 400.
|
|
14
|
+
*
|
|
15
|
+
* `response_format` is deliberately absent — the chat activity owns it via
|
|
16
|
+
* `outputSchema` / structured output, and Ark rejects `json_object` outright
|
|
17
|
+
* on every model.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Reasoning ("deep thinking") switch.
|
|
22
|
+
*
|
|
23
|
+
* - `enabled` — the model reasons before answering (default on every model
|
|
24
|
+
* except `deepseek-v3-2-251201`, where reasoning defaults to off).
|
|
25
|
+
* - `disabled` — skip reasoning.
|
|
26
|
+
* - `auto` — let the model decide. Only accepted by `gpt-oss-120b-250805`.
|
|
27
|
+
*/
|
|
28
|
+
export interface BytePlusThinkingOption {
|
|
29
|
+
type: 'enabled' | 'disabled' | 'auto'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Reasoning budget hint. `none` and `xhigh` are only accepted by
|
|
34
|
+
* `glm-5-2-260617`; `max` by `glm-5-2-260617` and the `deepseek-v4-*` models.
|
|
35
|
+
*
|
|
36
|
+
* Cannot be combined with `thinking: {type: 'disabled'}` — Ark rejects the
|
|
37
|
+
* pair with `400 InvalidParameter` ("Invalid combination of reasoning_effort
|
|
38
|
+
* and thinking type").
|
|
39
|
+
*/
|
|
40
|
+
export type BytePlusReasoningEffort =
|
|
41
|
+
| 'none'
|
|
42
|
+
| 'minimal'
|
|
43
|
+
| 'low'
|
|
44
|
+
| 'medium'
|
|
45
|
+
| 'high'
|
|
46
|
+
| 'xhigh'
|
|
47
|
+
| 'max'
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Request routing tier. `flex` routes to the batch queue at a lower price with
|
|
51
|
+
* no latency guarantee; `default` is the standard online tier.
|
|
52
|
+
*/
|
|
53
|
+
export type BytePlusServiceTier = 'default' | 'flex'
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Forces the model to call one specific function.
|
|
57
|
+
*/
|
|
58
|
+
export interface BytePlusNamedToolChoice {
|
|
59
|
+
type: 'function'
|
|
60
|
+
function: { name: string }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Controls which (if any) tool the model calls.
|
|
65
|
+
*/
|
|
66
|
+
export type BytePlusToolChoice =
|
|
67
|
+
| 'none'
|
|
68
|
+
| 'auto'
|
|
69
|
+
| 'required'
|
|
70
|
+
| BytePlusNamedToolChoice
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Provider options for BytePlus chat models.
|
|
74
|
+
*/
|
|
75
|
+
export interface BytePlusTextProviderOptions {
|
|
76
|
+
// --------------------------------------------------------------------
|
|
77
|
+
// Ark-only
|
|
78
|
+
// --------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
/** Reasoning switch — see {@link BytePlusThinkingOption}. */
|
|
81
|
+
thinking?: BytePlusThinkingOption
|
|
82
|
+
|
|
83
|
+
/** Reasoning budget hint — see {@link BytePlusReasoningEffort}. */
|
|
84
|
+
reasoning_effort?: BytePlusReasoningEffort
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Penalty applied to repeated tokens. Values above 1 discourage repetition.
|
|
88
|
+
*/
|
|
89
|
+
repetition_penalty?: number
|
|
90
|
+
|
|
91
|
+
/** Request routing tier — see {@link BytePlusServiceTier}. */
|
|
92
|
+
service_tier?: BytePlusServiceTier
|
|
93
|
+
|
|
94
|
+
// --------------------------------------------------------------------
|
|
95
|
+
// OpenAI-compatible sampling surface
|
|
96
|
+
// --------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
/** Sampling temperature. Higher values produce more varied output. */
|
|
99
|
+
temperature?: number
|
|
100
|
+
|
|
101
|
+
/** Nucleus sampling cutoff. */
|
|
102
|
+
top_p?: number
|
|
103
|
+
|
|
104
|
+
/** Restricts sampling to the `k` most likely tokens. */
|
|
105
|
+
top_k?: number
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Maximum tokens to generate. Mutually exclusive with
|
|
109
|
+
* `max_completion_tokens` — sending both is a 400.
|
|
110
|
+
*/
|
|
111
|
+
max_tokens?: number
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* OpenAI's newer name for {@link BytePlusTextProviderOptions.max_tokens}.
|
|
115
|
+
* Mutually exclusive with it.
|
|
116
|
+
*/
|
|
117
|
+
max_completion_tokens?: number
|
|
118
|
+
|
|
119
|
+
/** Penalizes tokens by how often they have already appeared. */
|
|
120
|
+
frequency_penalty?: number
|
|
121
|
+
|
|
122
|
+
/** Penalizes tokens that have appeared at all, regardless of count. */
|
|
123
|
+
presence_penalty?: number
|
|
124
|
+
|
|
125
|
+
/** Up to four strings that stop generation when produced. */
|
|
126
|
+
stop?: string | Array<string>
|
|
127
|
+
|
|
128
|
+
/** Number of completions to generate. */
|
|
129
|
+
n?: number
|
|
130
|
+
|
|
131
|
+
/** Best-effort determinism hint for repeated identical requests. */
|
|
132
|
+
seed?: number
|
|
133
|
+
|
|
134
|
+
/** Return log probabilities for the generated tokens. */
|
|
135
|
+
logprobs?: boolean
|
|
136
|
+
|
|
137
|
+
/** How many alternatives to report per token. Requires `logprobs`. */
|
|
138
|
+
top_logprobs?: number
|
|
139
|
+
|
|
140
|
+
/** Additive bias per token id, applied before sampling. */
|
|
141
|
+
logit_bias?: Record<string, number>
|
|
142
|
+
|
|
143
|
+
/** Opaque end-user identifier forwarded for abuse monitoring. */
|
|
144
|
+
user?: string
|
|
145
|
+
|
|
146
|
+
/** Whether the model may emit several tool calls in one turn. */
|
|
147
|
+
parallel_tool_calls?: boolean
|
|
148
|
+
|
|
149
|
+
/** Tool-selection strategy — see {@link BytePlusToolChoice}. */
|
|
150
|
+
tool_choice?: BytePlusToolChoice
|
|
151
|
+
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { getApiKeyFromEnv } from '@tanstack/ai-utils'
|
|
2
|
+
import type { ClientOptions } from 'openai'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* BytePlus splits its APIs across two hosts with two different products,
|
|
6
|
+
* two different auth headers, and two different API keys:
|
|
7
|
+
*
|
|
8
|
+
* - **Ark (ModelArk)** — chat, video (Seedance) and image (Seedream).
|
|
9
|
+
* `Authorization: Bearer $ARK_API_KEY`.
|
|
10
|
+
* - **Seed Speech** — TTS and ASR on the voice host.
|
|
11
|
+
* `X-Api-Key: $BYTEPLUS_VOICE_API_KEY`.
|
|
12
|
+
*
|
|
13
|
+
* Ark keys are region-isolated: a key issued for `ap-southeast` does not work
|
|
14
|
+
* against the EU host and vice versa.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Default Ark data-plane base URL (Asia-Pacific south-east).
|
|
19
|
+
*
|
|
20
|
+
* Per the BytePlus docs the EU endpoint
|
|
21
|
+
* (`https://ark.eu-west.bytepluses.com/api/v3`) serves chat and image only —
|
|
22
|
+
* Seedance video is not available there. Docs-derived: only the ap-southeast
|
|
23
|
+
* host was exercised live.
|
|
24
|
+
*/
|
|
25
|
+
export const BYTEPLUS_ARK_BASE_URL =
|
|
26
|
+
'https://ark.ap-southeast.bytepluses.com/api/v3'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Default Seed Speech base URL. Endpoint paths are appended under
|
|
30
|
+
* `/api/v3` (e.g. `/api/v3/tts/create`).
|
|
31
|
+
*/
|
|
32
|
+
export const BYTEPLUS_VOICE_BASE_URL =
|
|
33
|
+
'https://voice.ap-southeast-1.bytepluses.com'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Configuration for the Ark-hosted adapters (chat, video, image).
|
|
37
|
+
*
|
|
38
|
+
* Extends the OpenAI SDK's client options because the chat adapter drives the
|
|
39
|
+
* OpenAI-compatible `/chat/completions` endpoint through the shared
|
|
40
|
+
* `@tanstack/openai-base` adapter. `fetch` and `defaultHeaders` are inherited
|
|
41
|
+
* from `ClientOptions`, and the video/image adapters — which issue plain JSON
|
|
42
|
+
* requests rather than SDK calls — honour the same two fields so tests can
|
|
43
|
+
* inject a fetch instead of patching the global one.
|
|
44
|
+
*
|
|
45
|
+
* Two inherited fields differ in reach, because the fetch-based adapters have
|
|
46
|
+
* no SDK to delegate to:
|
|
47
|
+
* - `timeout` is honoured everywhere — the fetch-based adapters convert it to
|
|
48
|
+
* an `AbortSignal` (see {@link bytePlusTimeoutSignal}).
|
|
49
|
+
* - `maxRetries` applies to the **chat adapter only**. The video, image and
|
|
50
|
+
* speech adapters do not retry; video polling is driven by core's loop,
|
|
51
|
+
* which owns its own retry policy.
|
|
52
|
+
*/
|
|
53
|
+
export interface BytePlusArkConfig extends Omit<ClientOptions, 'apiKey'> {
|
|
54
|
+
apiKey: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Configuration for the Seed Speech adapters (TTS, ASR).
|
|
59
|
+
*
|
|
60
|
+
* Seed Speech is not OpenAI-compatible, so this is a minimal config for
|
|
61
|
+
* direct `fetch` calls rather than an OpenAI `ClientOptions` extension.
|
|
62
|
+
*/
|
|
63
|
+
export interface BytePlusVoiceConfig {
|
|
64
|
+
/** Seed Speech API key — *not* the Ark key. Sent as `X-Api-Key`. */
|
|
65
|
+
apiKey: string
|
|
66
|
+
|
|
67
|
+
/** Overrides {@link BYTEPLUS_VOICE_BASE_URL}. */
|
|
68
|
+
baseURL?: string
|
|
69
|
+
|
|
70
|
+
/** Additional headers merged into every request (e.g., test ids). */
|
|
71
|
+
defaultHeaders?: Record<string, string>
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Override the underlying fetch. Defaults to the global `fetch`. Useful for
|
|
75
|
+
* proxying, instrumentation, or pointing requests at a mock in tests.
|
|
76
|
+
*/
|
|
77
|
+
fetch?: typeof fetch
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Gets the BytePlus Ark API key from environment variables, preferring
|
|
82
|
+
* `ARK_API_KEY` and falling back to `BYTEPLUS_API_KEY`.
|
|
83
|
+
* @throws Error if neither variable is set
|
|
84
|
+
*/
|
|
85
|
+
export function getBytePlusArkApiKeyFromEnv(): string {
|
|
86
|
+
try {
|
|
87
|
+
return getApiKeyFromEnv('ARK_API_KEY')
|
|
88
|
+
} catch {
|
|
89
|
+
try {
|
|
90
|
+
return getApiKeyFromEnv('BYTEPLUS_API_KEY')
|
|
91
|
+
} catch {
|
|
92
|
+
throw new Error(
|
|
93
|
+
'ARK_API_KEY or BYTEPLUS_API_KEY is required. Please set one of these environment variables or use the factory function with an explicit API key.',
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Gets the Seed Speech API key from environment variables.
|
|
101
|
+
*
|
|
102
|
+
* Seed Speech is a separate BytePlus product from Ark with its own key — an
|
|
103
|
+
* Ark key sent as `X-Api-Key` is rejected with `45000010 Invalid X-Api-Key`.
|
|
104
|
+
*
|
|
105
|
+
* @throws Error if BYTEPLUS_VOICE_API_KEY is not found
|
|
106
|
+
*/
|
|
107
|
+
export function getBytePlusVoiceApiKeyFromEnv(): string {
|
|
108
|
+
try {
|
|
109
|
+
return getApiKeyFromEnv('BYTEPLUS_VOICE_API_KEY')
|
|
110
|
+
} catch {
|
|
111
|
+
throw new Error(
|
|
112
|
+
'BYTEPLUS_VOICE_API_KEY is required for Seed Speech (TTS/ASR). This is a different key from ARK_API_KEY. Please set it in your environment variables or use the factory function with an explicit API key.',
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Returns an Ark client config with the default Ark base URL applied when not
|
|
119
|
+
* already set, and any trailing slashes trimmed so path joins stay
|
|
120
|
+
* single-slashed.
|
|
121
|
+
*
|
|
122
|
+
* The returned `baseURL` is always a string: adapters that build request paths
|
|
123
|
+
* by interpolation can use it directly without re-applying a default (which
|
|
124
|
+
* would otherwise risk interpolating `undefined` into a URL). The config's own
|
|
125
|
+
* type is preserved, so adapter-specific config fields survive the call.
|
|
126
|
+
*/
|
|
127
|
+
export function withBytePlusArkDefaults<TConfig extends BytePlusArkConfig>(
|
|
128
|
+
config: TConfig,
|
|
129
|
+
): Omit<TConfig, 'baseURL'> & { baseURL: string } {
|
|
130
|
+
return {
|
|
131
|
+
...config,
|
|
132
|
+
baseURL: (config.baseURL || BYTEPLUS_ARK_BASE_URL).replace(/\/+$/, ''),
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Returns a Seed Speech config with the default voice base URL applied (and
|
|
138
|
+
* any trailing slashes trimmed) when not already set.
|
|
139
|
+
*
|
|
140
|
+
* As with {@link withBytePlusArkDefaults}, the returned `baseURL` is always a
|
|
141
|
+
* string, so adapters can interpolate it without re-applying a fallback, and
|
|
142
|
+
* the config's own type is preserved.
|
|
143
|
+
*/
|
|
144
|
+
export function withBytePlusVoiceDefaults<TConfig extends BytePlusVoiceConfig>(
|
|
145
|
+
config: TConfig,
|
|
146
|
+
): Omit<TConfig, 'baseURL'> & { baseURL: string } {
|
|
147
|
+
return {
|
|
148
|
+
...config,
|
|
149
|
+
baseURL: (config.baseURL || BYTEPLUS_VOICE_BASE_URL).replace(/\/+$/, ''),
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Normalizes the OpenAI-shaped `defaultHeaders` config field (which accepts a
|
|
155
|
+
* `Headers` instance, an entry list, or a record with nullable values) into
|
|
156
|
+
* the plain record the header builders below take. Non-string values are
|
|
157
|
+
* dropped rather than serialized.
|
|
158
|
+
*
|
|
159
|
+
* Shared by every fetch-based adapter (image, video, speech): they all read
|
|
160
|
+
* `defaultHeaders` off a config typed by the OpenAI SDK but issue plain JSON
|
|
161
|
+
* requests.
|
|
162
|
+
*/
|
|
163
|
+
export function toHeaderRecord(
|
|
164
|
+
headers: BytePlusArkConfig['defaultHeaders'],
|
|
165
|
+
): Record<string, string> {
|
|
166
|
+
const record: Record<string, string> = {}
|
|
167
|
+
if (!headers) return record
|
|
168
|
+
|
|
169
|
+
if (headers instanceof Headers) {
|
|
170
|
+
headers.forEach((value, key) => {
|
|
171
|
+
record[key] = value
|
|
172
|
+
})
|
|
173
|
+
return record
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// The entry-list form is typed as arrays of nullable values rather than
|
|
177
|
+
// strict [name, value] tuples, so both halves are checked.
|
|
178
|
+
if (Array.isArray(headers)) {
|
|
179
|
+
for (const [key, value] of headers) {
|
|
180
|
+
if (typeof key === 'string' && typeof value === 'string') {
|
|
181
|
+
record[key] = value
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return record
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Record form. A value may be null/undefined (openai's "unset this header"
|
|
188
|
+
// signal) or an array for a repeated header; neither maps onto a single
|
|
189
|
+
// string, so both are dropped.
|
|
190
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
191
|
+
if (typeof value === 'string') record[key] = value
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return record
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Drops any caller-supplied header whose name case-insensitively collides with
|
|
199
|
+
* one the adapter sets itself, then applies the adapter's own.
|
|
200
|
+
*
|
|
201
|
+
* Spreading `reserved` last is not enough on its own: HTTP header names are
|
|
202
|
+
* case-insensitive, but plain object keys are not, so `authorization` and
|
|
203
|
+
* `Authorization` are two distinct properties that both survive the spread.
|
|
204
|
+
* `fetch` then feeds the object to the `Headers` constructor, which *appends*
|
|
205
|
+
* rather than replaces — turning the pair into
|
|
206
|
+
* `authorization: "Bearer wrong, Bearer right"` and 401ing every request with
|
|
207
|
+
* what reads like a bad key. `toHeaderRecord` lowercases names whenever
|
|
208
|
+
* `defaultHeaders` arrives as a `Headers` instance, so that collision is
|
|
209
|
+
* reachable through ordinary config, not just a hand-built record.
|
|
210
|
+
*/
|
|
211
|
+
function applyReservedHeaders(
|
|
212
|
+
extraHeaders: Record<string, string> | undefined,
|
|
213
|
+
reserved: Record<string, string>,
|
|
214
|
+
): Record<string, string> {
|
|
215
|
+
const blocked = new Set(Object.keys(reserved).map((key) => key.toLowerCase()))
|
|
216
|
+
const merged: Record<string, string> = {}
|
|
217
|
+
for (const [key, value] of Object.entries(extraHeaders ?? {})) {
|
|
218
|
+
if (!blocked.has(key.toLowerCase())) merged[key] = value
|
|
219
|
+
}
|
|
220
|
+
return { ...merged, ...reserved }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Turns the OpenAI-shaped `timeout` config field (milliseconds) into the
|
|
225
|
+
* `signal` a plain `fetch` needs, or `undefined` when no timeout is set.
|
|
226
|
+
*
|
|
227
|
+
* `BytePlusArkConfig` extends the OpenAI SDK's `ClientOptions` because the
|
|
228
|
+
* chat adapter drives the SDK, which honours `timeout` and `maxRetries`
|
|
229
|
+
* itself. The video, image and speech adapters issue plain JSON requests, so
|
|
230
|
+
* without this they would accept a `timeout` and ignore it — a stalled Ark
|
|
231
|
+
* connection hanging the caller forever despite an explicit setting.
|
|
232
|
+
*
|
|
233
|
+
* `maxRetries` has no equivalent here and stays SDK-path-only; it is
|
|
234
|
+
* documented as such on {@link BytePlusArkConfig}.
|
|
235
|
+
*/
|
|
236
|
+
export function bytePlusTimeoutSignal(
|
|
237
|
+
timeout: number | undefined,
|
|
238
|
+
): AbortSignal | undefined {
|
|
239
|
+
return typeof timeout === 'number' && timeout > 0
|
|
240
|
+
? AbortSignal.timeout(timeout)
|
|
241
|
+
: undefined
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Headers for a JSON request against the Ark data plane.
|
|
246
|
+
*
|
|
247
|
+
* A caller-supplied `Authorization` or `Content-Type` in `defaultHeaders` is
|
|
248
|
+
* dropped in any casing — see {@link applyReservedHeaders}.
|
|
249
|
+
*/
|
|
250
|
+
export function bytePlusArkHeaders(
|
|
251
|
+
apiKey: string,
|
|
252
|
+
extraHeaders?: Record<string, string>,
|
|
253
|
+
): Record<string, string> {
|
|
254
|
+
return applyReservedHeaders(extraHeaders, {
|
|
255
|
+
'Content-Type': 'application/json',
|
|
256
|
+
Authorization: `Bearer ${apiKey}`,
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Headers for a JSON request against the Seed Speech host.
|
|
262
|
+
*
|
|
263
|
+
* As with {@link bytePlusArkHeaders}, a caller-supplied `X-Api-Key` is dropped
|
|
264
|
+
* in any casing. Seed Speech answers a clobbered key with
|
|
265
|
+
* `45000010 Invalid X-Api-Key`, which reads as a misconfigured key rather than
|
|
266
|
+
* a header collision.
|
|
267
|
+
*/
|
|
268
|
+
export function bytePlusVoiceHeaders(
|
|
269
|
+
apiKey: string,
|
|
270
|
+
extraHeaders?: Record<string, string>,
|
|
271
|
+
): Record<string, string> {
|
|
272
|
+
return applyReservedHeaders(extraHeaders, {
|
|
273
|
+
'Content-Type': 'application/json',
|
|
274
|
+
'X-Api-Key': apiKey,
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Reads a response body as JSON, tolerating the non-JSON failures both
|
|
280
|
+
* BytePlus hosts can return (an empty body, or an HTML error page from a proxy
|
|
281
|
+
* in front of the API).
|
|
282
|
+
*
|
|
283
|
+
* Returns the parsed value, the raw text when it is not JSON, or `undefined`
|
|
284
|
+
* for an empty body — all three of which {@link bytePlusArkError} and
|
|
285
|
+
* {@link bytePlusVoiceError} know how to render.
|
|
286
|
+
*/
|
|
287
|
+
export async function readJsonBody(response: Response): Promise<unknown> {
|
|
288
|
+
const text = await response.text()
|
|
289
|
+
if (!text) return undefined
|
|
290
|
+
try {
|
|
291
|
+
return JSON.parse(text)
|
|
292
|
+
} catch {
|
|
293
|
+
return text
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Best-effort human-readable rendering of a response body we could not pull a
|
|
299
|
+
* `message` out of — a raw string passes through, any other object is
|
|
300
|
+
* serialized so the detail reaches the error instead of being dropped.
|
|
301
|
+
*
|
|
302
|
+
* Exported for adapters that need to attach a body to an error they raise
|
|
303
|
+
* themselves rather than one derived from a non-OK response — e.g. the image
|
|
304
|
+
* adapter reporting a 200 whose `data[]` items match no known shape.
|
|
305
|
+
*/
|
|
306
|
+
export function describeBody(body: unknown): string | undefined {
|
|
307
|
+
if (typeof body === 'string') return body || undefined
|
|
308
|
+
if (typeof body !== 'object' || body === null) return undefined
|
|
309
|
+
try {
|
|
310
|
+
return JSON.stringify(body)
|
|
311
|
+
} catch {
|
|
312
|
+
// Circular or otherwise unserializable — the status code stands alone.
|
|
313
|
+
return undefined
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function readStringField(value: unknown, field: string): string | undefined {
|
|
318
|
+
if (typeof value !== 'object' || value === null || !(field in value)) {
|
|
319
|
+
return undefined
|
|
320
|
+
}
|
|
321
|
+
const candidate = Reflect.get(value, field)
|
|
322
|
+
if (typeof candidate === 'string') return candidate
|
|
323
|
+
if (typeof candidate === 'number') return String(candidate)
|
|
324
|
+
return undefined
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Formats an Ark error response into an `Error`.
|
|
329
|
+
*
|
|
330
|
+
* Ark uses the OpenAI error envelope with dotted string codes:
|
|
331
|
+
* `{"error": {"code": "InvalidEndpointOrModel.NotFound", "message": "…"}}`.
|
|
332
|
+
* Bodies that don't match (HTML error pages, proxy responses) fall back to
|
|
333
|
+
* the raw text so the failure stays diagnosable.
|
|
334
|
+
*/
|
|
335
|
+
export function bytePlusArkError(
|
|
336
|
+
status: number,
|
|
337
|
+
body: unknown,
|
|
338
|
+
context?: string,
|
|
339
|
+
): Error {
|
|
340
|
+
const prefix = context ? `BytePlus Ark ${context}` : 'BytePlus Ark request'
|
|
341
|
+
const error =
|
|
342
|
+
typeof body === 'object' && body !== null && 'error' in body
|
|
343
|
+
? Reflect.get(body, 'error')
|
|
344
|
+
: undefined
|
|
345
|
+
const code = readStringField(error, 'code')
|
|
346
|
+
const message = readStringField(error, 'message')
|
|
347
|
+
const detail = message ?? describeBody(body)
|
|
348
|
+
return new Error(
|
|
349
|
+
`${prefix} failed (${status}${code ? ` ${code}` : ''})${
|
|
350
|
+
detail ? `: ${detail}` : ''
|
|
351
|
+
}`,
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Formats a Seed Speech error response into an `Error`.
|
|
357
|
+
*
|
|
358
|
+
* Seed Speech does not use the Ark envelope — it returns a flat numeric code:
|
|
359
|
+
* `{"code": 45000010, "message": "Invalid X-Api-Key"}`.
|
|
360
|
+
*/
|
|
361
|
+
export function bytePlusVoiceError(
|
|
362
|
+
status: number,
|
|
363
|
+
body: unknown,
|
|
364
|
+
context?: string,
|
|
365
|
+
): Error {
|
|
366
|
+
const prefix = context
|
|
367
|
+
? `BytePlus Seed Speech ${context}`
|
|
368
|
+
: 'BytePlus Seed Speech request'
|
|
369
|
+
const code = readStringField(body, 'code')
|
|
370
|
+
const message = readStringField(body, 'message')
|
|
371
|
+
const detail = message ?? describeBody(body)
|
|
372
|
+
return new Error(
|
|
373
|
+
`${prefix} failed (${status}${code ? ` ${code}` : ''})${
|
|
374
|
+
detail ? `: ${detail}` : ''
|
|
375
|
+
}`,
|
|
376
|
+
)
|
|
377
|
+
}
|