@huace-aigc/aigc-maas-client 0.1.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/README.md +38 -0
- package/package.json +22 -0
- package/src/index.js +182 -0
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Huace AIGC MaaS Node.js SDK
|
|
2
|
+
|
|
3
|
+
Node.js 客户端用于接入 new-api MaaS 托管 Key 能力,支持申请或复用托管 Key、查询 Key
|
|
4
|
+
元信息和查询用量。
|
|
5
|
+
|
|
6
|
+
调用方或 Agent 请先阅读 [`SKILL.md`](SKILL.md);公共 HTTP 契约见
|
|
7
|
+
[`../openapi/maas.yaml`](../openapi/maas.yaml)。
|
|
8
|
+
|
|
9
|
+
## 安装
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @huace-aigc/aigc-maas-client
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 使用
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import { MaaSClient } from "@huace-aigc/aigc-maas-client";
|
|
19
|
+
|
|
20
|
+
const client = new MaaSClient({
|
|
21
|
+
baseUrl: "https://maas.example.com",
|
|
22
|
+
appId: "12", // Numeric AIGC Auth application ID.
|
|
23
|
+
});
|
|
24
|
+
const ensured = await client.ensureKey({ userToken: "auth-user-token" });
|
|
25
|
+
const key = ensured.credential.apiKey;
|
|
26
|
+
const info = await client.getKeyInfo(key);
|
|
27
|
+
const usage = await client.getUsage(key);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`userToken` 只在申请时传入,Client 不保存用户 Token。未提供幂等键时,SDK 会自动
|
|
31
|
+
生成 UUID,并在内部重试中复用。API Key 是敏感凭证,请通过 HTTPS 传输并避免写入
|
|
32
|
+
日志。Key 信息查询接口不会返回明文 Key。
|
|
33
|
+
|
|
34
|
+
## 配置
|
|
35
|
+
|
|
36
|
+
- `baseUrl` 和 `appId` 必填。
|
|
37
|
+
- 默认请求超时为 10 秒,默认最多重试 2 次。
|
|
38
|
+
- 仅网络错误、429 和 5xx 会自动重试,4xx 错误直接抛出 `APIError`。
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@huace-aigc/aigc-maas-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Node.js client for the new-api MaaS managed-key API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": ".",
|
|
8
|
+
"files": [
|
|
9
|
+
"src",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public",
|
|
14
|
+
"registry": "https://registry.npmjs.org/"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node --test test/client.test.js"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
class ResponseDecodeError extends Error {}
|
|
4
|
+
|
|
5
|
+
export class APIError extends Error {
|
|
6
|
+
constructor(statusCode, code, message) {
|
|
7
|
+
super(`maas: request failed with status ${statusCode} (${code}): ${message}`);
|
|
8
|
+
this.name = "APIError";
|
|
9
|
+
this.statusCode = statusCode;
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.message = message;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class MaaSClient {
|
|
16
|
+
constructor({ baseUrl, appId, timeout = 10000, maxRetries = 2, retryBaseDelay = 100, fetchImpl = globalThis.fetch } = {}) {
|
|
17
|
+
if (typeof baseUrl !== "string" || !baseUrl.trim()) {
|
|
18
|
+
throw new TypeError("baseUrl is required");
|
|
19
|
+
}
|
|
20
|
+
const parsed = new URL(baseUrl.trim());
|
|
21
|
+
if (!parsed.protocol || !parsed.host) {
|
|
22
|
+
throw new TypeError("baseUrl must include scheme and host");
|
|
23
|
+
}
|
|
24
|
+
if (typeof appId !== "string" || !/^\d+$/.test(appId.trim())) {
|
|
25
|
+
throw new TypeError("appId must be a positive integer");
|
|
26
|
+
}
|
|
27
|
+
const normalizedAppId = BigInt(appId.trim());
|
|
28
|
+
if (normalizedAppId <= 0n || normalizedAppId > 9223372036854775807n) {
|
|
29
|
+
throw new TypeError("appId must be a positive integer");
|
|
30
|
+
}
|
|
31
|
+
if (timeout < 0 || maxRetries < 0 || retryBaseDelay < 0) {
|
|
32
|
+
throw new TypeError("timeout, retries and retry delay cannot be negative");
|
|
33
|
+
}
|
|
34
|
+
if (typeof fetchImpl !== "function") {
|
|
35
|
+
throw new TypeError("fetch is unavailable");
|
|
36
|
+
}
|
|
37
|
+
this.baseUrl = baseUrl.trim().replace(/\/+$/, "");
|
|
38
|
+
this.appId = normalizedAppId.toString();
|
|
39
|
+
this.timeout = timeout;
|
|
40
|
+
this.maxRetries = maxRetries;
|
|
41
|
+
this.retryBaseDelay = retryBaseDelay;
|
|
42
|
+
this.fetchImpl = fetchImpl;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async ensureKey({ userToken, idempotencyKey } = {}) {
|
|
46
|
+
if (typeof userToken !== "string" || !userToken.trim()) {
|
|
47
|
+
throw new TypeError("userToken is required");
|
|
48
|
+
}
|
|
49
|
+
const key = typeof idempotencyKey === "string" && idempotencyKey.trim() ? idempotencyKey.trim() : randomUUID();
|
|
50
|
+
const body = await this.#request("POST", "/api/integrations/v1/keys/ensure", userToken, {
|
|
51
|
+
"X-AIGC-Auth-App-Id": this.appId,
|
|
52
|
+
"Idempotency-Key": key,
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
}, "{}");
|
|
55
|
+
return body.data;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async getKeyInfo(apiKey) {
|
|
59
|
+
const body = await this.#request("GET", "/api/integrations/v1/self/key", apiKey);
|
|
60
|
+
return body.data;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async getUsage(apiKey, { startTimestamp, endTimestamp } = {}) {
|
|
64
|
+
const params = new URLSearchParams();
|
|
65
|
+
if (startTimestamp !== undefined && startTimestamp !== null) {
|
|
66
|
+
params.set("start_timestamp", String(startTimestamp));
|
|
67
|
+
}
|
|
68
|
+
if (endTimestamp !== undefined && endTimestamp !== null) {
|
|
69
|
+
params.set("end_timestamp", String(endTimestamp));
|
|
70
|
+
}
|
|
71
|
+
const query = params.toString();
|
|
72
|
+
const path = `/api/integrations/v1/self/usage${query ? `?${query}` : ""}`;
|
|
73
|
+
const body = await this.#request("GET", path, apiKey);
|
|
74
|
+
return normalizeUsage(body.data);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async #request(method, path, credential, extraHeaders = {}, body) {
|
|
78
|
+
if (typeof credential !== "string" || !credential.trim()) {
|
|
79
|
+
throw new TypeError("credential is required");
|
|
80
|
+
}
|
|
81
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) {
|
|
82
|
+
const controller = new AbortController();
|
|
83
|
+
const timer = this.timeout > 0 ? setTimeout(() => controller.abort(), this.timeout) : null;
|
|
84
|
+
try {
|
|
85
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
86
|
+
method,
|
|
87
|
+
headers: { Authorization: bearerHeader(credential), ...extraHeaders },
|
|
88
|
+
body,
|
|
89
|
+
signal: controller.signal,
|
|
90
|
+
});
|
|
91
|
+
const text = await response.text();
|
|
92
|
+
if (response.ok) {
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(text);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw new ResponseDecodeError(`decode MaaS response: ${error.message}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const apiError = decodeAPIError(response.status, text);
|
|
100
|
+
if (attempt < this.maxRetries && retryableStatus(response.status)) {
|
|
101
|
+
await this.#wait(attempt);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
throw apiError;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error instanceof APIError || error instanceof ResponseDecodeError) {
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
if (attempt < this.maxRetries) {
|
|
110
|
+
await this.#wait(attempt);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
throw error;
|
|
114
|
+
} finally {
|
|
115
|
+
if (timer) {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
throw new Error("maas: retry loop exhausted");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async #wait(attempt) {
|
|
124
|
+
const delay = this.retryBaseDelay * (2 ** attempt);
|
|
125
|
+
if (delay > 0) {
|
|
126
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function bearerHeader(value) {
|
|
132
|
+
const trimmed = value.trim();
|
|
133
|
+
if (trimmed.toLowerCase().startsWith("bearer ")) {
|
|
134
|
+
return `Bearer ${trimmed.slice(7).trim()}`;
|
|
135
|
+
}
|
|
136
|
+
return `Bearer ${trimmed}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function retryableStatus(status) {
|
|
140
|
+
return status === 429 || status >= 500;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function decodeAPIError(status, text) {
|
|
144
|
+
let code = `HTTP_${status}`;
|
|
145
|
+
let message = `HTTP status ${status}`;
|
|
146
|
+
try {
|
|
147
|
+
const error = JSON.parse(text).error || {};
|
|
148
|
+
code = error.code || code;
|
|
149
|
+
message = error.message || message;
|
|
150
|
+
} catch {
|
|
151
|
+
// Keep the HTTP fallback when the server did not return JSON.
|
|
152
|
+
}
|
|
153
|
+
return new APIError(status, code, message);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function normalizeUsage(data = {}) {
|
|
157
|
+
return {
|
|
158
|
+
...data,
|
|
159
|
+
period: {
|
|
160
|
+
startTimestamp: data.period?.start_timestamp ?? 0,
|
|
161
|
+
endTimestamp: data.period?.end_timestamp ?? 0,
|
|
162
|
+
},
|
|
163
|
+
remainQuota: data.remain_quota ?? 0,
|
|
164
|
+
summary: normalizeMetrics(data.summary),
|
|
165
|
+
models: (data.models ?? []).map((model) => ({
|
|
166
|
+
...model,
|
|
167
|
+
modelName: model.model_name ?? "",
|
|
168
|
+
...normalizeMetrics(model),
|
|
169
|
+
})),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function normalizeMetrics(metrics = {}) {
|
|
174
|
+
return {
|
|
175
|
+
quota: metrics.quota ?? 0,
|
|
176
|
+
count: metrics.count ?? 0,
|
|
177
|
+
tokenUsed: metrics.token_used ?? 0,
|
|
178
|
+
promptTokens: metrics.prompt_tokens ?? 0,
|
|
179
|
+
completionTokens: metrics.completion_tokens ?? 0,
|
|
180
|
+
cacheTokens: metrics.cache_tokens ?? 0,
|
|
181
|
+
};
|
|
182
|
+
}
|