@axemere/gateway 0.1.6
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 +49 -0
- package/dist/client.d.ts +48 -0
- package/dist/client.js +297 -0
- package/dist/config.d.ts +53 -0
- package/dist/config.js +100 -0
- package/dist/errors.d.ts +44 -0
- package/dist/errors.js +61 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/providers.d.ts +5 -0
- package/dist/providers.js +22 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.js +2 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Axemere LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# @axemere/gateway
|
|
2
|
+
|
|
3
|
+
[](../../LICENSE)
|
|
4
|
+
|
|
5
|
+
Framework-independent TypeScript client for the [Axemere AI Gateway](https://axemere.ai).
|
|
6
|
+
|
|
7
|
+
Use this package when you want explicit control over every request, or when you are not using OpenAI or Anthropic SDKs directly. If you are already using one of those SDKs, install the matching wrapper instead (`@axemere/gateway-openai`, `@axemere/gateway-anthropic`) — it requires no code changes beyond the import.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @axemere/gateway
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { AiGatewayClient, AiGatewayConfig } from "@axemere/gateway";
|
|
19
|
+
|
|
20
|
+
const config = new AiGatewayConfig(); // reads AXEMERE_GATEWAY_URL + AXEMERE_GATEWAY_TOKEN
|
|
21
|
+
const client = new AiGatewayClient(config);
|
|
22
|
+
|
|
23
|
+
const result = await client.execute({
|
|
24
|
+
provider: "openai",
|
|
25
|
+
model: "gpt-4o-mini",
|
|
26
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
27
|
+
});
|
|
28
|
+
console.log(result.content);
|
|
29
|
+
console.log(result.metering?.cost_usd); // "0.000042"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
| Env var | Description |
|
|
35
|
+
|---------|-------------|
|
|
36
|
+
| `AXEMERE_GATEWAY_URL` | Gateway base URL, e.g. `http://localhost:7080` |
|
|
37
|
+
| `AXEMERE_GATEWAY_TOKEN` | Gateway token issued by the gateway |
|
|
38
|
+
| `AXEMERE_WORKLOAD_ID` | Workload identifier for attribution |
|
|
39
|
+
| `AXEMERE_PROJECT_ID` | Project identifier for spend grouping |
|
|
40
|
+
|
|
41
|
+
## Links
|
|
42
|
+
|
|
43
|
+
- [Axemere AI Gateway](https://axemere.ai)
|
|
44
|
+
- [Documentation](https://axemere.ai/docs)
|
|
45
|
+
- [GitHub](https://github.com/Axemere-LLC/axemere-node)
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
MIT
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { AiGatewayConfig } from "./config";
|
|
2
|
+
import { ExecuteParams, ExecuteResponse, StreamChunk } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* Client for the Axemere AI Gateway explicit action API.
|
|
5
|
+
*
|
|
6
|
+
* Wraps the gateway's `/v1/actions:execute` endpoint, building the wire request
|
|
7
|
+
* from {@link ExecuteParams}, applying attribution and delegation, and normalizing
|
|
8
|
+
* provider responses (OpenAI- and Anthropic-style) into a uniform shape.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const client = new AiGatewayClient(new AiGatewayConfig());
|
|
13
|
+
* const res = await client.execute({
|
|
14
|
+
* provider: "openai",
|
|
15
|
+
* model: "gpt-4o-mini",
|
|
16
|
+
* messages: [{ role: "user", content: "Hello" }],
|
|
17
|
+
* });
|
|
18
|
+
* console.log(res.content);
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare class AiGatewayClient {
|
|
22
|
+
private readonly config;
|
|
23
|
+
constructor(config: AiGatewayConfig);
|
|
24
|
+
/**
|
|
25
|
+
* Executes a governed AI request through the gateway.
|
|
26
|
+
*
|
|
27
|
+
* Resolves provider/model and attribution from {@link ExecuteParams}, falling
|
|
28
|
+
* back to the {@link AiGatewayConfig} defaults. When `stream` is `true` the
|
|
29
|
+
* returned promise resolves to an async-iterable of {@link StreamChunk};
|
|
30
|
+
* otherwise it resolves to a single {@link ExecuteResponse}.
|
|
31
|
+
*
|
|
32
|
+
* @param params - Request parameters: messages plus optional provider, model,
|
|
33
|
+
* attribution fields, delegation token, and provider-specific passthrough.
|
|
34
|
+
* @returns The completed response, or a stream of chunks when `stream: true`.
|
|
35
|
+
* @throws {GatewayError} On missing config (gateway_url/provider/model),
|
|
36
|
+
* network failure, or non-OK gateway/provider status.
|
|
37
|
+
* @throws {PolicyDeniedError} When the gateway denies the request by policy.
|
|
38
|
+
* @throws {QuotaExceededError} When a spend/quota limit is exceeded (HTTP 429).
|
|
39
|
+
* @throws {GatewayTimeoutError} When the request times out (client or HTTP 504).
|
|
40
|
+
*/
|
|
41
|
+
execute(params: ExecuteParams & {
|
|
42
|
+
stream?: false;
|
|
43
|
+
}): Promise<ExecuteResponse>;
|
|
44
|
+
execute(params: ExecuteParams & {
|
|
45
|
+
stream: true;
|
|
46
|
+
}): Promise<AsyncIterable<StreamChunk>>;
|
|
47
|
+
execute(params: ExecuteParams): Promise<ExecuteResponse | AsyncIterable<StreamChunk>>;
|
|
48
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AiGatewayClient = void 0;
|
|
4
|
+
const uuidv7_1 = require("uuidv7");
|
|
5
|
+
const errors_1 = require("./errors");
|
|
6
|
+
const providers_1 = require("./providers");
|
|
7
|
+
/**
|
|
8
|
+
* Client for the Axemere AI Gateway explicit action API.
|
|
9
|
+
*
|
|
10
|
+
* Wraps the gateway's `/v1/actions:execute` endpoint, building the wire request
|
|
11
|
+
* from {@link ExecuteParams}, applying attribution and delegation, and normalizing
|
|
12
|
+
* provider responses (OpenAI- and Anthropic-style) into a uniform shape.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const client = new AiGatewayClient(new AiGatewayConfig());
|
|
17
|
+
* const res = await client.execute({
|
|
18
|
+
* provider: "openai",
|
|
19
|
+
* model: "gpt-4o-mini",
|
|
20
|
+
* messages: [{ role: "user", content: "Hello" }],
|
|
21
|
+
* });
|
|
22
|
+
* console.log(res.content);
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
class AiGatewayClient {
|
|
26
|
+
constructor(config) {
|
|
27
|
+
this.config = config;
|
|
28
|
+
}
|
|
29
|
+
async execute(params) {
|
|
30
|
+
if (!this.config.gateway_url) {
|
|
31
|
+
throw new errors_1.GatewayError("gateway_url is required; set AXEMERE_GATEWAY_URL or pass it to AiGatewayConfig");
|
|
32
|
+
}
|
|
33
|
+
const provider = params.provider ?? this.config.default_provider;
|
|
34
|
+
const model = params.model ?? this.config.default_model;
|
|
35
|
+
if (!provider) {
|
|
36
|
+
throw new errors_1.GatewayError("provider is required");
|
|
37
|
+
}
|
|
38
|
+
if (!model) {
|
|
39
|
+
throw new errors_1.GatewayError("model is required");
|
|
40
|
+
}
|
|
41
|
+
const route = providers_1.PROVIDER_ROUTES[provider];
|
|
42
|
+
if (!route) {
|
|
43
|
+
throw new errors_1.GatewayError(`unknown provider: ${provider}`);
|
|
44
|
+
}
|
|
45
|
+
const workload_id = params.workload_id ?? this.config.workload_id;
|
|
46
|
+
const project_id = params.project_id ?? this.config.project_id;
|
|
47
|
+
const account_id = params.account_id ?? this.config.account_id;
|
|
48
|
+
const customer_id = params.customer_id ?? this.config.customer_id;
|
|
49
|
+
const labels = params.labels ?? this.config.labels;
|
|
50
|
+
const provider_api_key = params.provider_api_key ?? this.config.provider_api_key;
|
|
51
|
+
const target_path = route.path.replace("{model}", model);
|
|
52
|
+
// Build provider_params: strip known SDK fields, pass remaining through.
|
|
53
|
+
const { messages, provider: _p, model: _m, workload_id: _wl, project_id: _pi, account_id: _ai, customer_id: _ci, labels: _lb, provider_api_key: _pak, delegation_token, stream, ...extra_params } = params;
|
|
54
|
+
const provider_params = {
|
|
55
|
+
model,
|
|
56
|
+
messages,
|
|
57
|
+
...extra_params,
|
|
58
|
+
};
|
|
59
|
+
if (stream) {
|
|
60
|
+
provider_params["stream"] = true;
|
|
61
|
+
}
|
|
62
|
+
// Build wire body.
|
|
63
|
+
const body = {
|
|
64
|
+
schema: "mvgc.action_request.v2",
|
|
65
|
+
request_id: (0, uuidv7_1.uuidv7)(),
|
|
66
|
+
org_id: "",
|
|
67
|
+
workload_id: workload_id ?? "",
|
|
68
|
+
ingress_mode: "explicit_action_request",
|
|
69
|
+
action: {
|
|
70
|
+
type: "llm_chat",
|
|
71
|
+
method: "POST",
|
|
72
|
+
target_host: route.host,
|
|
73
|
+
target_path,
|
|
74
|
+
params: provider_params,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
// Attribution — omit if all fields are empty.
|
|
78
|
+
const attribution = {};
|
|
79
|
+
if (project_id)
|
|
80
|
+
attribution.project_id = project_id;
|
|
81
|
+
if (account_id)
|
|
82
|
+
attribution.account_id = account_id;
|
|
83
|
+
if (customer_id)
|
|
84
|
+
attribution.customer_id = customer_id;
|
|
85
|
+
if (labels && Object.keys(labels).length > 0)
|
|
86
|
+
attribution.labels = labels;
|
|
87
|
+
if (Object.keys(attribution).length > 0) {
|
|
88
|
+
body.attribution = attribution;
|
|
89
|
+
}
|
|
90
|
+
if (delegation_token) {
|
|
91
|
+
body.delegation_token = delegation_token;
|
|
92
|
+
}
|
|
93
|
+
if (provider_api_key) {
|
|
94
|
+
body.credential_hint = provider_api_key;
|
|
95
|
+
}
|
|
96
|
+
const headers = {
|
|
97
|
+
"Content-Type": "application/json",
|
|
98
|
+
};
|
|
99
|
+
if (this.config.gateway_token) {
|
|
100
|
+
headers["Authorization"] = `Bearer ${this.config.gateway_token}`;
|
|
101
|
+
}
|
|
102
|
+
const url = `${this.config.gateway_url}/v1/actions:execute`;
|
|
103
|
+
// Apply timeout to connection establishment for all requests.
|
|
104
|
+
// The timeout is cleared in the finally block once response headers arrive,
|
|
105
|
+
// so streaming payloads are not cut off by this timer.
|
|
106
|
+
const controller = new AbortController();
|
|
107
|
+
const timeoutMs = this.config.timeout * 1000;
|
|
108
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
109
|
+
let response;
|
|
110
|
+
try {
|
|
111
|
+
response = await fetch(url, {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers,
|
|
114
|
+
body: JSON.stringify(body),
|
|
115
|
+
signal: controller.signal,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
120
|
+
throw new errors_1.GatewayTimeoutError("Request timed out");
|
|
121
|
+
}
|
|
122
|
+
throw new errors_1.GatewayError(`Network error: ${err instanceof Error ? err.message : String(err)}`);
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
clearTimeout(timeoutId);
|
|
126
|
+
}
|
|
127
|
+
if (response.status === 504) {
|
|
128
|
+
throw new errors_1.GatewayTimeoutError("Gateway timeout (HTTP 504)");
|
|
129
|
+
}
|
|
130
|
+
if (stream) {
|
|
131
|
+
return parseStreamResponse(response, provider);
|
|
132
|
+
}
|
|
133
|
+
return parseResponse(response, provider);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
exports.AiGatewayClient = AiGatewayClient;
|
|
137
|
+
async function parseResponse(response, provider) {
|
|
138
|
+
let data;
|
|
139
|
+
try {
|
|
140
|
+
data = await response.json();
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
throw new errors_1.GatewayError(`Failed to parse JSON response: HTTP ${response.status}`);
|
|
144
|
+
}
|
|
145
|
+
const obj = data;
|
|
146
|
+
if (obj["decision"] === "deny") {
|
|
147
|
+
const err = new errors_1.PolicyDeniedError(`Request denied by policy: ${String(obj["reason"] ?? "unknown")}`);
|
|
148
|
+
err.status_code = response.status;
|
|
149
|
+
err.response_body = data;
|
|
150
|
+
err.reason = String(obj["reason"] ?? "");
|
|
151
|
+
err.trace = obj["trace"];
|
|
152
|
+
err.record_id = typeof obj["record_id"] === "string" ? obj["record_id"] : undefined;
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
if (response.status === 429 && obj["error"] === "quota_exceeded") {
|
|
156
|
+
const err = new errors_1.QuotaExceededError("Quota exceeded");
|
|
157
|
+
err.status_code = 429;
|
|
158
|
+
err.response_body = data;
|
|
159
|
+
err.upgrade_url = typeof obj["upgrade_url"] === "string" ? obj["upgrade_url"] : undefined;
|
|
160
|
+
err.retry_after = typeof obj["retry_after"] === "number" ? obj["retry_after"] : undefined;
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
const err = new errors_1.GatewayError(`HTTP ${response.status}`);
|
|
165
|
+
err.status_code = response.status;
|
|
166
|
+
err.response_body = data;
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
const result = obj["result"];
|
|
170
|
+
if (result && typeof result["status_code"] === "number" && result["status_code"] >= 400) {
|
|
171
|
+
const err = new errors_1.GatewayError(`Provider error: HTTP ${result["status_code"]}`);
|
|
172
|
+
err.status_code = result["status_code"];
|
|
173
|
+
err.response_body = result["body"];
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
const providerBody = result?.["body"];
|
|
177
|
+
let content = "";
|
|
178
|
+
if (providerBody) {
|
|
179
|
+
if (provider === "anthropic") {
|
|
180
|
+
// Anthropic returns an array of content blocks; concatenate every
|
|
181
|
+
// text block (tool_use / thinking blocks have no text and are skipped).
|
|
182
|
+
const contentArr = providerBody["content"];
|
|
183
|
+
const textBlocks = contentArr?.filter((b) => b.type === "text").map((b) => b.text ?? "") ?? [];
|
|
184
|
+
content = textBlocks.join("");
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
// OpenAI-compatible format
|
|
188
|
+
const choices = providerBody["choices"];
|
|
189
|
+
content = choices?.[0]?.message?.content ?? "";
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
content,
|
|
194
|
+
record_id: String(obj["record_id"] ?? ""),
|
|
195
|
+
metering: (obj["metering"] ?? {}),
|
|
196
|
+
provider,
|
|
197
|
+
model: String((providerBody?.["model"]) ?? (obj["model"]) ?? ""),
|
|
198
|
+
record_hash: typeof obj["record_hash"] === "string" ? obj["record_hash"] : undefined,
|
|
199
|
+
provider_response: providerBody,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function parseStreamResponse(response, provider) {
|
|
203
|
+
if (!response.ok) {
|
|
204
|
+
return {
|
|
205
|
+
[Symbol.asyncIterator]: async function* () {
|
|
206
|
+
let data;
|
|
207
|
+
try {
|
|
208
|
+
data = await response.json();
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
data = undefined;
|
|
212
|
+
}
|
|
213
|
+
const err = new errors_1.GatewayError(`HTTP ${response.status}`);
|
|
214
|
+
err.status_code = response.status;
|
|
215
|
+
err.response_body = data;
|
|
216
|
+
throw err;
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const body = response.body;
|
|
221
|
+
if (!body) {
|
|
222
|
+
return {
|
|
223
|
+
[Symbol.asyncIterator]: async function* () {
|
|
224
|
+
throw new errors_1.GatewayError("Empty response body on streaming request");
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
[Symbol.asyncIterator]: async function* () {
|
|
230
|
+
let recordId;
|
|
231
|
+
let metering;
|
|
232
|
+
const reader = body.getReader();
|
|
233
|
+
const decoder = new TextDecoder();
|
|
234
|
+
let buffer = "";
|
|
235
|
+
try {
|
|
236
|
+
while (true) {
|
|
237
|
+
const { done, value } = await reader.read();
|
|
238
|
+
if (done)
|
|
239
|
+
break;
|
|
240
|
+
buffer += decoder.decode(value, { stream: true });
|
|
241
|
+
// Split on newlines; keep any partial line in the buffer.
|
|
242
|
+
const lines = buffer.split("\n");
|
|
243
|
+
buffer = lines.pop() ?? "";
|
|
244
|
+
for (const line of lines) {
|
|
245
|
+
const trimmed = line.trim();
|
|
246
|
+
if (!trimmed || trimmed.startsWith(":"))
|
|
247
|
+
continue;
|
|
248
|
+
if (!trimmed.startsWith("data: "))
|
|
249
|
+
continue;
|
|
250
|
+
const payload = trimmed.slice(6).trim();
|
|
251
|
+
if (payload === "[DONE]") {
|
|
252
|
+
yield { content: "", is_final: true, record_id: recordId, metering };
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
let chunk;
|
|
256
|
+
try {
|
|
257
|
+
chunk = JSON.parse(payload);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
continue; // skip malformed chunks
|
|
261
|
+
}
|
|
262
|
+
if (chunk["type"] === "mvgc_metering") {
|
|
263
|
+
recordId = typeof chunk["record_id"] === "string"
|
|
264
|
+
? chunk["record_id"]
|
|
265
|
+
: undefined;
|
|
266
|
+
metering = chunk["metering"];
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (provider === "anthropic") {
|
|
270
|
+
// Anthropic delta format:
|
|
271
|
+
// {"type":"content_block_delta","delta":{"type":"text_delta","text":"hi"}}
|
|
272
|
+
if (chunk["type"] === "content_block_delta") {
|
|
273
|
+
const aDelta = chunk["delta"];
|
|
274
|
+
const text = aDelta?.text;
|
|
275
|
+
if (text) {
|
|
276
|
+
yield { content: text, is_final: false };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
// OpenAI-compatible delta format
|
|
282
|
+
const choices = chunk["choices"];
|
|
283
|
+
const delta = choices?.[0]?.delta?.content;
|
|
284
|
+
if (delta) {
|
|
285
|
+
yield { content: delta, is_final: false };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
reader.releaseLock();
|
|
292
|
+
}
|
|
293
|
+
// Stream ended without [DONE] — emit final chunk.
|
|
294
|
+
yield { content: "", is_final: true, record_id: recordId, metering };
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface AiGatewayOptions {
|
|
2
|
+
gateway_url?: string;
|
|
3
|
+
gateway_token?: string;
|
|
4
|
+
default_provider?: string;
|
|
5
|
+
default_model?: string;
|
|
6
|
+
workload_id?: string;
|
|
7
|
+
project_id?: string;
|
|
8
|
+
account_id?: string;
|
|
9
|
+
customer_id?: string;
|
|
10
|
+
labels?: Record<string, string>;
|
|
11
|
+
provider_api_key?: string;
|
|
12
|
+
timeout?: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Resolved configuration for talking to the Axemere AI Gateway.
|
|
16
|
+
*
|
|
17
|
+
* Each field is taken from the matching {@link AiGatewayOptions} value when
|
|
18
|
+
* provided, otherwise from its `AXEMERE_*` environment variable, otherwise a
|
|
19
|
+
* safe default. Values are read once at construction time. Also builds the
|
|
20
|
+
* proxy base URL used by the OpenAI/Anthropic drop-in wrappers.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* // From env (AXEMERE_GATEWAY_URL, AXEMERE_GATEWAY_TOKEN, ...)
|
|
25
|
+
* const config = new AiGatewayConfig();
|
|
26
|
+
* // Or explicit overrides
|
|
27
|
+
* const config2 = new AiGatewayConfig({ gateway_url: "http://localhost:7080" });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare class AiGatewayConfig {
|
|
31
|
+
readonly gateway_url: string;
|
|
32
|
+
readonly gateway_token: string;
|
|
33
|
+
default_provider: string;
|
|
34
|
+
default_model: string;
|
|
35
|
+
readonly workload_id: string;
|
|
36
|
+
readonly project_id: string;
|
|
37
|
+
readonly account_id: string;
|
|
38
|
+
readonly customer_id: string;
|
|
39
|
+
readonly labels: Record<string, string>;
|
|
40
|
+
readonly provider_api_key: string;
|
|
41
|
+
readonly timeout: number;
|
|
42
|
+
constructor(opts?: AiGatewayOptions);
|
|
43
|
+
/**
|
|
44
|
+
* Builds the proxy base URL for a given provider.
|
|
45
|
+
* Format: {gateway_url}/proxy/{provider}[/k/{gateway_token}][/w/{workload_id}]
|
|
46
|
+
* [/p/{project_id}][/a/{account_id}][/c/{customer_id}]/
|
|
47
|
+
*/
|
|
48
|
+
proxyUrl(provider: string): string;
|
|
49
|
+
setDefaults(opts: {
|
|
50
|
+
provider?: string;
|
|
51
|
+
model?: string;
|
|
52
|
+
}): void;
|
|
53
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AiGatewayConfig = void 0;
|
|
4
|
+
function parseLabelsEnv(raw) {
|
|
5
|
+
try {
|
|
6
|
+
const parsed = JSON.parse(raw);
|
|
7
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
8
|
+
const result = {};
|
|
9
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
10
|
+
result[k] = String(v);
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// Ignore parse errors — return empty
|
|
17
|
+
}
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolved configuration for talking to the Axemere AI Gateway.
|
|
22
|
+
*
|
|
23
|
+
* Each field is taken from the matching {@link AiGatewayOptions} value when
|
|
24
|
+
* provided, otherwise from its `AXEMERE_*` environment variable, otherwise a
|
|
25
|
+
* safe default. Values are read once at construction time. Also builds the
|
|
26
|
+
* proxy base URL used by the OpenAI/Anthropic drop-in wrappers.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* // From env (AXEMERE_GATEWAY_URL, AXEMERE_GATEWAY_TOKEN, ...)
|
|
31
|
+
* const config = new AiGatewayConfig();
|
|
32
|
+
* // Or explicit overrides
|
|
33
|
+
* const config2 = new AiGatewayConfig({ gateway_url: "http://localhost:7080" });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
class AiGatewayConfig {
|
|
37
|
+
constructor(opts = {}) {
|
|
38
|
+
// Constructor args take precedence over env vars (read at construction time).
|
|
39
|
+
this.gateway_url =
|
|
40
|
+
opts.gateway_url ?? process.env["AXEMERE_GATEWAY_URL"] ?? "";
|
|
41
|
+
this.gateway_token =
|
|
42
|
+
opts.gateway_token ?? process.env["AXEMERE_GATEWAY_TOKEN"] ?? "";
|
|
43
|
+
this.default_provider =
|
|
44
|
+
opts.default_provider ?? process.env["AXEMERE_PROVIDER"] ?? "";
|
|
45
|
+
this.default_model =
|
|
46
|
+
opts.default_model ?? process.env["AXEMERE_MODEL"] ?? "";
|
|
47
|
+
this.workload_id =
|
|
48
|
+
opts.workload_id ?? process.env["AXEMERE_WORKLOAD_ID"] ?? "";
|
|
49
|
+
this.project_id =
|
|
50
|
+
opts.project_id ?? process.env["AXEMERE_PROJECT_ID"] ?? "";
|
|
51
|
+
this.account_id =
|
|
52
|
+
opts.account_id ?? process.env["AXEMERE_ACCOUNT_ID"] ?? "";
|
|
53
|
+
this.customer_id =
|
|
54
|
+
opts.customer_id ?? process.env["AXEMERE_CUSTOMER_ID"] ?? "";
|
|
55
|
+
this.provider_api_key =
|
|
56
|
+
opts.provider_api_key ?? process.env["AXEMERE_PROVIDER_API_KEY"] ?? "";
|
|
57
|
+
// Timeout in seconds; falls back to 120 on missing/invalid input so an
|
|
58
|
+
// unparseable env value can never produce a NaN AbortController delay.
|
|
59
|
+
const timeoutEnv = process.env["AXEMERE_TIMEOUT_SECONDS"];
|
|
60
|
+
const rawTimeout = opts.timeout ?? (timeoutEnv ? parseInt(timeoutEnv, 10) : undefined);
|
|
61
|
+
this.timeout =
|
|
62
|
+
rawTimeout === undefined || isNaN(rawTimeout) || rawTimeout <= 0
|
|
63
|
+
? 120
|
|
64
|
+
: rawTimeout;
|
|
65
|
+
if (opts.labels !== undefined) {
|
|
66
|
+
this.labels = opts.labels;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const labelsEnv = process.env["AXEMERE_LABELS"];
|
|
70
|
+
this.labels = labelsEnv ? parseLabelsEnv(labelsEnv) : {};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Builds the proxy base URL for a given provider.
|
|
75
|
+
* Format: {gateway_url}/proxy/{provider}[/k/{gateway_token}][/w/{workload_id}]
|
|
76
|
+
* [/p/{project_id}][/a/{account_id}][/c/{customer_id}]/
|
|
77
|
+
*/
|
|
78
|
+
proxyUrl(provider) {
|
|
79
|
+
let url = `${this.gateway_url}/proxy/${provider}`;
|
|
80
|
+
if (this.gateway_token)
|
|
81
|
+
url += `/k/${this.gateway_token}`;
|
|
82
|
+
if (this.workload_id)
|
|
83
|
+
url += `/w/${this.workload_id}`;
|
|
84
|
+
if (this.project_id)
|
|
85
|
+
url += `/p/${this.project_id}`;
|
|
86
|
+
if (this.account_id)
|
|
87
|
+
url += `/a/${this.account_id}`;
|
|
88
|
+
if (this.customer_id)
|
|
89
|
+
url += `/c/${this.customer_id}`;
|
|
90
|
+
url += "/";
|
|
91
|
+
return url;
|
|
92
|
+
}
|
|
93
|
+
setDefaults(opts) {
|
|
94
|
+
if (opts.provider !== undefined)
|
|
95
|
+
this.default_provider = opts.provider;
|
|
96
|
+
if (opts.model !== undefined)
|
|
97
|
+
this.default_model = opts.model;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
exports.AiGatewayConfig = AiGatewayConfig;
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base error for all gateway failures (config, network, and non-OK HTTP
|
|
3
|
+
* responses from the gateway or upstream provider). More specific subclasses
|
|
4
|
+
* are thrown for policy denials, quota limits, and timeouts.
|
|
5
|
+
*
|
|
6
|
+
* @property status_code - HTTP status of the failing response, when available.
|
|
7
|
+
* @property response_body - Parsed response body, when available.
|
|
8
|
+
*/
|
|
9
|
+
export declare class GatewayError extends Error {
|
|
10
|
+
status_code?: number;
|
|
11
|
+
response_body?: unknown;
|
|
12
|
+
constructor(message: string);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Thrown when the gateway denies a request by policy.
|
|
16
|
+
*
|
|
17
|
+
* @property reason - Human-readable denial reason from the gateway.
|
|
18
|
+
* @property trace - Optional policy-evaluation trace explaining the decision.
|
|
19
|
+
* @property record_id - Audit ledger record id for the denied request, if any.
|
|
20
|
+
*/
|
|
21
|
+
export declare class PolicyDeniedError extends GatewayError {
|
|
22
|
+
reason: string;
|
|
23
|
+
trace?: unknown;
|
|
24
|
+
record_id?: string;
|
|
25
|
+
constructor(message: string);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Thrown when a spend or quota limit is exceeded (HTTP 429).
|
|
29
|
+
*
|
|
30
|
+
* @property upgrade_url - URL to raise the limit / upgrade the plan, if provided.
|
|
31
|
+
* @property retry_after - Seconds to wait before retrying, if provided.
|
|
32
|
+
*/
|
|
33
|
+
export declare class QuotaExceededError extends GatewayError {
|
|
34
|
+
upgrade_url?: string;
|
|
35
|
+
retry_after?: number;
|
|
36
|
+
constructor(message: string);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Thrown when a request times out — either the client-side AbortController
|
|
40
|
+
* deadline (`AXEMERE_TIMEOUT_SECONDS`) or an HTTP 504 from the gateway.
|
|
41
|
+
*/
|
|
42
|
+
export declare class GatewayTimeoutError extends GatewayError {
|
|
43
|
+
constructor(message: string);
|
|
44
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GatewayTimeoutError = exports.QuotaExceededError = exports.PolicyDeniedError = exports.GatewayError = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Base error for all gateway failures (config, network, and non-OK HTTP
|
|
6
|
+
* responses from the gateway or upstream provider). More specific subclasses
|
|
7
|
+
* are thrown for policy denials, quota limits, and timeouts.
|
|
8
|
+
*
|
|
9
|
+
* @property status_code - HTTP status of the failing response, when available.
|
|
10
|
+
* @property response_body - Parsed response body, when available.
|
|
11
|
+
*/
|
|
12
|
+
class GatewayError extends Error {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "GatewayError";
|
|
16
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.GatewayError = GatewayError;
|
|
20
|
+
/**
|
|
21
|
+
* Thrown when the gateway denies a request by policy.
|
|
22
|
+
*
|
|
23
|
+
* @property reason - Human-readable denial reason from the gateway.
|
|
24
|
+
* @property trace - Optional policy-evaluation trace explaining the decision.
|
|
25
|
+
* @property record_id - Audit ledger record id for the denied request, if any.
|
|
26
|
+
*/
|
|
27
|
+
class PolicyDeniedError extends GatewayError {
|
|
28
|
+
constructor(message) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "PolicyDeniedError";
|
|
31
|
+
this.reason = "";
|
|
32
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.PolicyDeniedError = PolicyDeniedError;
|
|
36
|
+
/**
|
|
37
|
+
* Thrown when a spend or quota limit is exceeded (HTTP 429).
|
|
38
|
+
*
|
|
39
|
+
* @property upgrade_url - URL to raise the limit / upgrade the plan, if provided.
|
|
40
|
+
* @property retry_after - Seconds to wait before retrying, if provided.
|
|
41
|
+
*/
|
|
42
|
+
class QuotaExceededError extends GatewayError {
|
|
43
|
+
constructor(message) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = "QuotaExceededError";
|
|
46
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.QuotaExceededError = QuotaExceededError;
|
|
50
|
+
/**
|
|
51
|
+
* Thrown when a request times out — either the client-side AbortController
|
|
52
|
+
* deadline (`AXEMERE_TIMEOUT_SECONDS`) or an HTTP 504 from the gateway.
|
|
53
|
+
*/
|
|
54
|
+
class GatewayTimeoutError extends GatewayError {
|
|
55
|
+
constructor(message) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = "GatewayTimeoutError";
|
|
58
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.GatewayTimeoutError = GatewayTimeoutError;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { AiGatewayConfig } from "./config";
|
|
2
|
+
export type { AiGatewayOptions } from "./config";
|
|
3
|
+
export { AiGatewayClient } from "./client";
|
|
4
|
+
export { GatewayError, PolicyDeniedError, QuotaExceededError, GatewayTimeoutError } from "./errors";
|
|
5
|
+
export { PROVIDER_ROUTES } from "./providers";
|
|
6
|
+
export type { ProviderRoute } from "./providers";
|
|
7
|
+
export type { Message, CostBreakdownItem, Metering, ExecuteResponse, StreamChunk, ExecuteParams, } from "./types";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PROVIDER_ROUTES = exports.GatewayTimeoutError = exports.QuotaExceededError = exports.PolicyDeniedError = exports.GatewayError = exports.AiGatewayClient = exports.AiGatewayConfig = void 0;
|
|
4
|
+
var config_1 = require("./config");
|
|
5
|
+
Object.defineProperty(exports, "AiGatewayConfig", { enumerable: true, get: function () { return config_1.AiGatewayConfig; } });
|
|
6
|
+
var client_1 = require("./client");
|
|
7
|
+
Object.defineProperty(exports, "AiGatewayClient", { enumerable: true, get: function () { return client_1.AiGatewayClient; } });
|
|
8
|
+
var errors_1 = require("./errors");
|
|
9
|
+
Object.defineProperty(exports, "GatewayError", { enumerable: true, get: function () { return errors_1.GatewayError; } });
|
|
10
|
+
Object.defineProperty(exports, "PolicyDeniedError", { enumerable: true, get: function () { return errors_1.PolicyDeniedError; } });
|
|
11
|
+
Object.defineProperty(exports, "QuotaExceededError", { enumerable: true, get: function () { return errors_1.QuotaExceededError; } });
|
|
12
|
+
Object.defineProperty(exports, "GatewayTimeoutError", { enumerable: true, get: function () { return errors_1.GatewayTimeoutError; } });
|
|
13
|
+
var providers_1 = require("./providers");
|
|
14
|
+
Object.defineProperty(exports, "PROVIDER_ROUTES", { enumerable: true, get: function () { return providers_1.PROVIDER_ROUTES; } });
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PROVIDER_ROUTES = void 0;
|
|
4
|
+
exports.PROVIDER_ROUTES = {
|
|
5
|
+
openai: { host: "api.openai.com", path: "/v1/chat/completions" },
|
|
6
|
+
anthropic: { host: "api.anthropic.com", path: "/v1/messages" },
|
|
7
|
+
gemini: { host: "generativelanguage.googleapis.com", path: "/v1beta/models/{model}:generateContent" },
|
|
8
|
+
cohere: { host: "api.cohere.com", path: "/v2/chat" },
|
|
9
|
+
mistral: { host: "api.mistral.ai", path: "/v1/chat/completions" },
|
|
10
|
+
groq: { host: "api.groq.com", path: "/openai/v1/chat/completions" },
|
|
11
|
+
deepseek: { host: "api.deepseek.com", path: "/v1/chat/completions" },
|
|
12
|
+
together: { host: "api.together.ai", path: "/v1/chat/completions" },
|
|
13
|
+
minimax: { host: "api.minimax.chat", path: "/v1/text/chatcompletion_v2" },
|
|
14
|
+
moonshot: { host: "api.moonshot.ai", path: "/v1/chat/completions" },
|
|
15
|
+
zhipu: { host: "api.z.ai", path: "/api/paas/v4/chat/completions" },
|
|
16
|
+
xai: { host: "api.x.ai", path: "/v1/chat/completions" },
|
|
17
|
+
perplexity: { host: "api.perplexity.ai", path: "/chat/completions" },
|
|
18
|
+
openrouter: { host: "openrouter.ai", path: "/api/v1/chat/completions" },
|
|
19
|
+
"nvidia-nim": { host: "integrate.api.nvidia.com", path: "/v1/chat/completions" },
|
|
20
|
+
upstage: { host: "api.upstage.ai", path: "/v1/chat/completions" },
|
|
21
|
+
fireworks: { host: "api.fireworks.ai", path: "/inference/v1/chat/completions" },
|
|
22
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface Message {
|
|
2
|
+
role: "system" | "user" | "assistant" | "tool";
|
|
3
|
+
content: string;
|
|
4
|
+
}
|
|
5
|
+
export interface CostBreakdownItem {
|
|
6
|
+
label: string;
|
|
7
|
+
tokens: number;
|
|
8
|
+
rate_per_million: string;
|
|
9
|
+
subtotal_usd: string;
|
|
10
|
+
}
|
|
11
|
+
export interface Metering {
|
|
12
|
+
cost_usd: string;
|
|
13
|
+
tokens_in: number;
|
|
14
|
+
tokens_out: number;
|
|
15
|
+
bytes_in?: number;
|
|
16
|
+
bytes_out?: number;
|
|
17
|
+
cache_hit_tokens?: number;
|
|
18
|
+
cache_miss_tokens?: number;
|
|
19
|
+
cache_creation_tokens?: number;
|
|
20
|
+
reasoning_tokens?: number;
|
|
21
|
+
pricing_config_version?: number;
|
|
22
|
+
org_pricing_config_version?: number;
|
|
23
|
+
markup_multiplier_applied?: string;
|
|
24
|
+
cost_breakdown?: CostBreakdownItem[];
|
|
25
|
+
}
|
|
26
|
+
export interface ExecuteResponse {
|
|
27
|
+
content: string;
|
|
28
|
+
record_id: string;
|
|
29
|
+
metering: Metering;
|
|
30
|
+
provider: string;
|
|
31
|
+
model: string;
|
|
32
|
+
record_hash?: string;
|
|
33
|
+
provider_response?: unknown;
|
|
34
|
+
}
|
|
35
|
+
export interface StreamChunk {
|
|
36
|
+
content: string;
|
|
37
|
+
is_final: boolean;
|
|
38
|
+
record_id?: string;
|
|
39
|
+
metering?: Metering;
|
|
40
|
+
}
|
|
41
|
+
export interface ExecuteParams {
|
|
42
|
+
messages: Message[];
|
|
43
|
+
provider?: string;
|
|
44
|
+
model?: string;
|
|
45
|
+
workload_id?: string;
|
|
46
|
+
project_id?: string;
|
|
47
|
+
account_id?: string;
|
|
48
|
+
customer_id?: string;
|
|
49
|
+
labels?: Record<string, string>;
|
|
50
|
+
provider_api_key?: string;
|
|
51
|
+
delegation_token?: string;
|
|
52
|
+
stream?: boolean;
|
|
53
|
+
[key: string]: unknown;
|
|
54
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@axemere/gateway",
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"description": "Axemere AI Gateway SDK — core client, config, and proxy URL builder",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/Axemere-LLC/axemere-node.git",
|
|
18
|
+
"directory": "packages/gateway"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"uuidv7": "^1.0.2"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"typescript": "^5.4.0",
|
|
28
|
+
"@types/node": "^20.0.0",
|
|
29
|
+
"jest": "^29.0.0",
|
|
30
|
+
"@types/jest": "^29.0.0",
|
|
31
|
+
"ts-jest": "^29.0.0"
|
|
32
|
+
},
|
|
33
|
+
"jest": {
|
|
34
|
+
"preset": "ts-jest",
|
|
35
|
+
"testEnvironment": "node",
|
|
36
|
+
"extensionsToTreatAsEsm": [],
|
|
37
|
+
"transform": {
|
|
38
|
+
"^.+\\.tsx?$": [
|
|
39
|
+
"ts-jest",
|
|
40
|
+
{
|
|
41
|
+
"useESM": false
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsc",
|
|
48
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|
|
49
|
+
}
|
|
50
|
+
}
|