@volcengine/ark-runtime 1.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.txt +202 -0
- package/README.md +104 -0
- package/dist/cjs/index.js +1717 -0
- package/dist/esm/client.d.ts +97 -0
- package/dist/esm/client.d.ts.map +1 -0
- package/dist/esm/config.d.ts +46 -0
- package/dist/esm/config.d.ts.map +1 -0
- package/dist/esm/encryption/encrypt-chat.d.ts +24 -0
- package/dist/esm/encryption/encrypt-chat.d.ts.map +1 -0
- package/dist/esm/encryption/index.d.ts +4 -0
- package/dist/esm/encryption/index.d.ts.map +1 -0
- package/dist/esm/encryption/key-agreement.d.ts +73 -0
- package/dist/esm/encryption/key-agreement.d.ts.map +1 -0
- package/dist/esm/index.d.ts +11 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.mjs +1476 -0
- package/dist/esm/rslib-runtime.mjs +37 -0
- package/dist/esm/types/bot.d.ts +109 -0
- package/dist/esm/types/bot.d.ts.map +1 -0
- package/dist/esm/types/chat-completion.d.ts +167 -0
- package/dist/esm/types/chat-completion.d.ts.map +1 -0
- package/dist/esm/types/common.d.ts +29 -0
- package/dist/esm/types/common.d.ts.map +1 -0
- package/dist/esm/types/content-generation.d.ts +118 -0
- package/dist/esm/types/content-generation.d.ts.map +1 -0
- package/dist/esm/types/context.d.ts +49 -0
- package/dist/esm/types/context.d.ts.map +1 -0
- package/dist/esm/types/embeddings.d.ts +44 -0
- package/dist/esm/types/embeddings.d.ts.map +1 -0
- package/dist/esm/types/error.d.ts +45 -0
- package/dist/esm/types/error.d.ts.map +1 -0
- package/dist/esm/types/file.d.ts +66 -0
- package/dist/esm/types/file.d.ts.map +1 -0
- package/dist/esm/types/http-request-error.d.ts +13 -0
- package/dist/esm/types/http-request-error.d.ts.map +1 -0
- package/dist/esm/types/images.d.ts +78 -0
- package/dist/esm/types/images.d.ts.map +1 -0
- package/dist/esm/types/index.d.ts +13 -0
- package/dist/esm/types/index.d.ts.map +1 -0
- package/dist/esm/types/multimodal-embedding.d.ts +56 -0
- package/dist/esm/types/multimodal-embedding.d.ts.map +1 -0
- package/dist/esm/types/responses/enums.d.ts +38 -0
- package/dist/esm/types/responses/enums.d.ts.map +1 -0
- package/dist/esm/types/responses/helpers.d.ts +22 -0
- package/dist/esm/types/responses/helpers.d.ts.map +1 -0
- package/dist/esm/types/responses/index.d.ts +4 -0
- package/dist/esm/types/responses/index.d.ts.map +1 -0
- package/dist/esm/types/responses/types.d.ts +906 -0
- package/dist/esm/types/responses/types.d.ts.map +1 -0
- package/dist/esm/types/tokenization.d.ts +22 -0
- package/dist/esm/types/tokenization.d.ts.map +1 -0
- package/dist/esm/utils/breaker-provider.d.ts +9 -0
- package/dist/esm/utils/breaker-provider.d.ts.map +1 -0
- package/dist/esm/utils/breaker.d.ts +28 -0
- package/dist/esm/utils/breaker.d.ts.map +1 -0
- package/dist/esm/utils/normalize.d.ts +51 -0
- package/dist/esm/utils/normalize.d.ts.map +1 -0
- package/dist/esm/utils/request-builder.d.ts +15 -0
- package/dist/esm/utils/request-builder.d.ts.map +1 -0
- package/dist/esm/utils/request-id.d.ts +5 -0
- package/dist/esm/utils/request-id.d.ts.map +1 -0
- package/dist/esm/utils/retry.d.ts +11 -0
- package/dist/esm/utils/retry.d.ts.map +1 -0
- package/dist/esm/utils/sse-decoder.d.ts +23 -0
- package/dist/esm/utils/sse-decoder.d.ts.map +1 -0
- package/dist/esm/utils/stream-reader.d.ts +67 -0
- package/dist/esm/utils/stream-reader.d.ts.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/example/README.md +118 -0
- package/example/batch-chat.ts +64 -0
- package/example/bot-chat.ts +66 -0
- package/example/chat-completion-function-call.ts +141 -0
- package/example/chat-completion-reasoning.ts +64 -0
- package/example/chat-completion-vision.ts +70 -0
- package/example/chat-completion.ts +62 -0
- package/example/content-generation.ts +70 -0
- package/example/context.ts +69 -0
- package/example/embeddings.ts +31 -0
- package/example/file-upload.ts +53 -0
- package/example/images.ts +74 -0
- package/example/list-input-items.ts +34 -0
- package/example/multimodal-embeddings.ts +36 -0
- package/example/responses/basic.ts +75 -0
- package/example/responses/doubao-app.ts +53 -0
- package/example/responses/mcp.ts +66 -0
- package/example/responses/streaming.ts +45 -0
- package/example/responses/video.ts +74 -0
- package/example/responses/web-search.ts +52 -0
- package/example/structured-outputs.ts +71 -0
- package/example/tokenization.ts +30 -0
- package/package.json +47 -0
- package/src/client.ts +1199 -0
- package/src/config.ts +68 -0
- package/src/encryption/encrypt-chat.ts +146 -0
- package/src/encryption/index.ts +21 -0
- package/src/encryption/key-agreement.ts +270 -0
- package/src/index.ts +10 -0
- package/src/types/ark.d.ts +9 -0
- package/src/types/bot.ts +127 -0
- package/src/types/chat-completion.ts +228 -0
- package/src/types/common.ts +37 -0
- package/src/types/content-generation.ts +135 -0
- package/src/types/context.ts +59 -0
- package/src/types/embeddings.ts +74 -0
- package/src/types/error.ts +93 -0
- package/src/types/file.ts +76 -0
- package/src/types/http-request-error.ts +34 -0
- package/src/types/images.ts +102 -0
- package/src/types/index.ts +12 -0
- package/src/types/multimodal-embedding.ts +67 -0
- package/src/types/responses/enums.ts +163 -0
- package/src/types/responses/helpers.ts +67 -0
- package/src/types/responses/index.ts +3 -0
- package/src/types/responses/types.ts +1335 -0
- package/src/types/tokenization.ts +24 -0
- package/src/utils/breaker-provider.ts +17 -0
- package/src/utils/breaker.ts +56 -0
- package/src/utils/normalize.ts +154 -0
- package/src/utils/request-builder.ts +51 -0
- package/src/utils/request-id.ts +17 -0
- package/src/utils/retry.ts +76 -0
- package/src/utils/sse-decoder.ts +140 -0
- package/src/utils/stream-reader.ts +270 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,1199 @@
|
|
|
1
|
+
import axios, {
|
|
2
|
+
type AxiosInstance,
|
|
3
|
+
type AxiosResponse,
|
|
4
|
+
type AxiosRequestConfig,
|
|
5
|
+
} from "axios";
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import * as https from "https";
|
|
8
|
+
import { genRequestId } from "./utils/request-id";
|
|
9
|
+
import { retry, type RetryPolicy } from "./utils/retry";
|
|
10
|
+
import { ModelBreakerProvider } from "./utils/breaker-provider";
|
|
11
|
+
import {
|
|
12
|
+
ChatCompletionStreamReader,
|
|
13
|
+
BotChatCompletionStreamReader,
|
|
14
|
+
ImageGenerationStreamReader,
|
|
15
|
+
ResponsesStreamReader,
|
|
16
|
+
} from "./utils/stream-reader";
|
|
17
|
+
import {
|
|
18
|
+
type ClientConfig,
|
|
19
|
+
type ResolvedConfig,
|
|
20
|
+
resolveConfig,
|
|
21
|
+
resourceTypeEndpoint,
|
|
22
|
+
resourceTypeBot,
|
|
23
|
+
resourceTypePresetEndpoint,
|
|
24
|
+
} from "./config";
|
|
25
|
+
import {
|
|
26
|
+
ClientRequestHeader,
|
|
27
|
+
ClientSessionTokenHeader,
|
|
28
|
+
ClientEncryptInfoHeader,
|
|
29
|
+
ClientIsEncryptedHeader,
|
|
30
|
+
ErrorRetryBaseDelayMs,
|
|
31
|
+
ErrorRetryMaxDelayMs,
|
|
32
|
+
DefaultAdvisoryRefreshTimeout,
|
|
33
|
+
DefaultMandatoryRefreshTimeout,
|
|
34
|
+
DefaultStsTimeout,
|
|
35
|
+
} from "./types/common";
|
|
36
|
+
import { ArkAPIError, ArkRequestError, type ErrorResponse } from "./types/error";
|
|
37
|
+
import {
|
|
38
|
+
ErrChatCompletionStreamNotSupported,
|
|
39
|
+
ErrAKSKNotSupported,
|
|
40
|
+
} from "./types/error";
|
|
41
|
+
import type {
|
|
42
|
+
ChatCompletionRequest,
|
|
43
|
+
ChatCompletionResponse,
|
|
44
|
+
ChatCompletionStreamResponse,
|
|
45
|
+
} from "./types/chat-completion";
|
|
46
|
+
import type {
|
|
47
|
+
BotChatCompletionRequest,
|
|
48
|
+
BotChatCompletionResponse,
|
|
49
|
+
BotChatCompletionStreamResponse,
|
|
50
|
+
} from "./types/bot";
|
|
51
|
+
import type {
|
|
52
|
+
EmbeddingRequest,
|
|
53
|
+
EmbeddingResponse,
|
|
54
|
+
} from "./types/embeddings";
|
|
55
|
+
import type { GenerateImagesRequest, ImagesResponse, ImagesStreamResponse } from "./types/images";
|
|
56
|
+
import type { TokenizationRequest, TokenizationResponse } from "./types/tokenization";
|
|
57
|
+
import type {
|
|
58
|
+
CreateContextRequest,
|
|
59
|
+
CreateContextResponse,
|
|
60
|
+
ContextChatCompletionRequest,
|
|
61
|
+
} from "./types/context";
|
|
62
|
+
import type {
|
|
63
|
+
CreateContentGenerationTaskRequest,
|
|
64
|
+
CreateContentGenerationTaskResponse,
|
|
65
|
+
GetContentGenerationTaskResponse,
|
|
66
|
+
ListContentGenerationTasksRequest,
|
|
67
|
+
ListContentGenerationTasksResponse,
|
|
68
|
+
DeleteContentGenerationTaskRequest,
|
|
69
|
+
} from "./types/content-generation";
|
|
70
|
+
import type {
|
|
71
|
+
FileMeta,
|
|
72
|
+
UploadFileRequest,
|
|
73
|
+
ListFilesRequest,
|
|
74
|
+
ListFilesResponse,
|
|
75
|
+
DeleteFileResponse,
|
|
76
|
+
} from "./types/file";
|
|
77
|
+
import { PurposeUserData, FileStatusProcessing } from "./types/file";
|
|
78
|
+
import type {
|
|
79
|
+
MultiModalEmbeddingRequest,
|
|
80
|
+
MultimodalEmbeddingResponse,
|
|
81
|
+
} from "./types/multimodal-embedding";
|
|
82
|
+
import type {
|
|
83
|
+
ResponsesRequest,
|
|
84
|
+
ResponsesInput,
|
|
85
|
+
InputItem,
|
|
86
|
+
ContentItem,
|
|
87
|
+
ContentItemImage,
|
|
88
|
+
ContentItemVideo,
|
|
89
|
+
ContentItemFile,
|
|
90
|
+
} from "./types/responses/types";
|
|
91
|
+
import {
|
|
92
|
+
E2eeClient,
|
|
93
|
+
checkIsModeAICC,
|
|
94
|
+
loadLocalCertificate,
|
|
95
|
+
saveToLocalCertificate,
|
|
96
|
+
} from "./encryption/key-agreement";
|
|
97
|
+
import {
|
|
98
|
+
encryptChatRequest,
|
|
99
|
+
deepCopyRequest,
|
|
100
|
+
} from "./encryption/encrypt-chat";
|
|
101
|
+
import {
|
|
102
|
+
normalizeChatCompletionResponse,
|
|
103
|
+
normalizeEmbeddingResponse,
|
|
104
|
+
} from "./utils/normalize";
|
|
105
|
+
|
|
106
|
+
// Path constants
|
|
107
|
+
const chatCompletionsSuffix = "/chat/completions";
|
|
108
|
+
const botChatCompletionsSuffix = "/bots/chat/completions";
|
|
109
|
+
const embeddingsSuffix = "/embeddings";
|
|
110
|
+
const multimodalEmbeddingsSuffix = "/embeddings/multimodal";
|
|
111
|
+
const generateImagesPath = "/images/generations";
|
|
112
|
+
const tokenizationSuffix = "/tokenization";
|
|
113
|
+
const contextCreateSuffix = "/context/create";
|
|
114
|
+
const contextChatSuffix = "/context/chat/completions";
|
|
115
|
+
const contentGenerationTaskPath = "/contents/generations/tasks";
|
|
116
|
+
const filePrefix = "/files";
|
|
117
|
+
const batchChatCompletionsSuffix = "/batch/chat/completions";
|
|
118
|
+
const batchEmbeddingsSuffix = "/batch/embeddings";
|
|
119
|
+
const batchMultiModalEmbeddingsSuffix = "/batch/embeddings/multimodal";
|
|
120
|
+
const e2eGetCertificatePath = "/e2e/get/certificate";
|
|
121
|
+
|
|
122
|
+
// File preprocessing constants
|
|
123
|
+
const FILE_SCHEME = "file";
|
|
124
|
+
const POLL_INTERVAL_MS = 3000;
|
|
125
|
+
const MAX_WAIT_TIME_MS = 10 * 60 * 1000;
|
|
126
|
+
|
|
127
|
+
interface CertificateResponse {
|
|
128
|
+
Certificate: string;
|
|
129
|
+
error?: Record<string, string>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface RequestOptions {
|
|
133
|
+
customHeaders?: Record<string, string>;
|
|
134
|
+
projectName?: string;
|
|
135
|
+
signal?: AbortSignal;
|
|
136
|
+
query?: Record<string, string>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
interface TokenInfo {
|
|
140
|
+
token: string;
|
|
141
|
+
expiredTime: number; // unix timestamp (seconds)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export class ArkRuntimeClient {
|
|
145
|
+
private config: ResolvedConfig;
|
|
146
|
+
private httpClient: AxiosInstance;
|
|
147
|
+
private batchHttpClient: AxiosInstance;
|
|
148
|
+
private resourceStsTokens = new Map<string, TokenInfo>();
|
|
149
|
+
private refreshPromise: Promise<void> | null = null;
|
|
150
|
+
private advisoryRefreshTimeout = DefaultAdvisoryRefreshTimeout;
|
|
151
|
+
private mandatoryRefreshTimeout = DefaultMandatoryRefreshTimeout;
|
|
152
|
+
private modelBreakerProvider = new ModelBreakerProvider();
|
|
153
|
+
|
|
154
|
+
// E2EE state
|
|
155
|
+
private e2eeManager = new Map<string, E2eeClient>();
|
|
156
|
+
private keyNonceMap = new Map<string, Buffer>();
|
|
157
|
+
private e2eeFlightMap = new Map<string, Promise<E2eeClient>>();
|
|
158
|
+
|
|
159
|
+
constructor(config: ClientConfig) {
|
|
160
|
+
this.config = resolveConfig(config);
|
|
161
|
+
this.httpClient =
|
|
162
|
+
this.config.httpClient ??
|
|
163
|
+
axios.create({
|
|
164
|
+
timeout: this.config.timeout,
|
|
165
|
+
httpsAgent: new https.Agent({ keepAlive: true }),
|
|
166
|
+
paramsSerializer: (params) => {
|
|
167
|
+
const parts: string[] = [];
|
|
168
|
+
for (const [key, value] of Object.entries(params)) {
|
|
169
|
+
if (Array.isArray(value)) {
|
|
170
|
+
for (const v of value) {
|
|
171
|
+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`);
|
|
172
|
+
}
|
|
173
|
+
} else if (value != null) {
|
|
174
|
+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return parts.join("&");
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
this.batchHttpClient = axios.create({
|
|
181
|
+
timeout: this.config.timeout,
|
|
182
|
+
httpsAgent: new https.Agent({ keepAlive: true }),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Create with API key auth */
|
|
187
|
+
static withApiKey(apiKey: string, config: Omit<ClientConfig, "apiKey"> = {}): ArkRuntimeClient {
|
|
188
|
+
return new ArkRuntimeClient({ ...config, apiKey });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Create with AK/SK auth */
|
|
192
|
+
static withAkSk(ak: string, sk: string, config: Omit<ClientConfig, "ak" | "sk"> = {}): ArkRuntimeClient {
|
|
193
|
+
return new ArkRuntimeClient({ ...config, ak, sk });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ─── URL helpers ──────────────────────────────────────────────
|
|
197
|
+
private fullURL(suffix: string): string {
|
|
198
|
+
return `${this.config.baseURL}${suffix}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private isAPIKeyAuth(): boolean {
|
|
202
|
+
return this.config.apiKey.length > 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ─── Resource type detection ──────────────────────────────────
|
|
206
|
+
private getResourceTypeById(resourceId: string): string {
|
|
207
|
+
if (resourceId.startsWith("ep-m-")) return resourceTypePresetEndpoint;
|
|
208
|
+
if (resourceId.startsWith("ep-")) return resourceTypeEndpoint;
|
|
209
|
+
if (resourceId.startsWith("bot-")) return resourceTypeBot;
|
|
210
|
+
return resourceTypePresetEndpoint;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ─── STS token management ────────────────────────────────────
|
|
214
|
+
private stsKey(resourceType: string, resourceId: string): string {
|
|
215
|
+
return `${resourceType}#${resourceId}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private needRefresh(
|
|
219
|
+
resourceType: string,
|
|
220
|
+
resourceId: string,
|
|
221
|
+
delta: number,
|
|
222
|
+
): boolean {
|
|
223
|
+
const info = this.resourceStsTokens.get(
|
|
224
|
+
this.stsKey(resourceType, resourceId),
|
|
225
|
+
);
|
|
226
|
+
if (!info) return true;
|
|
227
|
+
return info.expiredTime - Math.floor(Date.now() / 1000) < delta;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private async refreshToken(
|
|
231
|
+
resourceType: string,
|
|
232
|
+
resourceId: string,
|
|
233
|
+
projectName: string,
|
|
234
|
+
): Promise<void> {
|
|
235
|
+
if (!this.needRefresh(resourceType, resourceId, this.advisoryRefreshTimeout)) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Coalesce concurrent refreshes into a single promise
|
|
240
|
+
if (this.refreshPromise) {
|
|
241
|
+
await this.refreshPromise;
|
|
242
|
+
if (!this.needRefresh(resourceType, resourceId, this.advisoryRefreshTimeout)) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
this.refreshPromise = this.doRefresh(resourceType, resourceId, projectName);
|
|
248
|
+
try {
|
|
249
|
+
await this.refreshPromise;
|
|
250
|
+
} finally {
|
|
251
|
+
this.refreshPromise = null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private async doRefresh(
|
|
256
|
+
resourceType: string,
|
|
257
|
+
resourceId: string,
|
|
258
|
+
projectName: string,
|
|
259
|
+
): Promise<void> {
|
|
260
|
+
// Dynamic import to avoid circular deps at module load time
|
|
261
|
+
try {
|
|
262
|
+
const { ARKClient } = await import("@volcengine/ark");
|
|
263
|
+
const { GetApiKeyCommand } = await import("@volcengine/ark");
|
|
264
|
+
const arkClient = new ARKClient({
|
|
265
|
+
accessKeyId: this.config.ak,
|
|
266
|
+
secretAccessKey: this.config.sk,
|
|
267
|
+
region: this.config.region,
|
|
268
|
+
});
|
|
269
|
+
const input = {
|
|
270
|
+
DurationSeconds: DefaultStsTimeout,
|
|
271
|
+
ResourceIds: [resourceId],
|
|
272
|
+
ResourceType: resourceType,
|
|
273
|
+
...(projectName ? { ProjectName: projectName } : {}),
|
|
274
|
+
};
|
|
275
|
+
const resp = await arkClient.send(new GetApiKeyCommand(input));
|
|
276
|
+
const result = resp.Result;
|
|
277
|
+
if (result?.ApiKey && result?.ExpiredTime) {
|
|
278
|
+
this.resourceStsTokens.set(this.stsKey(resourceType, resourceId), {
|
|
279
|
+
token: result.ApiKey,
|
|
280
|
+
expiredTime: result.ExpiredTime,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
} catch (err) {
|
|
284
|
+
const isMandatory = this.needRefresh(
|
|
285
|
+
resourceType,
|
|
286
|
+
resourceId,
|
|
287
|
+
this.mandatoryRefreshTimeout,
|
|
288
|
+
);
|
|
289
|
+
if (isMandatory) throw err;
|
|
290
|
+
// Advisory refresh failure is silently ignored
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ─── Public STS token API ────────────────────────────────────
|
|
295
|
+
/** Get an STS token for the given endpoint ID (mirrors Go SDK). */
|
|
296
|
+
async getEndpointStsToken(endpointId: string): Promise<string> {
|
|
297
|
+
return this.getResourceStsToken(resourceTypeEndpoint, endpointId);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Get an STS token for an arbitrary resource (mirrors Go SDK). */
|
|
301
|
+
async getResourceStsToken(
|
|
302
|
+
resourceType: string,
|
|
303
|
+
resourceId: string,
|
|
304
|
+
projectName?: string,
|
|
305
|
+
): Promise<string> {
|
|
306
|
+
await this.refreshToken(resourceType, resourceId, projectName ?? "");
|
|
307
|
+
const info = this.resourceStsTokens.get(
|
|
308
|
+
this.stsKey(resourceType, resourceId),
|
|
309
|
+
);
|
|
310
|
+
return info?.token ?? "";
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ─── Auth header ──────────────────────────────────────────────
|
|
314
|
+
private async getAuthHeader(
|
|
315
|
+
resourceType: string,
|
|
316
|
+
resourceId: string,
|
|
317
|
+
projectName?: string,
|
|
318
|
+
): Promise<Record<string, string>> {
|
|
319
|
+
if (this.isAPIKeyAuth()) {
|
|
320
|
+
return { Authorization: `Bearer ${this.config.apiKey}` };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
resourceType = this.getResourceTypeById(resourceId);
|
|
324
|
+
await this.refreshToken(resourceType, resourceId, projectName ?? "");
|
|
325
|
+
const info = this.resourceStsTokens.get(
|
|
326
|
+
this.stsKey(resourceType, resourceId),
|
|
327
|
+
);
|
|
328
|
+
return { Authorization: `Bearer ${info?.token ?? ""}` };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ─── Core request execution ───────────────────────────────────
|
|
332
|
+
private retryPolicy(): RetryPolicy {
|
|
333
|
+
return {
|
|
334
|
+
maxAttempts: this.config.retryTimes,
|
|
335
|
+
initialBackoffMs: ErrorRetryBaseDelayMs,
|
|
336
|
+
maxBackoffMs: ErrorRetryMaxDelayMs,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private isRetryable(err: unknown): boolean {
|
|
341
|
+
if (err instanceof ArkAPIError) {
|
|
342
|
+
return (
|
|
343
|
+
err.httpStatusCode >= 500 || err.httpStatusCode === 429
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
if (err instanceof ArkRequestError) {
|
|
347
|
+
return err.httpStatusCode >= 500;
|
|
348
|
+
}
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private async executeRequest<T>(
|
|
353
|
+
method: string,
|
|
354
|
+
url: string,
|
|
355
|
+
resourceType: string,
|
|
356
|
+
resourceId: string,
|
|
357
|
+
opts?: RequestOptions & { body?: unknown; stream?: boolean },
|
|
358
|
+
): Promise<AxiosResponse<T>> {
|
|
359
|
+
const authHeader = await this.getAuthHeader(
|
|
360
|
+
resourceType,
|
|
361
|
+
resourceId,
|
|
362
|
+
opts?.projectName,
|
|
363
|
+
);
|
|
364
|
+
const requestId = genRequestId();
|
|
365
|
+
const headers: Record<string, string> = {
|
|
366
|
+
[ClientRequestHeader]: requestId,
|
|
367
|
+
"Content-Type": "application/json",
|
|
368
|
+
Accept: opts?.stream ? "text/event-stream" : "application/json",
|
|
369
|
+
...authHeader,
|
|
370
|
+
...opts?.customHeaders,
|
|
371
|
+
};
|
|
372
|
+
if (opts?.stream) {
|
|
373
|
+
headers["Cache-Control"] = "no-cache";
|
|
374
|
+
headers["Connection"] = "keep-alive";
|
|
375
|
+
}
|
|
376
|
+
if (opts?.projectName) {
|
|
377
|
+
headers["X-Project-Name"] = opts.projectName;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const axiosConfig: AxiosRequestConfig = {
|
|
381
|
+
method: method as any,
|
|
382
|
+
url,
|
|
383
|
+
headers,
|
|
384
|
+
data: opts?.body,
|
|
385
|
+
params: opts?.query,
|
|
386
|
+
signal: opts?.signal,
|
|
387
|
+
responseType: opts?.stream ? "stream" : "json",
|
|
388
|
+
validateStatus: () => true, // handle status codes ourselves
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const resp = await this.httpClient.request<T>(axiosConfig);
|
|
392
|
+
|
|
393
|
+
// Error handling
|
|
394
|
+
if (resp.status < 200 || resp.status >= 400) {
|
|
395
|
+
this.handleErrorResponse(resp, requestId);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return resp;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private handleErrorResponse(resp: AxiosResponse, requestId: string): never {
|
|
402
|
+
let errData: any;
|
|
403
|
+
try {
|
|
404
|
+
errData = typeof resp.data === "string" ? JSON.parse(resp.data) : resp.data;
|
|
405
|
+
} catch {
|
|
406
|
+
throw new ArkRequestError({
|
|
407
|
+
httpStatusCode: resp.status,
|
|
408
|
+
message: `Request failed with status ${resp.status}`,
|
|
409
|
+
requestId,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (errData?.error) {
|
|
414
|
+
throw new ArkAPIError({
|
|
415
|
+
message: errData.error.message ?? "Unknown error",
|
|
416
|
+
code: errData.error.code,
|
|
417
|
+
param: errData.error.param,
|
|
418
|
+
type: errData.error.type ?? "unknown",
|
|
419
|
+
httpStatusCode: resp.status,
|
|
420
|
+
requestId:
|
|
421
|
+
errData.error.request_id ??
|
|
422
|
+
(resp.headers?.[ClientRequestHeader.toLowerCase()] as string) ??
|
|
423
|
+
requestId,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
throw new ArkRequestError({
|
|
428
|
+
httpStatusCode: resp.status,
|
|
429
|
+
message: `Request failed with status ${resp.status}`,
|
|
430
|
+
requestId,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ─── E2EE encryption ─────────────────────────────────────────
|
|
435
|
+
private async getE2eeClient(resourceId: string, authorization: string): Promise<E2eeClient> {
|
|
436
|
+
// Check in-memory cache
|
|
437
|
+
const cached = this.e2eeManager.get(resourceId);
|
|
438
|
+
if (cached && (cached.isAICC === checkIsModeAICC())) {
|
|
439
|
+
return cached;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Singleflight: coalesce concurrent certificate fetches
|
|
443
|
+
const existing = this.e2eeFlightMap.get(resourceId);
|
|
444
|
+
if (existing) return existing;
|
|
445
|
+
|
|
446
|
+
const promise = this.doLoadE2eeClient(resourceId, authorization);
|
|
447
|
+
this.e2eeFlightMap.set(resourceId, promise);
|
|
448
|
+
try {
|
|
449
|
+
const client = await promise;
|
|
450
|
+
return client;
|
|
451
|
+
} finally {
|
|
452
|
+
this.e2eeFlightMap.delete(resourceId);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private async doLoadE2eeClient(resourceId: string, authorization: string): Promise<E2eeClient> {
|
|
457
|
+
// Re-check after acquiring flight
|
|
458
|
+
const cached = this.e2eeManager.get(resourceId);
|
|
459
|
+
if (cached && (cached.isAICC === checkIsModeAICC())) {
|
|
460
|
+
return cached;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Try local certificate cache
|
|
464
|
+
let certPem = loadLocalCertificate(resourceId);
|
|
465
|
+
if (!certPem) {
|
|
466
|
+
// Fetch from server
|
|
467
|
+
certPem = await this.loadServerCertificate(resourceId, authorization);
|
|
468
|
+
saveToLocalCertificate(resourceId, certPem);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const client = new E2eeClient(certPem);
|
|
472
|
+
this.e2eeManager.set(resourceId, client);
|
|
473
|
+
return client;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private async loadServerCertificate(resourceId: string, authorization: string): Promise<string> {
|
|
477
|
+
const url = this.fullURL(e2eGetCertificatePath);
|
|
478
|
+
const body: Record<string, string> = { model: resourceId };
|
|
479
|
+
if (checkIsModeAICC()) {
|
|
480
|
+
body.type = "AICCv0.1";
|
|
481
|
+
}
|
|
482
|
+
const resp = await this.httpClient.request<CertificateResponse>({
|
|
483
|
+
method: "POST",
|
|
484
|
+
url,
|
|
485
|
+
headers: {
|
|
486
|
+
"Content-Type": "application/json",
|
|
487
|
+
Accept: "application/json",
|
|
488
|
+
Authorization: authorization,
|
|
489
|
+
[ClientSessionTokenHeader]: e2eGetCertificatePath,
|
|
490
|
+
},
|
|
491
|
+
data: body,
|
|
492
|
+
validateStatus: () => true,
|
|
493
|
+
});
|
|
494
|
+
if (resp.status < 200 || resp.status >= 400) {
|
|
495
|
+
throw new Error(`getting Certificate failed: HTTP ${resp.status}`);
|
|
496
|
+
}
|
|
497
|
+
const cr = resp.data;
|
|
498
|
+
if (cr.error && Object.keys(cr.error).length > 0) {
|
|
499
|
+
throw new Error(`getting Certificate failed: ${JSON.stringify(cr.error)}`);
|
|
500
|
+
}
|
|
501
|
+
return cr.Certificate;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Encrypt a streaming chat completion request.
|
|
506
|
+
* Only called when x-is-encrypted header is "true" AND request is streaming.
|
|
507
|
+
* Matches Go's encryptRequest.
|
|
508
|
+
*/
|
|
509
|
+
private async encryptRequestBody(
|
|
510
|
+
resourceId: string,
|
|
511
|
+
requestId: string,
|
|
512
|
+
authorization: string,
|
|
513
|
+
request: ChatCompletionRequest,
|
|
514
|
+
): Promise<{ body: ChatCompletionRequest; extraHeaders: Record<string, string> }> {
|
|
515
|
+
const e2eeClient = await this.getE2eeClient(resourceId, authorization);
|
|
516
|
+
const [keyNonce, sessionToken] = e2eeClient.generateECIESKeyPair();
|
|
517
|
+
|
|
518
|
+
const extraHeaders: Record<string, string> = {
|
|
519
|
+
[ClientSessionTokenHeader]: sessionToken,
|
|
520
|
+
};
|
|
521
|
+
if (checkIsModeAICC()) {
|
|
522
|
+
extraHeaders[ClientEncryptInfoHeader] = e2eeClient.getEncryptInfo();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Store keyNonce for later stream decryption
|
|
526
|
+
this.keyNonceMap.set(requestId, keyNonce);
|
|
527
|
+
|
|
528
|
+
// Deep copy and encrypt
|
|
529
|
+
const requestCopy = deepCopyRequest(request);
|
|
530
|
+
encryptChatRequest(keyNonce, requestCopy);
|
|
531
|
+
|
|
532
|
+
return { body: requestCopy, extraHeaders };
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// ─── Responses file preprocessing ──────────────────────────────
|
|
536
|
+
private async preprocessResponseInput(input: ResponsesInput | undefined): Promise<void> {
|
|
537
|
+
if (!input || typeof input === "string" || !Array.isArray(input)) return;
|
|
538
|
+
await this.preprocessResponseMultiModal(input);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
private async preprocessResponseMultiModal(inputItems: InputItem[]): Promise<void> {
|
|
542
|
+
const tasks: Promise<void>[] = [];
|
|
543
|
+
|
|
544
|
+
for (const item of inputItems) {
|
|
545
|
+
if (!item || item.type !== "message" && !("role" in item && "content" in item)) continue;
|
|
546
|
+
const inputMessage = item as { content?: ContentItem[] };
|
|
547
|
+
if (!inputMessage.content || !Array.isArray(inputMessage.content)) continue;
|
|
548
|
+
|
|
549
|
+
for (const contentItem of inputMessage.content) {
|
|
550
|
+
const fileUrl = this.getMultiModalFileUrl(contentItem);
|
|
551
|
+
if (!fileUrl) continue;
|
|
552
|
+
|
|
553
|
+
tasks.push(this.preprocessResponseFile(contentItem, fileUrl));
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (tasks.length > 0) {
|
|
558
|
+
await Promise.all(tasks);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
private getMultiModalFileUrl(contentItem: ContentItem): string | null {
|
|
563
|
+
if (contentItem.type === "input_video") {
|
|
564
|
+
const video = contentItem as ContentItemVideo;
|
|
565
|
+
if (video.video_url) return video.video_url;
|
|
566
|
+
} else if (contentItem.type === "input_image") {
|
|
567
|
+
const image = contentItem as ContentItemImage;
|
|
568
|
+
if (image.image_url) return image.image_url;
|
|
569
|
+
} else if (contentItem.type === "input_file") {
|
|
570
|
+
const file = contentItem as ContentItemFile;
|
|
571
|
+
if (file.file_url) return file.file_url;
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private parseFileUrl(rawUrl: string): string | null {
|
|
577
|
+
try {
|
|
578
|
+
const parsed = new URL(rawUrl);
|
|
579
|
+
if (parsed.protocol !== `${FILE_SCHEME}:`) return null;
|
|
580
|
+
return parsed.pathname;
|
|
581
|
+
} catch {
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private async preprocessResponseFile(contentItem: ContentItem, rawUrl: string): Promise<void> {
|
|
587
|
+
const localPath = this.parseFileUrl(rawUrl);
|
|
588
|
+
if (!localPath) return;
|
|
589
|
+
|
|
590
|
+
const fileStream = fs.createReadStream(localPath);
|
|
591
|
+
try {
|
|
592
|
+
// Upload file
|
|
593
|
+
let fileMeta = await this.uploadFile({
|
|
594
|
+
file: fileStream as any,
|
|
595
|
+
purpose: PurposeUserData,
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// Poll until processed (with timeout)
|
|
599
|
+
const deadline = Date.now() + MAX_WAIT_TIME_MS;
|
|
600
|
+
while (fileMeta.status === FileStatusProcessing && Date.now() < deadline) {
|
|
601
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
602
|
+
fileMeta = await this.retrieveFile(fileMeta.id);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Replace URL with file_id
|
|
606
|
+
if (contentItem.type === "input_video") {
|
|
607
|
+
const video = contentItem as ContentItemVideo;
|
|
608
|
+
video.video_url = "";
|
|
609
|
+
video.file_id = fileMeta.id;
|
|
610
|
+
} else if (contentItem.type === "input_image") {
|
|
611
|
+
const image = contentItem as ContentItemImage;
|
|
612
|
+
delete image.image_url;
|
|
613
|
+
image.file_id = fileMeta.id;
|
|
614
|
+
} else if (contentItem.type === "input_file") {
|
|
615
|
+
const file = contentItem as ContentItemFile;
|
|
616
|
+
delete file.file_url;
|
|
617
|
+
file.file_id = fileMeta.id;
|
|
618
|
+
}
|
|
619
|
+
} finally {
|
|
620
|
+
fileStream.destroy();
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
private async doRequest<T>(
|
|
625
|
+
method: string,
|
|
626
|
+
url: string,
|
|
627
|
+
resourceType: string,
|
|
628
|
+
resourceId: string,
|
|
629
|
+
opts?: RequestOptions & { body?: unknown },
|
|
630
|
+
): Promise<T> {
|
|
631
|
+
return retry(
|
|
632
|
+
this.retryPolicy(),
|
|
633
|
+
async () => {
|
|
634
|
+
const resp = await this.executeRequest<T>(
|
|
635
|
+
method,
|
|
636
|
+
url,
|
|
637
|
+
resourceType,
|
|
638
|
+
resourceId,
|
|
639
|
+
opts,
|
|
640
|
+
);
|
|
641
|
+
return resp.data;
|
|
642
|
+
},
|
|
643
|
+
(err) => this.isRetryable(err),
|
|
644
|
+
opts?.signal,
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
private async doStreamRequest(
|
|
649
|
+
method: string,
|
|
650
|
+
url: string,
|
|
651
|
+
resourceType: string,
|
|
652
|
+
resourceId: string,
|
|
653
|
+
opts?: RequestOptions & { body?: unknown },
|
|
654
|
+
): Promise<{ stream: NodeJS.ReadableStream; headers: Record<string, string> }> {
|
|
655
|
+
const resp = await retry(
|
|
656
|
+
this.retryPolicy(),
|
|
657
|
+
async () => {
|
|
658
|
+
return this.executeRequest<NodeJS.ReadableStream>(
|
|
659
|
+
method,
|
|
660
|
+
url,
|
|
661
|
+
resourceType,
|
|
662
|
+
resourceId,
|
|
663
|
+
{ ...opts, stream: true },
|
|
664
|
+
);
|
|
665
|
+
},
|
|
666
|
+
(err) => this.isRetryable(err),
|
|
667
|
+
opts?.signal,
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
const headers: Record<string, string> = {};
|
|
671
|
+
if (resp.headers) {
|
|
672
|
+
for (const [k, v] of Object.entries(resp.headers)) {
|
|
673
|
+
if (typeof v === "string") headers[k] = v;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return { stream: resp.data, headers };
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// ═══════════════════════════════════════════════════════════════
|
|
680
|
+
// PUBLIC API METHODS
|
|
681
|
+
// ═══════════════════════════════════════════════════════════════
|
|
682
|
+
|
|
683
|
+
// ─── Chat Completions ─────────────────────────────────────────
|
|
684
|
+
async createChatCompletion(
|
|
685
|
+
request: ChatCompletionRequest,
|
|
686
|
+
opts?: RequestOptions,
|
|
687
|
+
): Promise<ChatCompletionResponse> {
|
|
688
|
+
if (request.stream) throw ErrChatCompletionStreamNotSupported;
|
|
689
|
+
const resp = await this.doRequest<ChatCompletionResponse>(
|
|
690
|
+
"POST",
|
|
691
|
+
this.fullURL(chatCompletionsSuffix),
|
|
692
|
+
resourceTypeEndpoint,
|
|
693
|
+
request.model,
|
|
694
|
+
{ ...opts, body: request },
|
|
695
|
+
);
|
|
696
|
+
return normalizeChatCompletionResponse(resp);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
async createChatCompletionStream(
|
|
700
|
+
request: ChatCompletionRequest,
|
|
701
|
+
opts?: RequestOptions,
|
|
702
|
+
): Promise<ChatCompletionStreamReader> {
|
|
703
|
+
let body: ChatCompletionRequest = { ...request, stream: true };
|
|
704
|
+
let extraHeaders: Record<string, string> | undefined;
|
|
705
|
+
|
|
706
|
+
// E2EE: encrypt if x-is-encrypted header is set and request is streaming
|
|
707
|
+
const isEncrypted = opts?.customHeaders?.[ClientIsEncryptedHeader] === "true";
|
|
708
|
+
const requestId = genRequestId();
|
|
709
|
+
if (isEncrypted) {
|
|
710
|
+
const authHeader = await this.getAuthHeader(
|
|
711
|
+
resourceTypeEndpoint,
|
|
712
|
+
request.model,
|
|
713
|
+
opts?.projectName,
|
|
714
|
+
);
|
|
715
|
+
const result = await this.encryptRequestBody(
|
|
716
|
+
request.model,
|
|
717
|
+
requestId,
|
|
718
|
+
authHeader.Authorization,
|
|
719
|
+
body,
|
|
720
|
+
);
|
|
721
|
+
body = result.body;
|
|
722
|
+
extraHeaders = result.extraHeaders;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const mergedOpts = {
|
|
726
|
+
...opts,
|
|
727
|
+
body,
|
|
728
|
+
customHeaders: { ...opts?.customHeaders, ...extraHeaders },
|
|
729
|
+
};
|
|
730
|
+
const { stream, headers } = await this.doStreamRequest(
|
|
731
|
+
"POST",
|
|
732
|
+
this.fullURL(chatCompletionsSuffix),
|
|
733
|
+
resourceTypeEndpoint,
|
|
734
|
+
request.model,
|
|
735
|
+
mergedOpts,
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
// Retrieve keyNonce for stream decryption (use local requestId to match encryptRequestBody's store key)
|
|
739
|
+
const keyNonce = this.keyNonceMap.get(requestId);
|
|
740
|
+
const cleanup = keyNonce
|
|
741
|
+
? () => { this.keyNonceMap.delete(requestId); }
|
|
742
|
+
: undefined;
|
|
743
|
+
|
|
744
|
+
return new ChatCompletionStreamReader(
|
|
745
|
+
stream,
|
|
746
|
+
this.config.emptyMessagesLimit,
|
|
747
|
+
headers,
|
|
748
|
+
keyNonce,
|
|
749
|
+
cleanup,
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// ─── Bot Chat Completions ─────────────────────────────────────
|
|
754
|
+
async createBotChatCompletion(
|
|
755
|
+
request: BotChatCompletionRequest,
|
|
756
|
+
opts?: RequestOptions,
|
|
757
|
+
): Promise<BotChatCompletionResponse> {
|
|
758
|
+
if (request.stream) throw ErrChatCompletionStreamNotSupported;
|
|
759
|
+
const model = request.bot_id || request.model;
|
|
760
|
+
const resp = await this.doRequest<BotChatCompletionResponse>(
|
|
761
|
+
"POST",
|
|
762
|
+
this.fullURL(botChatCompletionsSuffix),
|
|
763
|
+
resourceTypeBot,
|
|
764
|
+
model,
|
|
765
|
+
{ ...opts, body: { ...request, model } },
|
|
766
|
+
);
|
|
767
|
+
return normalizeChatCompletionResponse(resp as any) as any;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
async createBotChatCompletionStream(
|
|
771
|
+
request: BotChatCompletionRequest,
|
|
772
|
+
opts?: RequestOptions,
|
|
773
|
+
): Promise<BotChatCompletionStreamReader> {
|
|
774
|
+
const model = request.bot_id || request.model;
|
|
775
|
+
const body = { ...request, model, stream: true };
|
|
776
|
+
const { stream, headers } = await this.doStreamRequest(
|
|
777
|
+
"POST",
|
|
778
|
+
this.fullURL(botChatCompletionsSuffix),
|
|
779
|
+
resourceTypeBot,
|
|
780
|
+
model,
|
|
781
|
+
{ ...opts, body },
|
|
782
|
+
);
|
|
783
|
+
return new BotChatCompletionStreamReader(
|
|
784
|
+
stream,
|
|
785
|
+
this.config.emptyMessagesLimit,
|
|
786
|
+
headers,
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// ─── Embeddings ───────────────────────────────────────────────
|
|
791
|
+
async createEmbeddings(
|
|
792
|
+
request: EmbeddingRequest,
|
|
793
|
+
opts?: RequestOptions,
|
|
794
|
+
): Promise<EmbeddingResponse> {
|
|
795
|
+
const resp = await this.doRequest<EmbeddingResponse>(
|
|
796
|
+
"POST",
|
|
797
|
+
this.fullURL(embeddingsSuffix),
|
|
798
|
+
resourceTypeEndpoint,
|
|
799
|
+
request.model,
|
|
800
|
+
{ ...opts, body: request },
|
|
801
|
+
);
|
|
802
|
+
return normalizeEmbeddingResponse(resp);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
async createMultiModalEmbeddings(
|
|
806
|
+
request: MultiModalEmbeddingRequest,
|
|
807
|
+
opts?: RequestOptions,
|
|
808
|
+
): Promise<MultimodalEmbeddingResponse> {
|
|
809
|
+
return this.doRequest<MultimodalEmbeddingResponse>(
|
|
810
|
+
"POST",
|
|
811
|
+
this.fullURL(multimodalEmbeddingsSuffix),
|
|
812
|
+
resourceTypeEndpoint,
|
|
813
|
+
request.model,
|
|
814
|
+
{ ...opts, body: request },
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// ─── Images ───────────────────────────────────────────────────
|
|
819
|
+
async generateImages(
|
|
820
|
+
request: GenerateImagesRequest,
|
|
821
|
+
opts?: RequestOptions,
|
|
822
|
+
): Promise<ImagesResponse> {
|
|
823
|
+
if (!this.isAPIKeyAuth()) throw ErrAKSKNotSupported;
|
|
824
|
+
return this.doRequest<ImagesResponse>(
|
|
825
|
+
"POST",
|
|
826
|
+
this.fullURL(generateImagesPath),
|
|
827
|
+
resourceTypeEndpoint,
|
|
828
|
+
request.model,
|
|
829
|
+
{ ...opts, body: request },
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
async generateImagesStream(
|
|
834
|
+
request: GenerateImagesRequest,
|
|
835
|
+
opts?: RequestOptions,
|
|
836
|
+
): Promise<ImageGenerationStreamReader> {
|
|
837
|
+
if (!this.isAPIKeyAuth()) throw ErrAKSKNotSupported;
|
|
838
|
+
const body = { ...request, stream: true };
|
|
839
|
+
const { stream, headers } = await this.doStreamRequest(
|
|
840
|
+
"POST",
|
|
841
|
+
this.fullURL(generateImagesPath),
|
|
842
|
+
resourceTypeEndpoint,
|
|
843
|
+
request.model,
|
|
844
|
+
{ ...opts, body },
|
|
845
|
+
);
|
|
846
|
+
return new ImageGenerationStreamReader(
|
|
847
|
+
stream,
|
|
848
|
+
this.config.emptyMessagesLimit,
|
|
849
|
+
headers,
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// ─── Tokenization ─────────────────────────────────────────────
|
|
854
|
+
async createTokenization(
|
|
855
|
+
request: TokenizationRequest,
|
|
856
|
+
opts?: RequestOptions,
|
|
857
|
+
): Promise<TokenizationResponse> {
|
|
858
|
+
return this.doRequest<TokenizationResponse>(
|
|
859
|
+
"POST",
|
|
860
|
+
this.fullURL(tokenizationSuffix),
|
|
861
|
+
resourceTypeEndpoint,
|
|
862
|
+
request.model,
|
|
863
|
+
{ ...opts, body: request },
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// ─── Context ──────────────────────────────────────────────────
|
|
868
|
+
async createContext(
|
|
869
|
+
request: CreateContextRequest,
|
|
870
|
+
opts?: RequestOptions,
|
|
871
|
+
): Promise<CreateContextResponse> {
|
|
872
|
+
return this.doRequest<CreateContextResponse>(
|
|
873
|
+
"POST",
|
|
874
|
+
this.fullURL(contextCreateSuffix),
|
|
875
|
+
resourceTypeEndpoint,
|
|
876
|
+
request.model,
|
|
877
|
+
{ ...opts, body: request },
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
async createContextChatCompletion(
|
|
882
|
+
request: ContextChatCompletionRequest,
|
|
883
|
+
opts?: RequestOptions,
|
|
884
|
+
): Promise<ChatCompletionResponse> {
|
|
885
|
+
if (request.stream) throw ErrChatCompletionStreamNotSupported;
|
|
886
|
+
const resp = await this.doRequest<ChatCompletionResponse>(
|
|
887
|
+
"POST",
|
|
888
|
+
this.fullURL(contextChatSuffix),
|
|
889
|
+
resourceTypeEndpoint,
|
|
890
|
+
request.model,
|
|
891
|
+
{ ...opts, body: request },
|
|
892
|
+
);
|
|
893
|
+
return normalizeChatCompletionResponse(resp);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
async createContextChatCompletionStream(
|
|
897
|
+
request: ContextChatCompletionRequest,
|
|
898
|
+
opts?: RequestOptions,
|
|
899
|
+
): Promise<ChatCompletionStreamReader> {
|
|
900
|
+
const body = { ...request, stream: true };
|
|
901
|
+
const { stream, headers } = await this.doStreamRequest(
|
|
902
|
+
"POST",
|
|
903
|
+
this.fullURL(contextChatSuffix),
|
|
904
|
+
resourceTypeEndpoint,
|
|
905
|
+
request.model,
|
|
906
|
+
{ ...opts, body },
|
|
907
|
+
);
|
|
908
|
+
return new ChatCompletionStreamReader(
|
|
909
|
+
stream,
|
|
910
|
+
this.config.emptyMessagesLimit,
|
|
911
|
+
headers,
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// ─── Content Generation ───────────────────────────────────────
|
|
916
|
+
async createContentGenerationTask(
|
|
917
|
+
request: CreateContentGenerationTaskRequest,
|
|
918
|
+
opts?: RequestOptions,
|
|
919
|
+
): Promise<CreateContentGenerationTaskResponse> {
|
|
920
|
+
if (!this.isAPIKeyAuth()) throw ErrAKSKNotSupported;
|
|
921
|
+
return this.doRequest<CreateContentGenerationTaskResponse>(
|
|
922
|
+
"POST",
|
|
923
|
+
this.fullURL(contentGenerationTaskPath),
|
|
924
|
+
resourceTypeEndpoint,
|
|
925
|
+
request.model,
|
|
926
|
+
{ ...opts, body: request },
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
async getContentGenerationTask(
|
|
931
|
+
taskId: string,
|
|
932
|
+
opts?: RequestOptions,
|
|
933
|
+
): Promise<GetContentGenerationTaskResponse> {
|
|
934
|
+
if (!this.isAPIKeyAuth()) throw ErrAKSKNotSupported;
|
|
935
|
+
return this.doRequest<GetContentGenerationTaskResponse>(
|
|
936
|
+
"GET",
|
|
937
|
+
`${this.fullURL(contentGenerationTaskPath)}/${taskId}`,
|
|
938
|
+
resourceTypeEndpoint,
|
|
939
|
+
"",
|
|
940
|
+
opts,
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
async deleteContentGenerationTask(
|
|
945
|
+
taskId: string,
|
|
946
|
+
opts?: RequestOptions,
|
|
947
|
+
): Promise<void> {
|
|
948
|
+
if (!this.isAPIKeyAuth()) throw ErrAKSKNotSupported;
|
|
949
|
+
await this.doRequest<void>(
|
|
950
|
+
"DELETE",
|
|
951
|
+
`${this.fullURL(contentGenerationTaskPath)}/${taskId}`,
|
|
952
|
+
resourceTypeEndpoint,
|
|
953
|
+
"",
|
|
954
|
+
opts,
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
async listContentGenerationTasks(
|
|
959
|
+
request: ListContentGenerationTasksRequest,
|
|
960
|
+
opts?: RequestOptions,
|
|
961
|
+
): Promise<ListContentGenerationTasksResponse> {
|
|
962
|
+
if (!this.isAPIKeyAuth()) throw ErrAKSKNotSupported;
|
|
963
|
+
const query: Record<string, string | string[]> = {};
|
|
964
|
+
if (request.page_num != null) query.page_num = String(request.page_num);
|
|
965
|
+
if (request.page_size != null) query.page_size = String(request.page_size);
|
|
966
|
+
if (request.filter) {
|
|
967
|
+
if (request.filter.status) query["filter.status"] = request.filter.status;
|
|
968
|
+
if (request.filter.model) query["filter.model"] = request.filter.model;
|
|
969
|
+
if (request.filter.service_tier)
|
|
970
|
+
query["filter.service_tier"] = request.filter.service_tier;
|
|
971
|
+
if (request.filter.task_ids?.length) {
|
|
972
|
+
query["filter.task_ids"] = request.filter.task_ids;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return this.doRequest<ListContentGenerationTasksResponse>(
|
|
976
|
+
"GET",
|
|
977
|
+
this.fullURL(contentGenerationTaskPath),
|
|
978
|
+
resourceTypeEndpoint,
|
|
979
|
+
"",
|
|
980
|
+
{ ...opts, query: query as any },
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// ─── Responses ────────────────────────────────────────────────
|
|
985
|
+
async createResponses(
|
|
986
|
+
body: ResponsesRequest,
|
|
987
|
+
opts?: RequestOptions,
|
|
988
|
+
): Promise<Record<string, unknown>> {
|
|
989
|
+
// Preprocess input multi modal files (matches Go)
|
|
990
|
+
await this.preprocessResponseInput(body.input);
|
|
991
|
+
return this.doRequest<Record<string, unknown>>(
|
|
992
|
+
"POST",
|
|
993
|
+
this.fullURL("/responses"),
|
|
994
|
+
resourceTypeEndpoint,
|
|
995
|
+
body.model,
|
|
996
|
+
{ ...opts, body },
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
async createResponsesStream(
|
|
1001
|
+
body: ResponsesRequest,
|
|
1002
|
+
opts?: RequestOptions,
|
|
1003
|
+
): Promise<ResponsesStreamReader> {
|
|
1004
|
+
// Preprocess input multi modal files (matches Go)
|
|
1005
|
+
await this.preprocessResponseInput(body.input);
|
|
1006
|
+
const streamBody = { ...body, stream: true };
|
|
1007
|
+
const { stream, headers } = await this.doStreamRequest(
|
|
1008
|
+
"POST",
|
|
1009
|
+
this.fullURL("/responses"),
|
|
1010
|
+
resourceTypeEndpoint,
|
|
1011
|
+
body.model,
|
|
1012
|
+
{ ...opts, body: streamBody },
|
|
1013
|
+
);
|
|
1014
|
+
return new ResponsesStreamReader(
|
|
1015
|
+
stream,
|
|
1016
|
+
this.config.emptyMessagesLimit,
|
|
1017
|
+
headers,
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
async getResponses(
|
|
1022
|
+
responseId: string,
|
|
1023
|
+
query?: Record<string, unknown>,
|
|
1024
|
+
opts?: RequestOptions,
|
|
1025
|
+
): Promise<Record<string, unknown>> {
|
|
1026
|
+
if (!responseId) throw new Error("missing required response_id parameter");
|
|
1027
|
+
return this.doRequest<Record<string, unknown>>(
|
|
1028
|
+
"GET",
|
|
1029
|
+
this.fullURL(`/responses/${responseId}`),
|
|
1030
|
+
"",
|
|
1031
|
+
"",
|
|
1032
|
+
{ ...opts, query: query as any },
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
async deleteResponse(
|
|
1037
|
+
responseId: string,
|
|
1038
|
+
opts?: RequestOptions,
|
|
1039
|
+
): Promise<void> {
|
|
1040
|
+
if (!responseId) throw new Error("missing required response_id parameter");
|
|
1041
|
+
await this.doRequest<void>(
|
|
1042
|
+
"DELETE",
|
|
1043
|
+
this.fullURL(`/responses/${responseId}`),
|
|
1044
|
+
"",
|
|
1045
|
+
"",
|
|
1046
|
+
opts,
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
async listResponseInputItems(
|
|
1051
|
+
responseId: string,
|
|
1052
|
+
query?: Record<string, unknown>,
|
|
1053
|
+
opts?: RequestOptions,
|
|
1054
|
+
): Promise<Record<string, unknown>> {
|
|
1055
|
+
if (!responseId) throw new Error("missing required response_id parameter");
|
|
1056
|
+
return this.doRequest<Record<string, unknown>>(
|
|
1057
|
+
"GET",
|
|
1058
|
+
this.fullURL(`/responses/${responseId}/input_items`),
|
|
1059
|
+
"",
|
|
1060
|
+
"",
|
|
1061
|
+
{ ...opts, query: query as any },
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// ─── Files ────────────────────────────────────────────────────
|
|
1066
|
+
async uploadFile(
|
|
1067
|
+
request: UploadFileRequest,
|
|
1068
|
+
opts?: RequestOptions,
|
|
1069
|
+
): Promise<FileMeta> {
|
|
1070
|
+
// File upload uses FormData, not JSON
|
|
1071
|
+
const formData = new FormData();
|
|
1072
|
+
formData.append("file", request.file as any);
|
|
1073
|
+
formData.append("purpose", request.purpose);
|
|
1074
|
+
if (request.preprocess_configs) {
|
|
1075
|
+
formData.append(
|
|
1076
|
+
"preprocess_configs",
|
|
1077
|
+
JSON.stringify(request.preprocess_configs),
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
if (request.expire_at != null) {
|
|
1081
|
+
formData.append("expire_at", String(request.expire_at));
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
const authHeader = await this.getAuthHeader("", "");
|
|
1085
|
+
const requestId = genRequestId();
|
|
1086
|
+
const resp = await this.httpClient.request<FileMeta>({
|
|
1087
|
+
method: "POST",
|
|
1088
|
+
url: this.fullURL(filePrefix),
|
|
1089
|
+
headers: {
|
|
1090
|
+
[ClientRequestHeader]: requestId,
|
|
1091
|
+
...authHeader,
|
|
1092
|
+
...opts?.customHeaders,
|
|
1093
|
+
},
|
|
1094
|
+
data: formData,
|
|
1095
|
+
signal: opts?.signal,
|
|
1096
|
+
validateStatus: () => true,
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
if (resp.status < 200 || resp.status >= 400) {
|
|
1100
|
+
this.handleErrorResponse(resp, requestId);
|
|
1101
|
+
}
|
|
1102
|
+
return resp.data;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
async retrieveFile(
|
|
1106
|
+
fileId: string,
|
|
1107
|
+
opts?: RequestOptions,
|
|
1108
|
+
): Promise<FileMeta> {
|
|
1109
|
+
if (!fileId) throw new Error("missing required file_id parameter");
|
|
1110
|
+
return this.doRequest<FileMeta>(
|
|
1111
|
+
"GET",
|
|
1112
|
+
this.fullURL(`${filePrefix}/${fileId}`),
|
|
1113
|
+
"",
|
|
1114
|
+
"",
|
|
1115
|
+
opts,
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
async listFiles(
|
|
1120
|
+
request?: ListFilesRequest,
|
|
1121
|
+
opts?: RequestOptions,
|
|
1122
|
+
): Promise<ListFilesResponse> {
|
|
1123
|
+
const query: Record<string, string> = {};
|
|
1124
|
+
if (request?.purpose) query.purpose = request.purpose;
|
|
1125
|
+
if (request?.after) query.after = request.after;
|
|
1126
|
+
if (request?.limit != null) query.limit = String(request.limit);
|
|
1127
|
+
if (request?.order) query.order = request.order;
|
|
1128
|
+
return this.doRequest<ListFilesResponse>(
|
|
1129
|
+
"GET",
|
|
1130
|
+
this.fullURL(filePrefix),
|
|
1131
|
+
"",
|
|
1132
|
+
"",
|
|
1133
|
+
{ ...opts, query },
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
async deleteFile(
|
|
1138
|
+
fileId: string,
|
|
1139
|
+
opts?: RequestOptions,
|
|
1140
|
+
): Promise<DeleteFileResponse> {
|
|
1141
|
+
if (!fileId) throw new Error("missing required file_id parameter");
|
|
1142
|
+
return this.doRequest<DeleteFileResponse>(
|
|
1143
|
+
"DELETE",
|
|
1144
|
+
this.fullURL(`${filePrefix}/${fileId}`),
|
|
1145
|
+
"",
|
|
1146
|
+
"",
|
|
1147
|
+
opts,
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// ─── Batch ────────────────────────────────────────────────────
|
|
1152
|
+
async createBatchChatCompletion(
|
|
1153
|
+
request: ChatCompletionRequest,
|
|
1154
|
+
opts?: RequestOptions,
|
|
1155
|
+
): Promise<ChatCompletionResponse> {
|
|
1156
|
+
if (request.stream) throw ErrChatCompletionStreamNotSupported;
|
|
1157
|
+
const breaker = this.modelBreakerProvider.getOrCreate(request.model);
|
|
1158
|
+
await breaker.wait();
|
|
1159
|
+
const resp = await this.doRequest<ChatCompletionResponse>(
|
|
1160
|
+
"POST",
|
|
1161
|
+
this.fullURL(batchChatCompletionsSuffix),
|
|
1162
|
+
resourceTypeEndpoint,
|
|
1163
|
+
request.model,
|
|
1164
|
+
{ ...opts, body: request },
|
|
1165
|
+
);
|
|
1166
|
+
return normalizeChatCompletionResponse(resp);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
async createBatchEmbeddings(
|
|
1170
|
+
request: EmbeddingRequest,
|
|
1171
|
+
opts?: RequestOptions,
|
|
1172
|
+
): Promise<EmbeddingResponse> {
|
|
1173
|
+
const breaker = this.modelBreakerProvider.getOrCreate(request.model);
|
|
1174
|
+
await breaker.wait();
|
|
1175
|
+
const resp = await this.doRequest<EmbeddingResponse>(
|
|
1176
|
+
"POST",
|
|
1177
|
+
this.fullURL(batchEmbeddingsSuffix),
|
|
1178
|
+
resourceTypeEndpoint,
|
|
1179
|
+
request.model,
|
|
1180
|
+
{ ...opts, body: request },
|
|
1181
|
+
);
|
|
1182
|
+
return normalizeEmbeddingResponse(resp);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
async createBatchMultiModalEmbeddings(
|
|
1186
|
+
request: MultiModalEmbeddingRequest,
|
|
1187
|
+
opts?: RequestOptions,
|
|
1188
|
+
): Promise<MultimodalEmbeddingResponse> {
|
|
1189
|
+
const breaker = this.modelBreakerProvider.getOrCreate(request.model);
|
|
1190
|
+
await breaker.wait();
|
|
1191
|
+
return this.doRequest<MultimodalEmbeddingResponse>(
|
|
1192
|
+
"POST",
|
|
1193
|
+
this.fullURL(batchMultiModalEmbeddingsSuffix),
|
|
1194
|
+
resourceTypeEndpoint,
|
|
1195
|
+
request.model,
|
|
1196
|
+
{ ...opts, body: request },
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
}
|