@pkent/aigateway 1.0.0 → 1.2.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 +56 -6
- package/package.json +2 -2
- package/src/AIGateway.js +4 -2
- package/src/providers/aibroker.js +22 -0
- package/src/providers/anthropic.js +7 -3
- package/src/providers/openaiCompatible.js +13 -3
- package/src/providers/registry.js +2 -1
- package/src/providers/requestOptions.js +14 -0
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A provider-neutral LLM client library. One class routes chat / vision /
|
|
4
4
|
streaming requests to OpenAI, Anthropic, Qwen (Alibaba DashScope), GLM (Zhipu
|
|
5
|
-
AI), or
|
|
6
|
-
single shape.
|
|
5
|
+
AI), OpenRouter, or an OpenAI-compatible gateway (`aibroker`) based on the model
|
|
6
|
+
name, and normalizes every response into a single shape.
|
|
7
7
|
|
|
8
8
|
No server, no `.env` — all configuration is passed into the constructor.
|
|
9
9
|
|
|
@@ -49,8 +49,9 @@ new AIGateway(model, key, options?)
|
|
|
49
49
|
|
|
50
50
|
| Option | Type | Applies to | Default | Description |
|
|
51
51
|
|-------------|--------|-----------------|------------------|-------------|
|
|
52
|
-
| `baseURL` | string | all | provider default | Override the upstream endpoint (regional endpoints, proxies, self-hosted). |
|
|
52
|
+
| `baseURL` | string | all | provider default | Override the upstream endpoint (regional endpoints, proxies, self-hosted). **Required** for `aibroker` (no default). |
|
|
53
53
|
| `maxTokens` | number | all | unset (see below)| Default max output tokens; a per-call `maxTokens` overrides it. |
|
|
54
|
+
| `timeout` | number | all | unset (SDK default) | Default per-call request timeout in ms; a per-call `timeout` overrides it. |
|
|
54
55
|
| `referer` | string | OpenRouter only | omitted | Sent as the `HTTP-Referer` attribution header. |
|
|
55
56
|
| `title` | string | OpenRouter only | omitted | Sent as the `X-Title` attribution header. |
|
|
56
57
|
| `client` | object | advanced | — | Inject a pre-built SDK client (or a compatible fake for testing). When set, `key`/`baseURL`/`referer`/`title` are not used to build a client. |
|
|
@@ -70,7 +71,8 @@ AIGateway.providers()
|
|
|
70
71
|
// { id: 'anthropic', prefix: 'anthropic/' },
|
|
71
72
|
// { id: 'qwen', prefix: 'qwen/' },
|
|
72
73
|
// { id: 'glm', prefix: 'glm/' },
|
|
73
|
-
// { id: 'openai', prefix: 'openai/' }
|
|
74
|
+
// { id: 'openai', prefix: 'openai/' },
|
|
75
|
+
// { id: 'aibroker', prefix: 'aibroker/' }
|
|
74
76
|
// ]
|
|
75
77
|
```
|
|
76
78
|
|
|
@@ -81,8 +83,9 @@ AIGateway.providers()
|
|
|
81
83
|
|
|
82
84
|
`vision` is `chat` with image content blocks in `messages`. Both return the
|
|
83
85
|
[v2 response shape](#response-shape). `options` may include `temperature`,
|
|
84
|
-
`maxTokens`,
|
|
85
|
-
|
|
86
|
+
`maxTokens`, `responseFormat`, `signal` (an `AbortSignal` to cancel the
|
|
87
|
+
request), and `timeout` (ms). (`responseFormat` is mapped to OpenAI-compatible
|
|
88
|
+
`response_format`; ignored by Anthropic.)
|
|
86
89
|
|
|
87
90
|
```js
|
|
88
91
|
const g = new AIGateway('openai/gpt-4o', OPENAI_KEY);
|
|
@@ -118,6 +121,9 @@ If the upstream errors, the iterator throws and `.final` rejects with the same
|
|
|
118
121
|
error. You may await `.final` without iterating (deltas buffer in memory), or
|
|
119
122
|
iterate without awaiting `.final`.
|
|
120
123
|
|
|
124
|
+
Accepts the same `signal`/`timeout` options; aborting the signal rejects both
|
|
125
|
+
the iterator and `.final`.
|
|
126
|
+
|
|
121
127
|
## Model routing
|
|
122
128
|
|
|
123
129
|
Every model is addressed as `<provider>/<model>`. The leading provider segment
|
|
@@ -132,10 +138,25 @@ catch-all — an unknown or missing segment throws `AIGatewayError`
|
|
|
132
138
|
| `qwen/<model>` | `qwen` | `<model>` | OpenAI-compatible. Default base URL `https://dashscope-intl.aliyuncs.com/compatible-mode/v1`. |
|
|
133
139
|
| `glm/<model>` | `glm` | `<model>` | OpenAI-compatible. Default base URL `https://open.bigmodel.cn/api/paas/v4`. |
|
|
134
140
|
| `openrouter/<vendor>/<model>` | `openrouter` | `<vendor>/<model>` | Only the `openrouter/` segment is stripped, leaving OpenRouter's native id. Default base URL `https://openrouter.ai/api/v1`. |
|
|
141
|
+
| `aibroker/<remainder>` | `aibroker` | `<remainder>` | OpenAI-compatible gateway meta-provider. Only the `aibroker/` segment is stripped; the remainder is forwarded verbatim and the gateway does its own routing. **Requires** `baseURL` (no default) — the constructor throws `AIGatewayError` (`code: 'missing_base_url'`) without it. |
|
|
135
142
|
|
|
136
143
|
OpenRouter is opt-in: send `openrouter/anthropic/claude-opus-4.8` to route
|
|
137
144
|
through OpenRouter, or `anthropic/claude-opus-4.8` to hit Anthropic directly.
|
|
138
145
|
|
|
146
|
+
`aibroker` is a gateway meta-provider: it forwards to *any* OpenAI-compatible
|
|
147
|
+
gateway you point it at, so it embeds no host and **requires** a `baseURL`. Only
|
|
148
|
+
the leading `aibroker/` segment is stripped — everything after it is sent
|
|
149
|
+
upstream as the model id, letting the gateway do its own routing
|
|
150
|
+
(`aibroker/openai/chatgpt-5.5` → `openai/chatgpt-5.5`,
|
|
151
|
+
`aibroker/openrouter/openai/chatgpt-5.5` → `openrouter/openai/chatgpt-5.5`). The
|
|
152
|
+
key is passed straight through as the Bearer for the gateway.
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
const g = new AIGateway('aibroker/openai/chatgpt-5.5', GATEWAY_TOKEN, {
|
|
156
|
+
baseURL: 'https://your-gateway/v1',
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
139
160
|
**Adding a provider:** drop a module into [src/providers/](src/providers/)
|
|
140
161
|
exporting `{ id, prefix, matches, create }` (where `matches` tests the
|
|
141
162
|
`<id>/` segment) and register it in
|
|
@@ -206,6 +227,32 @@ behavior:
|
|
|
206
227
|
Supply `maxTokens` (in the constructor or per call) to apply a cap to every
|
|
207
228
|
provider. A per-call value overrides the constructor default.
|
|
208
229
|
|
|
230
|
+
## Cancellation & timeouts
|
|
231
|
+
|
|
232
|
+
Pass an `AbortSignal` as `signal` to cancel an in-flight request; pass
|
|
233
|
+
`timeout` (ms) to bound it. Both are forwarded to the underlying provider
|
|
234
|
+
SDK's request options and apply to `chat`, `vision`, and `stream`.
|
|
235
|
+
|
|
236
|
+
```js
|
|
237
|
+
const controller = new AbortController();
|
|
238
|
+
const res = g.chat(messages, { signal: controller.signal });
|
|
239
|
+
// ...later:
|
|
240
|
+
controller.abort(); // res rejects with the SDK's APIUserAbortError
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
For a hard wall-clock cap that also honors an external cancel, combine a
|
|
244
|
+
timeout signal with your own:
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
const signal = AbortSignal.any([AbortSignal.timeout(120_000), runSignal]);
|
|
248
|
+
const res = await g.chat(messages, { signal });
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The `timeout` option is a per-attempt connection timeout and may be retried
|
|
252
|
+
by the SDK; prefer a combined `signal` (as above) when you need a guaranteed
|
|
253
|
+
wall-clock bound. Aborting a stream rejects both the async iterator and the
|
|
254
|
+
`.final` promise.
|
|
255
|
+
|
|
209
256
|
## Errors
|
|
210
257
|
|
|
211
258
|
- **Input errors** (invalid model/key/messages) throw `AIGatewayError` with a
|
|
@@ -214,6 +261,9 @@ provider. A per-call value overrides the constructor default.
|
|
|
214
261
|
`invalid_message_content`).
|
|
215
262
|
- **Upstream/provider errors** propagate as the underlying SDK error — `chat` /
|
|
216
263
|
`vision` reject; `stream` throws on the iterator and rejects `.final`.
|
|
264
|
+
- An aborted request rejects with the SDK's `APIUserAbortError`; a timed-out
|
|
265
|
+
request with `APIConnectionTimeoutError`. Like other provider errors, these
|
|
266
|
+
propagate unchanged (not wrapped in `AIGatewayError`).
|
|
217
267
|
|
|
218
268
|
```js
|
|
219
269
|
import AIGateway, { AIGatewayError } from '@pkent/aigateway';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pkent/aigateway",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A provider-neutral LLM client library for OpenAI, Anthropic, Qwen, GLM, and OpenRouter",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "node --test",
|
|
12
|
-
"prepublishOnly": "node --test test/aigateway.test.js test/cache-hints.test.js test/usage-normalization.test.js"
|
|
12
|
+
"prepublishOnly": "node --test test/aigateway.test.js test/cache-hints.test.js test/usage-normalization.test.js test/cancellation.test.js test/aibroker.test.js"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"src",
|
package/src/AIGateway.js
CHANGED
|
@@ -27,7 +27,7 @@ export class AIGateway {
|
|
|
27
27
|
);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const { baseURL, maxTokens, referer, title, client } = options;
|
|
30
|
+
const { baseURL, maxTokens, timeout, referer, title, client } = options;
|
|
31
31
|
|
|
32
32
|
if (!client && (typeof key !== 'string' || key.trim() === '')) {
|
|
33
33
|
throw new AIGatewayError('Missing or invalid API key', { code: 'invalid_api_key' });
|
|
@@ -36,7 +36,7 @@ export class AIGateway {
|
|
|
36
36
|
this.#model = model;
|
|
37
37
|
this.#providerId = entry.id;
|
|
38
38
|
this.#provider = entry.create({ apiKey: key, baseURL, referer, title, client });
|
|
39
|
-
this.#defaults = { maxTokens };
|
|
39
|
+
this.#defaults = { maxTokens, timeout };
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
get model() {
|
|
@@ -57,6 +57,8 @@ export class AIGateway {
|
|
|
57
57
|
maxTokens: callOptions.maxTokens ?? this.#defaults.maxTokens,
|
|
58
58
|
temperature: callOptions.temperature,
|
|
59
59
|
responseFormat: callOptions.responseFormat,
|
|
60
|
+
signal: callOptions.signal,
|
|
61
|
+
timeout: callOptions.timeout ?? this.#defaults.timeout,
|
|
60
62
|
};
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createOpenAICompatibleProvider } from './openaiCompatible.js';
|
|
2
|
+
import { AIGatewayError } from '../errors.js';
|
|
3
|
+
|
|
4
|
+
// "aibroker/<provider>/<model>" routes to an OpenAI-compatible gateway. The
|
|
5
|
+
// "aibroker/" segment is stripped (existing stripProviderPrefix) and the rest is
|
|
6
|
+
// forwarded verbatim as the model — the gateway does its own routing. The
|
|
7
|
+
// caller MUST supply the gateway URL via options.baseURL; there is no default.
|
|
8
|
+
export default {
|
|
9
|
+
id: 'aibroker',
|
|
10
|
+
prefix: 'aibroker/',
|
|
11
|
+
matches: model => typeof model === 'string' && model.startsWith('aibroker/'),
|
|
12
|
+
create({ apiKey, baseURL, client }) {
|
|
13
|
+
if (!client && !baseURL) {
|
|
14
|
+
throw new AIGatewayError(
|
|
15
|
+
'The "aibroker" provider requires a baseURL (the gateway URL), e.g. ' +
|
|
16
|
+
'new AIGateway(model, key, { baseURL: "https://your-gateway/v1" })',
|
|
17
|
+
{ code: 'missing_base_url' },
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return createOpenAICompatibleProvider({ id: 'aibroker', apiKey, baseURL, client });
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
2
|
import { convertMessagesForAnthropic } from './messageTransforms.js';
|
|
3
3
|
import { stripProviderPrefix } from './modelId.js';
|
|
4
|
+
import { buildRequestOptions } from './requestOptions.js';
|
|
4
5
|
import {
|
|
5
6
|
buildV2Response,
|
|
6
7
|
extractAnthropicUsage,
|
|
@@ -40,15 +41,18 @@ function createProvider({ apiKey, baseURL, client }) {
|
|
|
40
41
|
id: 'anthropic',
|
|
41
42
|
|
|
42
43
|
async chat(model, messages, options = {}) {
|
|
43
|
-
return normalize(await anthropic.messages.create(buildRequest(model, messages, options)), model);
|
|
44
|
+
return normalize(await anthropic.messages.create(buildRequest(model, messages, options), buildRequestOptions(options)), model);
|
|
44
45
|
},
|
|
45
46
|
|
|
46
47
|
async vision(model, messages, options = {}) {
|
|
47
|
-
return normalize(await anthropic.messages.create(buildRequest(model, messages, options)), model);
|
|
48
|
+
return normalize(await anthropic.messages.create(buildRequest(model, messages, options), buildRequestOptions(options)), model);
|
|
48
49
|
},
|
|
49
50
|
|
|
50
51
|
async stream(model, messages, options = {}, { emitDelta }) {
|
|
51
|
-
const upstream = await anthropic.messages.create(
|
|
52
|
+
const upstream = await anthropic.messages.create(
|
|
53
|
+
{ ...buildRequest(model, messages, options), stream: true },
|
|
54
|
+
buildRequestOptions(options),
|
|
55
|
+
);
|
|
52
56
|
|
|
53
57
|
let streamId = `anthropic_${Date.now()}`;
|
|
54
58
|
const created = Math.floor(Date.now() / 1000);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
2
|
import { stripCacheHintsFromMessages } from './messageTransforms.js';
|
|
3
3
|
import { stripProviderPrefix } from './modelId.js';
|
|
4
|
+
import { buildRequestOptions } from './requestOptions.js';
|
|
4
5
|
import {
|
|
5
6
|
buildV2Response,
|
|
6
7
|
extractOpenAIUsage,
|
|
@@ -53,15 +54,24 @@ export function createOpenAICompatibleProvider({
|
|
|
53
54
|
id,
|
|
54
55
|
|
|
55
56
|
async chat(model, messages, options = {}) {
|
|
56
|
-
return normalize(
|
|
57
|
+
return normalize(
|
|
58
|
+
await openai.chat.completions.create(buildRequest(model, messages, options), buildRequestOptions(options)),
|
|
59
|
+
model,
|
|
60
|
+
);
|
|
57
61
|
},
|
|
58
62
|
|
|
59
63
|
async vision(model, messages, options = {}) {
|
|
60
|
-
return normalize(
|
|
64
|
+
return normalize(
|
|
65
|
+
await openai.chat.completions.create(buildRequest(model, messages, options), buildRequestOptions(options)),
|
|
66
|
+
model,
|
|
67
|
+
);
|
|
61
68
|
},
|
|
62
69
|
|
|
63
70
|
async stream(model, messages, options = {}, { emitDelta }) {
|
|
64
|
-
const upstream = await openai.chat.completions.create(
|
|
71
|
+
const upstream = await openai.chat.completions.create(
|
|
72
|
+
buildRequest(model, messages, options, { stream: true }),
|
|
73
|
+
buildRequestOptions(options),
|
|
74
|
+
);
|
|
65
75
|
|
|
66
76
|
let streamId = `${id}_${Date.now()}`;
|
|
67
77
|
let created = Math.floor(Date.now() / 1000);
|
|
@@ -3,10 +3,11 @@ import anthropic from './anthropic.js';
|
|
|
3
3
|
import qwen from './qwen.js';
|
|
4
4
|
import glm from './glm.js';
|
|
5
5
|
import openai from './openai.js';
|
|
6
|
+
import aibroker from './aibroker.js';
|
|
6
7
|
|
|
7
8
|
// Every model is addressed as "<provider>/<model>". The provider segments are
|
|
8
9
|
// disjoint, so resolution order does not matter; there is no catch-all.
|
|
9
|
-
const ENTRIES = [openrouter, anthropic, qwen, glm, openai];
|
|
10
|
+
const ENTRIES = [openrouter, anthropic, qwen, glm, openai, aibroker];
|
|
10
11
|
|
|
11
12
|
export function resolveProviderEntry(model) {
|
|
12
13
|
for (const entry of ENTRIES) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Maps the provider-neutral transport options onto the SDK's per-call
|
|
2
|
+
// "request options" object — the SECOND argument to anthropic.messages.create /
|
|
3
|
+
// openai.chat.completions.create. Both SDKs accept { signal, timeout } there.
|
|
4
|
+
//
|
|
5
|
+
// These are transport concerns and must NEVER be placed in the request body.
|
|
6
|
+
// Each field is included only when set (mirroring the conditional-spread style
|
|
7
|
+
// used for body fields), so an absent signal/timeout leaves SDK defaults intact
|
|
8
|
+
// and an omitted-options call sends an empty {} — a no-op for both SDKs.
|
|
9
|
+
export function buildRequestOptions({ signal, timeout } = {}) {
|
|
10
|
+
return {
|
|
11
|
+
...(signal != null && { signal }),
|
|
12
|
+
...(timeout != null && { timeout }),
|
|
13
|
+
};
|
|
14
|
+
}
|