@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.
Files changed (123) hide show
  1. package/LICENSE.txt +202 -0
  2. package/README.md +104 -0
  3. package/dist/cjs/index.js +1717 -0
  4. package/dist/esm/client.d.ts +97 -0
  5. package/dist/esm/client.d.ts.map +1 -0
  6. package/dist/esm/config.d.ts +46 -0
  7. package/dist/esm/config.d.ts.map +1 -0
  8. package/dist/esm/encryption/encrypt-chat.d.ts +24 -0
  9. package/dist/esm/encryption/encrypt-chat.d.ts.map +1 -0
  10. package/dist/esm/encryption/index.d.ts +4 -0
  11. package/dist/esm/encryption/index.d.ts.map +1 -0
  12. package/dist/esm/encryption/key-agreement.d.ts +73 -0
  13. package/dist/esm/encryption/key-agreement.d.ts.map +1 -0
  14. package/dist/esm/index.d.ts +11 -0
  15. package/dist/esm/index.d.ts.map +1 -0
  16. package/dist/esm/index.mjs +1476 -0
  17. package/dist/esm/rslib-runtime.mjs +37 -0
  18. package/dist/esm/types/bot.d.ts +109 -0
  19. package/dist/esm/types/bot.d.ts.map +1 -0
  20. package/dist/esm/types/chat-completion.d.ts +167 -0
  21. package/dist/esm/types/chat-completion.d.ts.map +1 -0
  22. package/dist/esm/types/common.d.ts +29 -0
  23. package/dist/esm/types/common.d.ts.map +1 -0
  24. package/dist/esm/types/content-generation.d.ts +118 -0
  25. package/dist/esm/types/content-generation.d.ts.map +1 -0
  26. package/dist/esm/types/context.d.ts +49 -0
  27. package/dist/esm/types/context.d.ts.map +1 -0
  28. package/dist/esm/types/embeddings.d.ts +44 -0
  29. package/dist/esm/types/embeddings.d.ts.map +1 -0
  30. package/dist/esm/types/error.d.ts +45 -0
  31. package/dist/esm/types/error.d.ts.map +1 -0
  32. package/dist/esm/types/file.d.ts +66 -0
  33. package/dist/esm/types/file.d.ts.map +1 -0
  34. package/dist/esm/types/http-request-error.d.ts +13 -0
  35. package/dist/esm/types/http-request-error.d.ts.map +1 -0
  36. package/dist/esm/types/images.d.ts +78 -0
  37. package/dist/esm/types/images.d.ts.map +1 -0
  38. package/dist/esm/types/index.d.ts +13 -0
  39. package/dist/esm/types/index.d.ts.map +1 -0
  40. package/dist/esm/types/multimodal-embedding.d.ts +56 -0
  41. package/dist/esm/types/multimodal-embedding.d.ts.map +1 -0
  42. package/dist/esm/types/responses/enums.d.ts +38 -0
  43. package/dist/esm/types/responses/enums.d.ts.map +1 -0
  44. package/dist/esm/types/responses/helpers.d.ts +22 -0
  45. package/dist/esm/types/responses/helpers.d.ts.map +1 -0
  46. package/dist/esm/types/responses/index.d.ts +4 -0
  47. package/dist/esm/types/responses/index.d.ts.map +1 -0
  48. package/dist/esm/types/responses/types.d.ts +906 -0
  49. package/dist/esm/types/responses/types.d.ts.map +1 -0
  50. package/dist/esm/types/tokenization.d.ts +22 -0
  51. package/dist/esm/types/tokenization.d.ts.map +1 -0
  52. package/dist/esm/utils/breaker-provider.d.ts +9 -0
  53. package/dist/esm/utils/breaker-provider.d.ts.map +1 -0
  54. package/dist/esm/utils/breaker.d.ts +28 -0
  55. package/dist/esm/utils/breaker.d.ts.map +1 -0
  56. package/dist/esm/utils/normalize.d.ts +51 -0
  57. package/dist/esm/utils/normalize.d.ts.map +1 -0
  58. package/dist/esm/utils/request-builder.d.ts +15 -0
  59. package/dist/esm/utils/request-builder.d.ts.map +1 -0
  60. package/dist/esm/utils/request-id.d.ts +5 -0
  61. package/dist/esm/utils/request-id.d.ts.map +1 -0
  62. package/dist/esm/utils/retry.d.ts +11 -0
  63. package/dist/esm/utils/retry.d.ts.map +1 -0
  64. package/dist/esm/utils/sse-decoder.d.ts +23 -0
  65. package/dist/esm/utils/sse-decoder.d.ts.map +1 -0
  66. package/dist/esm/utils/stream-reader.d.ts +67 -0
  67. package/dist/esm/utils/stream-reader.d.ts.map +1 -0
  68. package/dist/tsconfig.tsbuildinfo +1 -0
  69. package/example/README.md +118 -0
  70. package/example/batch-chat.ts +64 -0
  71. package/example/bot-chat.ts +66 -0
  72. package/example/chat-completion-function-call.ts +141 -0
  73. package/example/chat-completion-reasoning.ts +64 -0
  74. package/example/chat-completion-vision.ts +70 -0
  75. package/example/chat-completion.ts +62 -0
  76. package/example/content-generation.ts +70 -0
  77. package/example/context.ts +69 -0
  78. package/example/embeddings.ts +31 -0
  79. package/example/file-upload.ts +53 -0
  80. package/example/images.ts +74 -0
  81. package/example/list-input-items.ts +34 -0
  82. package/example/multimodal-embeddings.ts +36 -0
  83. package/example/responses/basic.ts +75 -0
  84. package/example/responses/doubao-app.ts +53 -0
  85. package/example/responses/mcp.ts +66 -0
  86. package/example/responses/streaming.ts +45 -0
  87. package/example/responses/video.ts +74 -0
  88. package/example/responses/web-search.ts +52 -0
  89. package/example/structured-outputs.ts +71 -0
  90. package/example/tokenization.ts +30 -0
  91. package/package.json +47 -0
  92. package/src/client.ts +1199 -0
  93. package/src/config.ts +68 -0
  94. package/src/encryption/encrypt-chat.ts +146 -0
  95. package/src/encryption/index.ts +21 -0
  96. package/src/encryption/key-agreement.ts +270 -0
  97. package/src/index.ts +10 -0
  98. package/src/types/ark.d.ts +9 -0
  99. package/src/types/bot.ts +127 -0
  100. package/src/types/chat-completion.ts +228 -0
  101. package/src/types/common.ts +37 -0
  102. package/src/types/content-generation.ts +135 -0
  103. package/src/types/context.ts +59 -0
  104. package/src/types/embeddings.ts +74 -0
  105. package/src/types/error.ts +93 -0
  106. package/src/types/file.ts +76 -0
  107. package/src/types/http-request-error.ts +34 -0
  108. package/src/types/images.ts +102 -0
  109. package/src/types/index.ts +12 -0
  110. package/src/types/multimodal-embedding.ts +67 -0
  111. package/src/types/responses/enums.ts +163 -0
  112. package/src/types/responses/helpers.ts +67 -0
  113. package/src/types/responses/index.ts +3 -0
  114. package/src/types/responses/types.ts +1335 -0
  115. package/src/types/tokenization.ts +24 -0
  116. package/src/utils/breaker-provider.ts +17 -0
  117. package/src/utils/breaker.ts +56 -0
  118. package/src/utils/normalize.ts +154 -0
  119. package/src/utils/request-builder.ts +51 -0
  120. package/src/utils/request-id.ts +17 -0
  121. package/src/utils/retry.ts +76 -0
  122. package/src/utils/sse-decoder.ts +140 -0
  123. package/src/utils/stream-reader.ts +270 -0
package/src/config.ts ADDED
@@ -0,0 +1,68 @@
1
+ import type { AxiosInstance } from "axios";
2
+
3
+ export const defaultBaseURL = "https://ark.cn-beijing.volces.com/api/v3";
4
+ export const defaultRegion = "cn-beijing";
5
+ export const defaultEmptyMessagesLimit = 300;
6
+ export const defaultRetryTimes = 2;
7
+ export const defaultTimeoutMs = 10 * 60 * 1000; // 10 min
8
+ export const defaultBatchMaxParallel = 3000;
9
+
10
+ export const resourceTypeEndpoint = "endpoint";
11
+ export const resourceTypeBot = "bot";
12
+ export const resourceTypePresetEndpoint = "presetendpoint";
13
+
14
+ export interface ClientConfig {
15
+ /** API key for authentication (mutually exclusive with ak/sk) */
16
+ apiKey?: string;
17
+ /** Access key for AK/SK authentication */
18
+ ak?: string;
19
+ /** Secret key for AK/SK authentication */
20
+ sk?: string;
21
+ /** Region, defaults to "cn-beijing" */
22
+ region?: string;
23
+ /** Base URL for the API, defaults to "https://ark.cn-beijing.volces.com/api/v3" */
24
+ baseURL?: string;
25
+ /** Custom axios instance */
26
+ httpClient?: AxiosInstance;
27
+ /** Max number of empty SSE messages before error, defaults to 300 */
28
+ emptyMessagesLimit?: number;
29
+ /** Number of retry attempts, defaults to 2 */
30
+ retryTimes?: number;
31
+ /** Request timeout in milliseconds, defaults to 600000 (10 min) */
32
+ timeout?: number;
33
+ /** Max parallel batch requests, defaults to 3000 */
34
+ batchMaxParallel?: number;
35
+ }
36
+
37
+ export interface ResolvedConfig {
38
+ apiKey: string;
39
+ ak: string;
40
+ sk: string;
41
+ region: string;
42
+ baseURL: string;
43
+ httpClient?: AxiosInstance;
44
+ emptyMessagesLimit: number;
45
+ retryTimes: number;
46
+ timeout: number;
47
+ batchMaxParallel: number;
48
+ }
49
+
50
+ export function resolveConfig(config: ClientConfig): ResolvedConfig {
51
+ let baseURL = config.baseURL ?? process.env.ARK_BASE_URL ?? defaultBaseURL;
52
+ if (baseURL.endsWith("/")) {
53
+ baseURL = baseURL.slice(0, -1);
54
+ }
55
+
56
+ return {
57
+ apiKey: config.apiKey ?? process.env.ARK_API_KEY ?? "",
58
+ ak: config.ak ?? "",
59
+ sk: config.sk ?? "",
60
+ region: config.region ?? defaultRegion,
61
+ baseURL,
62
+ httpClient: config.httpClient,
63
+ emptyMessagesLimit: config.emptyMessagesLimit ?? defaultEmptyMessagesLimit,
64
+ retryTimes: config.retryTimes ?? defaultRetryTimes,
65
+ timeout: config.timeout ?? defaultTimeoutMs,
66
+ batchMaxParallel: config.batchMaxParallel ?? defaultBatchMaxParallel,
67
+ };
68
+ }
@@ -0,0 +1,146 @@
1
+ import { aesGcmEncryptBase64, aesGcmDecryptBase64 } from "./key-agreement";
2
+ import type {
3
+ ChatCompletionRequest,
4
+ ChatCompletionResponse,
5
+ ChatCompletionStreamResponse,
6
+ ChatCompletionMessage,
7
+ ChatCompletionMessageContentPart,
8
+ } from "../types/chat-completion";
9
+
10
+ const AES_KEY_SIZE = 32;
11
+ const AES_NONCE_SIZE = 12;
12
+
13
+ type TextTransformer = (text: string) => string;
14
+
15
+ function encryptURL(urlString: string, fn: TextTransformer): string {
16
+ try {
17
+ const parsed = new URL(urlString);
18
+ if (["https:", "http:", "file:", "ftp:"].includes(parsed.protocol)) {
19
+ // URLs with these protocols are not encrypted
20
+ return urlString;
21
+ }
22
+ } catch {
23
+ // Not a valid URL with protocol — check for data: scheme
24
+ }
25
+ if (urlString.startsWith("data:")) {
26
+ return fn(urlString);
27
+ }
28
+ return urlString;
29
+ }
30
+
31
+ function processMessageContent(
32
+ content: string | ChatCompletionMessageContentPart[] | undefined | null,
33
+ fn: TextTransformer,
34
+ ): string | ChatCompletionMessageContentPart[] | undefined | null {
35
+ if (content == null) return content;
36
+
37
+ if (typeof content === "string") {
38
+ return fn(content);
39
+ }
40
+
41
+ // Array of content parts
42
+ for (const part of content) {
43
+ if (part.type === "text" && part.text) {
44
+ part.text = fn(part.text);
45
+ }
46
+ if (part.type === "image_url" && part.image_url?.url) {
47
+ part.image_url.url = encryptURL(part.image_url.url, fn);
48
+ }
49
+ }
50
+ return content;
51
+ }
52
+
53
+ /**
54
+ * Deep-copy a chat completion request (JSON round-trip).
55
+ */
56
+ export function deepCopyRequest(
57
+ request: ChatCompletionRequest,
58
+ ): ChatCompletionRequest {
59
+ return JSON.parse(JSON.stringify(request));
60
+ }
61
+
62
+ /**
63
+ * Encrypt all text content in a chat completion request.
64
+ * Modifies the request in place.
65
+ * Matches Go's `EncryptChatRequest`.
66
+ */
67
+ export function encryptChatRequest(
68
+ keyNonce: Buffer,
69
+ request: ChatCompletionRequest,
70
+ ): void {
71
+ if (keyNonce.length !== AES_KEY_SIZE + AES_NONCE_SIZE) {
72
+ throw new Error(
73
+ `keyNonce must be ${AES_KEY_SIZE + AES_NONCE_SIZE} bytes, got ${keyNonce.length}`,
74
+ );
75
+ }
76
+ const key = keyNonce.subarray(0, AES_KEY_SIZE);
77
+ const nonce = keyNonce.subarray(AES_KEY_SIZE);
78
+ const fn: TextTransformer = (text) =>
79
+ aesGcmEncryptBase64(key, nonce, text);
80
+
81
+ for (const msg of request.messages ?? []) {
82
+ if (msg.content != null) {
83
+ (msg as ChatCompletionMessage).content = processMessageContent(
84
+ msg.content as string | ChatCompletionMessageContentPart[],
85
+ fn,
86
+ ) as typeof msg.content;
87
+ }
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Decrypt all text content in a chat completion response.
93
+ * Modifies the response in place.
94
+ * Matches Go's `DecryptChatResponse`.
95
+ */
96
+ export function decryptChatResponse(
97
+ keyNonce: Buffer,
98
+ response: ChatCompletionResponse,
99
+ ): void {
100
+ if (keyNonce.length !== AES_KEY_SIZE + AES_NONCE_SIZE) {
101
+ throw new Error(
102
+ `keyNonce must be ${AES_KEY_SIZE + AES_NONCE_SIZE} bytes, got ${keyNonce.length}`,
103
+ );
104
+ }
105
+ const key = keyNonce.subarray(0, AES_KEY_SIZE);
106
+ const nonce = keyNonce.subarray(AES_KEY_SIZE);
107
+ const fn: TextTransformer = (text) =>
108
+ aesGcmDecryptBase64(key, nonce, text);
109
+
110
+ for (const choice of response.choices ?? []) {
111
+ if (choice.finish_reason === "content_filter") continue;
112
+ if (choice.message?.content != null) {
113
+ choice.message.content = processMessageContent(
114
+ choice.message.content as string | ChatCompletionMessageContentPart[],
115
+ fn,
116
+ ) as typeof choice.message.content;
117
+ }
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Decrypt delta content in a streaming chat completion chunk.
123
+ * Modifies the response in place.
124
+ * Matches Go's `DecryptChatStreamResponse`.
125
+ */
126
+ export function decryptChatStreamResponse(
127
+ keyNonce: Buffer,
128
+ response: ChatCompletionStreamResponse,
129
+ ): void {
130
+ if (keyNonce.length !== AES_KEY_SIZE + AES_NONCE_SIZE) {
131
+ throw new Error(
132
+ `keyNonce must be ${AES_KEY_SIZE + AES_NONCE_SIZE} bytes, got ${keyNonce.length}`,
133
+ );
134
+ }
135
+ const key = keyNonce.subarray(0, AES_KEY_SIZE);
136
+ const nonce = keyNonce.subarray(AES_KEY_SIZE);
137
+ const fn: TextTransformer = (text) =>
138
+ aesGcmDecryptBase64(key, nonce, text);
139
+
140
+ for (const choice of response.choices ?? []) {
141
+ if (choice.finish_reason === "content_filter") continue;
142
+ if (choice.delta?.content && typeof choice.delta.content === "string") {
143
+ choice.delta.content = fn(choice.delta.content);
144
+ }
145
+ }
146
+ }
@@ -0,0 +1,21 @@
1
+ export {
2
+ KeyAgreementClient,
3
+ E2eeClient,
4
+ aesGcmEncrypt,
5
+ aesGcmDecrypt,
6
+ aesGcmEncryptBase64,
7
+ aesGcmDecryptBase64,
8
+ getCertInfo,
9
+ checkIsModeAICC,
10
+ loadLocalCertificate,
11
+ saveToLocalCertificate,
12
+ } from "./key-agreement";
13
+
14
+ export type { EncryptInfo } from "./key-agreement";
15
+
16
+ export {
17
+ encryptChatRequest,
18
+ decryptChatResponse,
19
+ decryptChatStreamResponse,
20
+ deepCopyRequest,
21
+ } from "./encrypt-chat";
@@ -0,0 +1,270 @@
1
+ import * as crypto from "crypto";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+
6
+ const AES_KEY_SIZE = 32;
7
+ const AES_NONCE_SIZE = 12;
8
+
9
+ export interface EncryptInfo {
10
+ version?: string;
11
+ ring_id?: string;
12
+ key_id?: string;
13
+ expire_time: number;
14
+ }
15
+
16
+ const CIPHER_VERSION_AICC_V01 = "AICCv01";
17
+
18
+ /**
19
+ * Key agreement client using ECIES (P-256 / prime256v1).
20
+ * Ports the Go `KeyAgreementClient` from `pkg/encryption/kaMgr.go`.
21
+ */
22
+ export class KeyAgreementClient {
23
+ private serverPublicKey: crypto.KeyObject;
24
+
25
+ constructor(pemCert: string) {
26
+ const x509 = new crypto.X509Certificate(pemCert);
27
+ this.serverPublicKey = x509.publicKey;
28
+ }
29
+
30
+ /**
31
+ * Generate ephemeral ECIES key pair and derive shared secret.
32
+ * Returns [keyNonce (AES key + nonce), sessionToken (base64)].
33
+ */
34
+ generateECIESKeyPair(): [Buffer, string] {
35
+ // Generate ephemeral EC key pair on prime256v1
36
+ const ecdh = crypto.createECDH("prime256v1");
37
+ ecdh.generateKeys();
38
+
39
+ // Get server public key in uncompressed format
40
+ const serverPubKeyDer = this.serverPublicKey.export({
41
+ type: "spki",
42
+ format: "der",
43
+ });
44
+ // Extract raw public key from SPKI DER
45
+ const serverPubKey = extractRawPublicKeyFromSPKI(serverPubKeyDer);
46
+
47
+ // Compute shared secret (ECDH)
48
+ const sharedSecret = ecdh.computeSecret(serverPubKey);
49
+
50
+ // Derive key material via HKDF (SHA-256, no salt, no info)
51
+ const keyMaterial = hkdfExpand(sharedSecret, AES_KEY_SIZE + AES_NONCE_SIZE);
52
+
53
+ // Session token is the uncompressed ephemeral public key, base64-encoded
54
+ const sessionToken = ecdh.getPublicKey("base64");
55
+
56
+ return [keyMaterial, sessionToken];
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Extract raw public key bytes from SPKI DER encoding for EC keys.
62
+ * The uncompressed point is at the end of the SPKI structure.
63
+ */
64
+ function extractRawPublicKeyFromSPKI(spkiDer: Buffer): Buffer {
65
+ // For P-256 uncompressed, the public key is 65 bytes (0x04 || X || Y)
66
+ // SPKI adds algorithm identifiers at the beginning
67
+ // The bitstring containing the public key is the last part
68
+ // For P-256 SPKI: the raw key starts at byte 26 (total = 91 bytes)
69
+ const uncompressedKeyLen = 65;
70
+ if (spkiDer.length >= uncompressedKeyLen) {
71
+ return spkiDer.subarray(spkiDer.length - uncompressedKeyLen);
72
+ }
73
+ return spkiDer;
74
+ }
75
+
76
+ /**
77
+ * HKDF extract + expand (SHA-256) with no salt and no info.
78
+ * Matches Go's `deriveKeyBasic`.
79
+ *
80
+ * The Go code uses ECDHMarshalBinary which is just the X coordinate of the shared point.
81
+ * However, Node's ECDH.computeSecret already returns the X coordinate by default.
82
+ */
83
+ function hkdfExpand(ikm: Buffer, length: number): Buffer {
84
+ // Use Node.js built-in hkdfSync (available since Node 15+)
85
+ const derivedKey = crypto.hkdfSync(
86
+ "sha256",
87
+ ikm,
88
+ Buffer.alloc(0), // no salt → use all-zero salt internally
89
+ Buffer.alloc(0), // no info
90
+ length,
91
+ );
92
+ return Buffer.from(derivedKey);
93
+ }
94
+
95
+ /**
96
+ * AES-256-GCM encryption.
97
+ */
98
+ export function aesGcmEncrypt(
99
+ key: Buffer,
100
+ nonce: Buffer,
101
+ plaintext: Buffer,
102
+ ): Buffer {
103
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, nonce);
104
+ const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
105
+ const tag = cipher.getAuthTag();
106
+ return Buffer.concat([encrypted, tag]);
107
+ }
108
+
109
+ /**
110
+ * AES-256-GCM decryption.
111
+ */
112
+ export function aesGcmDecrypt(
113
+ key: Buffer,
114
+ nonce: Buffer,
115
+ ciphertext: Buffer,
116
+ ): Buffer {
117
+ const tagLength = 16;
118
+ const encrypted = ciphertext.subarray(0, ciphertext.length - tagLength);
119
+ const tag = ciphertext.subarray(ciphertext.length - tagLength);
120
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce);
121
+ decipher.setAuthTag(tag);
122
+ return Buffer.concat([decipher.update(encrypted), decipher.final()]);
123
+ }
124
+
125
+ /**
126
+ * Encrypt plaintext string → base64 ciphertext using AES-GCM.
127
+ */
128
+ export function aesGcmEncryptBase64(
129
+ key: Buffer,
130
+ nonce: Buffer,
131
+ plaintext: string,
132
+ ): string {
133
+ const encrypted = aesGcmEncrypt(key, nonce, Buffer.from(plaintext, "utf-8"));
134
+ return encrypted.toString("base64");
135
+ }
136
+
137
+ /**
138
+ * Decrypt base64 ciphertext → plaintext string using AES-GCM.
139
+ */
140
+ export function aesGcmDecryptBase64(
141
+ key: Buffer,
142
+ nonce: Buffer,
143
+ ciphertext: string,
144
+ ): string {
145
+ const cipherBuf = Buffer.from(ciphertext, "base64");
146
+ const decrypted = aesGcmDecrypt(key, nonce, cipherBuf);
147
+ return decrypted.toString("utf-8");
148
+ }
149
+
150
+ /**
151
+ * Extract ring ID, key ID, and expiry from a PEM certificate.
152
+ * Matches Go's `GetCertInfo`.
153
+ */
154
+ export function getCertInfo(
155
+ certPem: string,
156
+ ): { ringId: string; keyId: string; expireTime: number } {
157
+ try {
158
+ const x509 = new crypto.X509Certificate(certPem);
159
+ const altNames = x509.subjectAltName ?? "";
160
+ // Parse DNS:ring.xxx, DNS:key.yyy from subject alt names
161
+ const dnsNames: string[] = [];
162
+ for (const part of altNames.split(",")) {
163
+ const trimmed = part.trim();
164
+ if (trimmed.startsWith("DNS:")) {
165
+ dnsNames.push(trimmed.substring(4));
166
+ }
167
+ }
168
+ const expireTime = Math.floor(new Date(x509.validTo).getTime() / 1000);
169
+ if (
170
+ dnsNames.length > 1 &&
171
+ dnsNames[0].startsWith("ring.") &&
172
+ dnsNames[1].startsWith("key.")
173
+ ) {
174
+ return {
175
+ ringId: dnsNames[0].substring(5),
176
+ keyId: dnsNames[1].substring(4),
177
+ expireTime,
178
+ };
179
+ }
180
+ return { ringId: "", keyId: "", expireTime };
181
+ } catch {
182
+ return { ringId: "", keyId: "", expireTime: 0 };
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Check if AICC encryption mode is enabled via environment variable.
188
+ */
189
+ export function checkIsModeAICC(): boolean {
190
+ return process.env.VOLC_ARK_ENCRYPTION === "AICC";
191
+ }
192
+
193
+ /**
194
+ * Load locally cached certificate for a model.
195
+ * Matches Go's `LoadLocalCertificate`.
196
+ */
197
+ export function loadLocalCertificate(model: string): string | null {
198
+ if (!model || /[/\\:..]/.test(model)) return null;
199
+ const dir = path.join(os.homedir(), ".ark", "certificates");
200
+ const certPath = path.join(dir, `${model}.pem`);
201
+ try {
202
+ const stat = fs.statSync(certPath);
203
+ const age = Date.now() - stat.mtimeMs;
204
+ if (age > 14 * 24 * 60 * 60 * 1000) {
205
+ fs.unlinkSync(certPath);
206
+ return null;
207
+ }
208
+ const certPem = fs.readFileSync(certPath, "utf-8");
209
+ const { ringId, keyId } = getCertInfo(certPem);
210
+ const aiccEnabled = checkIsModeAICC();
211
+ if ((ringId === "" || keyId === "") && !aiccEnabled) return certPem;
212
+ if (ringId !== "" && keyId !== "" && aiccEnabled) return certPem;
213
+ fs.unlinkSync(certPath);
214
+ return null;
215
+ } catch {
216
+ return null;
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Save certificate PEM to local cache.
222
+ * Matches Go's `SaveToLocalCertificate`.
223
+ */
224
+ export function saveToLocalCertificate(
225
+ model: string,
226
+ certPem: string,
227
+ ): void {
228
+ if (!model || /[/\\:..]/.test(model)) return;
229
+ const dir = path.join(os.homedir(), ".ark", "certificates");
230
+ fs.mkdirSync(dir, { recursive: true });
231
+ const certPath = path.join(dir, `${model}.pem`);
232
+ fs.writeFileSync(certPath, certPem, "utf-8");
233
+ }
234
+
235
+ /**
236
+ * E2EE client — manages certificate + key agreement + encrypt info.
237
+ * Matches Go's `E2eeClient`.
238
+ */
239
+ export class E2eeClient {
240
+ private certificate: string;
241
+ private cipher: KeyAgreementClient;
242
+ private info: EncryptInfo;
243
+ readonly isAICC: boolean;
244
+
245
+ constructor(certificate: string) {
246
+ this.certificate = certificate;
247
+ this.cipher = new KeyAgreementClient(certificate);
248
+ const { ringId, keyId, expireTime } = getCertInfo(certificate);
249
+ this.info = { expire_time: expireTime };
250
+ this.isAICC = false;
251
+ if (ringId && keyId) {
252
+ this.info.version = CIPHER_VERSION_AICC_V01;
253
+ this.info.ring_id = ringId;
254
+ this.info.key_id = keyId;
255
+ this.isAICC = true;
256
+ }
257
+ }
258
+
259
+ generateECIESKeyPair(): [Buffer, string] {
260
+ return this.cipher.generateECIESKeyPair();
261
+ }
262
+
263
+ getEncryptInfo(): string {
264
+ return JSON.stringify(this.info);
265
+ }
266
+
267
+ getExpireTime(): number {
268
+ return this.info.expire_time;
269
+ }
270
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export { ArkRuntimeClient } from "./client";
2
+ export type { ClientConfig } from "./config";
3
+ export * from "./types";
4
+ export * from "./utils/retry";
5
+ export * from "./utils/sse-decoder";
6
+ export * from "./utils/stream-reader";
7
+ export * from "./utils/breaker";
8
+ export * from "./utils/breaker-provider";
9
+ export * from "./utils/request-id";
10
+ export * from "./encryption";
@@ -0,0 +1,9 @@
1
+ declare module "@volcengine/ark" {
2
+ export class ARKClient {
3
+ constructor(opts: any);
4
+ send<T = any>(command: any): Promise<T>;
5
+ }
6
+ export class GetApiKeyCommand {
7
+ constructor(opts: any);
8
+ }
9
+ }
@@ -0,0 +1,127 @@
1
+ import type { Usage, HttpHeaders } from "./common";
2
+ import type {
3
+ ChatCompletionMessage,
4
+ ChatCompletionChoice,
5
+ ChatCompletionStreamChoice,
6
+ Tool,
7
+ ToolChoice,
8
+ StreamOptions,
9
+ ResponseFormat,
10
+ Thinking,
11
+ } from "./chat-completion";
12
+
13
+ export interface BotChatCompletionRequest {
14
+ bot_id?: string;
15
+ model: string;
16
+ messages: ChatCompletionMessage[];
17
+ max_tokens?: number;
18
+ temperature?: number;
19
+ top_p?: number;
20
+ stream?: boolean;
21
+ stop?: string[];
22
+ frequency_penalty?: number;
23
+ logit_bias?: Record<string, number>;
24
+ logprobs?: boolean;
25
+ top_logprobs?: number;
26
+ user?: string;
27
+ function_call?: unknown;
28
+ tools?: Tool[];
29
+ tool_choice?: string | ToolChoice;
30
+ stream_options?: StreamOptions;
31
+ presence_penalty?: number;
32
+ repetition_penalty?: number;
33
+ n?: number;
34
+ response_format?: ResponseFormat;
35
+ thinking?: Thinking;
36
+ metadata?: Record<string, unknown>;
37
+ }
38
+
39
+ export interface BotActionUsage {
40
+ name: string;
41
+ prompt_tokens?: string;
42
+ completion_tokens?: number;
43
+ total_tokens?: number;
44
+ search_count?: number;
45
+ action_name?: string;
46
+ count?: number;
47
+ }
48
+
49
+ export interface BotModelUsage extends Usage {
50
+ name: string;
51
+ }
52
+
53
+ export interface BotToolDetail {
54
+ name: string;
55
+ input: unknown;
56
+ output: unknown;
57
+ created_at: number;
58
+ completed_at: number;
59
+ }
60
+
61
+ export interface BotActionDetail {
62
+ name: string;
63
+ count: number;
64
+ tool_details?: BotToolDetail[];
65
+ }
66
+
67
+ export interface BotUsage {
68
+ model_usage?: BotModelUsage[];
69
+ action_usage?: BotActionUsage[];
70
+ action_details?: BotActionDetail[];
71
+ }
72
+
73
+ export interface BotCoverImage {
74
+ url?: string;
75
+ width?: number;
76
+ height?: number;
77
+ }
78
+
79
+ export interface BotChatResultReference {
80
+ url?: string;
81
+ logo_url?: string;
82
+ mobile_url?: string;
83
+ site_name?: string;
84
+ title?: string;
85
+ cover_image?: BotCoverImage;
86
+ summary?: string;
87
+ publish_time?: string;
88
+ collection_name?: string;
89
+ project?: string;
90
+ doc_id?: string;
91
+ doc_name?: string;
92
+ doc_type?: string;
93
+ doc_title?: string;
94
+ chunk_id?: string;
95
+ chunk_title?: string;
96
+ page_nums?: string;
97
+ origin_text_token_len?: number;
98
+ file_name?: string;
99
+ extra?: Record<string, unknown>;
100
+ }
101
+
102
+ export interface BotChatCompletionResponse {
103
+ id: string;
104
+ object: string;
105
+ created: number;
106
+ model: string;
107
+ service_tier?: string;
108
+ choices: ChatCompletionChoice[];
109
+ usage: Usage;
110
+ metadata?: Record<string, unknown>;
111
+ bot_usage?: BotUsage;
112
+ references?: BotChatResultReference[];
113
+ headers?: HttpHeaders;
114
+ }
115
+
116
+ export interface BotChatCompletionStreamResponse {
117
+ id: string;
118
+ object: string;
119
+ created: number;
120
+ model: string;
121
+ service_tier?: string;
122
+ choices: ChatCompletionStreamChoice[];
123
+ usage?: Usage;
124
+ metadata?: Record<string, unknown>;
125
+ bot_usage?: BotUsage;
126
+ references?: BotChatResultReference[];
127
+ }