@pkent/aigateway 1.0.0 → 1.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 +36 -2
- package/package.json +2 -2
- package/src/AIGateway.js +4 -2
- package/src/providers/anthropic.js +7 -3
- package/src/providers/openaiCompatible.js +13 -3
- package/src/providers/requestOptions.js +14 -0
package/README.md
CHANGED
|
@@ -51,6 +51,7 @@ new AIGateway(model, key, options?)
|
|
|
51
51
|
|-------------|--------|-----------------|------------------|-------------|
|
|
52
52
|
| `baseURL` | string | all | provider default | Override the upstream endpoint (regional endpoints, proxies, self-hosted). |
|
|
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. |
|
|
@@ -81,8 +82,9 @@ AIGateway.providers()
|
|
|
81
82
|
|
|
82
83
|
`vision` is `chat` with image content blocks in `messages`. Both return the
|
|
83
84
|
[v2 response shape](#response-shape). `options` may include `temperature`,
|
|
84
|
-
`maxTokens`,
|
|
85
|
-
|
|
85
|
+
`maxTokens`, `responseFormat`, `signal` (an `AbortSignal` to cancel the
|
|
86
|
+
request), and `timeout` (ms). (`responseFormat` is mapped to OpenAI-compatible
|
|
87
|
+
`response_format`; ignored by Anthropic.)
|
|
86
88
|
|
|
87
89
|
```js
|
|
88
90
|
const g = new AIGateway('openai/gpt-4o', OPENAI_KEY);
|
|
@@ -118,6 +120,9 @@ If the upstream errors, the iterator throws and `.final` rejects with the same
|
|
|
118
120
|
error. You may await `.final` without iterating (deltas buffer in memory), or
|
|
119
121
|
iterate without awaiting `.final`.
|
|
120
122
|
|
|
123
|
+
Accepts the same `signal`/`timeout` options; aborting the signal rejects both
|
|
124
|
+
the iterator and `.final`.
|
|
125
|
+
|
|
121
126
|
## Model routing
|
|
122
127
|
|
|
123
128
|
Every model is addressed as `<provider>/<model>`. The leading provider segment
|
|
@@ -206,6 +211,32 @@ behavior:
|
|
|
206
211
|
Supply `maxTokens` (in the constructor or per call) to apply a cap to every
|
|
207
212
|
provider. A per-call value overrides the constructor default.
|
|
208
213
|
|
|
214
|
+
## Cancellation & timeouts
|
|
215
|
+
|
|
216
|
+
Pass an `AbortSignal` as `signal` to cancel an in-flight request; pass
|
|
217
|
+
`timeout` (ms) to bound it. Both are forwarded to the underlying provider
|
|
218
|
+
SDK's request options and apply to `chat`, `vision`, and `stream`.
|
|
219
|
+
|
|
220
|
+
```js
|
|
221
|
+
const controller = new AbortController();
|
|
222
|
+
const res = g.chat(messages, { signal: controller.signal });
|
|
223
|
+
// ...later:
|
|
224
|
+
controller.abort(); // res rejects with the SDK's APIUserAbortError
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
For a hard wall-clock cap that also honors an external cancel, combine a
|
|
228
|
+
timeout signal with your own:
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
const signal = AbortSignal.any([AbortSignal.timeout(120_000), runSignal]);
|
|
232
|
+
const res = await g.chat(messages, { signal });
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
The `timeout` option is a per-attempt connection timeout and may be retried
|
|
236
|
+
by the SDK; prefer a combined `signal` (as above) when you need a guaranteed
|
|
237
|
+
wall-clock bound. Aborting a stream rejects both the async iterator and the
|
|
238
|
+
`.final` promise.
|
|
239
|
+
|
|
209
240
|
## Errors
|
|
210
241
|
|
|
211
242
|
- **Input errors** (invalid model/key/messages) throw `AIGatewayError` with a
|
|
@@ -214,6 +245,9 @@ provider. A per-call value overrides the constructor default.
|
|
|
214
245
|
`invalid_message_content`).
|
|
215
246
|
- **Upstream/provider errors** propagate as the underlying SDK error — `chat` /
|
|
216
247
|
`vision` reject; `stream` throws on the iterator and rejects `.final`.
|
|
248
|
+
- An aborted request rejects with the SDK's `APIUserAbortError`; a timed-out
|
|
249
|
+
request with `APIConnectionTimeoutError`. Like other provider errors, these
|
|
250
|
+
propagate unchanged (not wrapped in `AIGatewayError`).
|
|
217
251
|
|
|
218
252
|
```js
|
|
219
253
|
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.1.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"
|
|
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
|
|
|
@@ -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);
|
|
@@ -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
|
+
}
|