bare-agent 0.33.1 → 0.34.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/bareagent.context.md +3 -1
- package/package.json +1 -1
- package/src/provider-anthropic.d.ts +9 -2
- package/src/provider-anthropic.js +10 -3
- package/src/provider-gemini.d.ts +9 -2
- package/src/provider-gemini.js +10 -3
- package/src/provider-http.d.ts +34 -0
- package/src/provider-http.js +59 -0
- package/src/provider-ollama.d.ts +9 -2
- package/src/provider-ollama.js +10 -3
- package/src/provider-openai.d.ts +15 -2
- package/src/provider-openai.js +13 -3
package/bareagent.context.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# bareagent — Integration Guide
|
|
2
2
|
|
|
3
3
|
> For AI assistants and developers wiring bareagent into a project.
|
|
4
|
-
> v0.
|
|
4
|
+
> v0.34.0 | Node.js >= 18 | zero required deps (`bareguard >=0.9.0 <0.13.0` optional peer for governance) | Apache 2.0
|
|
5
5
|
>
|
|
6
6
|
> Full human guide with composition examples, design philosophy, and recipes: [Usage Guide](docs/02-features/usage-guide.md)
|
|
7
7
|
|
|
@@ -829,6 +829,8 @@ All return `{ text, toolCalls, usage: { inputTokens, outputTokens }, model?, cos
|
|
|
829
829
|
|
|
830
830
|
**Error body (v0.11.0):** on an HTTP error the OpenAI/Anthropic/Ollama providers throw a `ProviderError` whose `message` carries the upstream error string. The full parsed response is **not** attached to `err.body` by default (so an unexpected field can't leak through logs that dump the error object). Pass `{ exposeErrorBody: true }` to attach it for debugging.
|
|
831
831
|
|
|
832
|
+
**Request/idle timeout (BA-18, v0.34.0):** the four http(s) providers (Anthropic, OpenAI, Gemini, Ollama) accept a `timeoutMs` option — constructor default **600000 (10 min)**, overridable per call via `generate(..., { timeoutMs })`, and `0`/`Infinity` disables it. Before this they wired only `req.on('error')`, so a socket the server silently dropped — or a response that never starts — hung `generate()` until the OS TCP timeout (~2h): a hang, not an error, so retry/casualty policy above it never fired. `timeoutMs` bounds on socket **inactivity** (`req.setTimeout`), so a slow-but-streaming response is not killed — only a silent/never-answering socket trips it; the 10-min default clears any single non-streaming completion (TTFB ≈ generation time). On trip, `generate()` rejects with a retryable `TimeoutError` (`code: 'ETIMEDOUT'`, `retryable: true`). **Retry is caller-side and already wired:** `new Loop({ provider, retry: new Retry() })` wraps `provider.generate`, and `DEFAULT_RETRY_ON` classifies `ETIMEDOUT` (and `ECONNRESET`/`ENOTFOUND`/429/5xx) as transient — so a wired `Retry` retries a timed-out request and rethrows under `retryOn: () => false`, with no extra wiring (`run-plan`'s `stepRetry` is a second consumer of the same seam). CLIPipe already bounded its child process (`timeout`, default 30000 for one-shot) and is unchanged.
|
|
833
|
+
|
|
832
834
|
**Plaintext-key warning (Unreleased):** the OpenAI provider's `baseUrl` accepts `http://` (for local/OpenAI-compatible endpoints), but a `Bearer` key sent over plaintext http to a **non-loopback** host is exposed on the wire. The provider now warns once when that happens. Loopback hosts (`localhost`/`127.0.0.0/8`/`::1` — local proxies, Ollama-style endpoints) stay silent, since that's the legitimate keyless-local case. The header is **not** stripped (some local proxies want a key), so use `https` for any remote endpoint, or drop `apiKey` when the local endpoint needs none.
|
|
833
835
|
|
|
834
836
|
**Cost estimation:** Loop automatically estimates USD cost per run based on model and token usage. The `cost` field appears in every `loop.run()` result and in `loop:done` stream events. Pricing covers OpenAI and Anthropic models; unknown models use a default average. To adjust rates, edit `COST_PER_1K` at the top of `src/loop.js`. The model is resolved as `result.model || provider.model` (v0.16.1+) — providers now echo the model in their `generate()` result, so cost accounting holds even when `provider.model` is absent or varies per response, e.g. behind `FallbackProvider` or `CircuitBreaker.wrapProvider` (the wrapper also preserves `model`/`name` passthrough props). Wire `onLlmResult` (via `wireGate`) and a `budget.maxCostUsd` cap then halts on token-heavy workloads too.
|
package/package.json
CHANGED
|
@@ -32,6 +32,10 @@ export type AnthropicOptions = {
|
|
|
32
32
|
* - Attach the full upstream response to `err.body` on HTTP errors (off by default to avoid leaking unexpected fields through error logs; `err.message` still carries the API error).
|
|
33
33
|
*/
|
|
34
34
|
exposeErrorBody?: boolean | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* - BA-18: request/idle timeout in ms. A silent or never-answering socket (dropped by the server, or a response that never starts) was otherwise bounded only by the OS TCP timeout (~2h) — a hang, not an error, so every retry policy above it was inert. Bounds on socket INACTIVITY (the timer resets on activity, so a slow-but-streaming response is not killed); on trip, `generate()` rejects with a retryable `TimeoutError` (`code: 'ETIMEDOUT'`) that a wired `Retry` (`Loop({ retry })`) retries. Default 10 min sits above any single non-streaming completion; `0` or `Infinity` disables it (pre-BA-18 behaviour). Overridable per call via `generate(..., { timeoutMs })`.
|
|
37
|
+
*/
|
|
38
|
+
timeoutMs?: number | undefined;
|
|
35
39
|
};
|
|
36
40
|
/** @typedef {import('../types').Message} Message */
|
|
37
41
|
/** @typedef {import('../types').ToolDef} ToolDef */
|
|
@@ -47,6 +51,7 @@ export type AnthropicOptions = {
|
|
|
47
51
|
*
|
|
48
52
|
* **MEASURED CAVEAT — this option does not "turn thinking on".** On `claude-sonnet-5` adaptive thinking is ALREADY the default: sending this changed the observed thinking rate not at all (2/10 rounds with it vs 3/10 without — `poc/ba7-adaptive-default.mjs`). Its real use is pinning the mode and reaching `display`/`effort`. The change that mattered is that thinking blocks are now PRESERVED and replayed (see `Message.providerBlocks`), which happens whether or not you ever set this.
|
|
49
53
|
* @property {boolean} [exposeErrorBody=false] - Attach the full upstream response to `err.body` on HTTP errors (off by default to avoid leaking unexpected fields through error logs; `err.message` still carries the API error).
|
|
54
|
+
* @property {number} [timeoutMs=600000] - BA-18: request/idle timeout in ms. A silent or never-answering socket (dropped by the server, or a response that never starts) was otherwise bounded only by the OS TCP timeout (~2h) — a hang, not an error, so every retry policy above it was inert. Bounds on socket INACTIVITY (the timer resets on activity, so a slow-but-streaming response is not killed); on trip, `generate()` rejects with a retryable `TimeoutError` (`code: 'ETIMEDOUT'`) that a wired `Retry` (`Loop({ retry })`) retries. Default 10 min sits above any single non-streaming completion; `0` or `Infinity` disables it (pre-BA-18 behaviour). Overridable per call via `generate(..., { timeoutMs })`.
|
|
50
55
|
*/
|
|
51
56
|
export class AnthropicProvider {
|
|
52
57
|
/**
|
|
@@ -61,11 +66,12 @@ export class AnthropicProvider {
|
|
|
61
66
|
cacheMessages: boolean;
|
|
62
67
|
thinking: any;
|
|
63
68
|
exposeErrorBody: boolean;
|
|
69
|
+
timeoutMs: number | undefined;
|
|
64
70
|
/**
|
|
65
71
|
* Generate a response from the Anthropic API.
|
|
66
72
|
* @param {Message[]} messages - Conversation messages (OpenAI format, auto-converted).
|
|
67
73
|
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
68
|
-
* @param {Record<string, any>} [options={}] - Options (temperature, maxTokens, system).
|
|
74
|
+
* @param {Record<string, any>} [options={}] - Options (temperature, maxTokens, system, timeoutMs — a per-call override of the constructor's `timeoutMs`; see BA-18).
|
|
69
75
|
* @returns {Promise<GenerateResult>}
|
|
70
76
|
* @throws {Error} `[AnthropicProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response.
|
|
71
77
|
*/
|
|
@@ -94,8 +100,9 @@ export class AnthropicProvider {
|
|
|
94
100
|
_toAnthropicMessage(msg: Message): any;
|
|
95
101
|
/**
|
|
96
102
|
* @param {Record<string, any>} body
|
|
103
|
+
* @param {number} [timeoutMs=0] - Idle-socket timeout (ms); 0 disables. See BA-18 / provider-http.
|
|
97
104
|
* @returns {Promise<any>}
|
|
98
105
|
*/
|
|
99
|
-
_request(body: Record<string, any
|
|
106
|
+
_request(body: Record<string, any>, timeoutMs?: number): Promise<any>;
|
|
100
107
|
_warnedInsecure: boolean | undefined;
|
|
101
108
|
}
|
|
@@ -5,6 +5,7 @@ const http = require('http');
|
|
|
5
5
|
const { ProviderError } = require('./errors');
|
|
6
6
|
const { requestWithTemperatureFallback } = require('./provider-temperature');
|
|
7
7
|
const { normalizeStopReason } = require('./provider-stop-reason');
|
|
8
|
+
const { resolveTimeoutMs, applyRequestTimeout } = require('./provider-http');
|
|
8
9
|
|
|
9
10
|
/** @param {string} hostname @returns {boolean} */
|
|
10
11
|
function isLoopbackHost(hostname) {
|
|
@@ -27,6 +28,7 @@ function isLoopbackHost(hostname) {
|
|
|
27
28
|
*
|
|
28
29
|
* **MEASURED CAVEAT — this option does not "turn thinking on".** On `claude-sonnet-5` adaptive thinking is ALREADY the default: sending this changed the observed thinking rate not at all (2/10 rounds with it vs 3/10 without — `poc/ba7-adaptive-default.mjs`). Its real use is pinning the mode and reaching `display`/`effort`. The change that mattered is that thinking blocks are now PRESERVED and replayed (see `Message.providerBlocks`), which happens whether or not you ever set this.
|
|
29
30
|
* @property {boolean} [exposeErrorBody=false] - Attach the full upstream response to `err.body` on HTTP errors (off by default to avoid leaking unexpected fields through error logs; `err.message` still carries the API error).
|
|
31
|
+
* @property {number} [timeoutMs=600000] - BA-18: request/idle timeout in ms. A silent or never-answering socket (dropped by the server, or a response that never starts) was otherwise bounded only by the OS TCP timeout (~2h) — a hang, not an error, so every retry policy above it was inert. Bounds on socket INACTIVITY (the timer resets on activity, so a slow-but-streaming response is not killed); on trip, `generate()` rejects with a retryable `TimeoutError` (`code: 'ETIMEDOUT'`) that a wired `Retry` (`Loop({ retry })`) retries. Default 10 min sits above any single non-streaming completion; `0` or `Infinity` disables it (pre-BA-18 behaviour). Overridable per call via `generate(..., { timeoutMs })`.
|
|
30
32
|
*/
|
|
31
33
|
|
|
32
34
|
class AnthropicProvider {
|
|
@@ -52,13 +54,15 @@ class AnthropicProvider {
|
|
|
52
54
|
this.thinking = options.thinking != null ? options.thinking : null;
|
|
53
55
|
// See OpenAIProvider: attach full upstream body to err.body only on opt-in.
|
|
54
56
|
this.exposeErrorBody = options.exposeErrorBody === true;
|
|
57
|
+
// BA-18: request/idle timeout (ms). Resolved at call time (default 600000; 0/Infinity disable).
|
|
58
|
+
this.timeoutMs = options.timeoutMs;
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
/**
|
|
58
62
|
* Generate a response from the Anthropic API.
|
|
59
63
|
* @param {Message[]} messages - Conversation messages (OpenAI format, auto-converted).
|
|
60
64
|
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
61
|
-
* @param {Record<string, any>} [options={}] - Options (temperature, maxTokens, system).
|
|
65
|
+
* @param {Record<string, any>} [options={}] - Options (temperature, maxTokens, system, timeoutMs — a per-call override of the constructor's `timeoutMs`; see BA-18).
|
|
62
66
|
* @returns {Promise<GenerateResult>}
|
|
63
67
|
* @throws {Error} `[AnthropicProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response.
|
|
64
68
|
*/
|
|
@@ -142,8 +146,9 @@ class AnthropicProvider {
|
|
|
142
146
|
// BA-10: some models (e.g. claude-sonnet-5) reject a non-default `temperature` with a 400 — drop it
|
|
143
147
|
// and retry once rather than let the whole call fail. `temperatureDropped` flows back so an upstream
|
|
144
148
|
// receipt (recurse's refineLeaf) can report the effective temperature, not the one the model ignored.
|
|
149
|
+
const timeoutMs = resolveTimeoutMs(this.timeoutMs, options.timeoutMs);
|
|
145
150
|
const { data, temperatureDropped } = await requestWithTemperatureFallback({
|
|
146
|
-
request: () => this._request(body),
|
|
151
|
+
request: () => this._request(body, timeoutMs),
|
|
147
152
|
hadTemperature: () => body.temperature != null,
|
|
148
153
|
stripTemperature: () => { delete body.temperature; },
|
|
149
154
|
warnOnce: () => this._warnTemperatureDropped(),
|
|
@@ -275,9 +280,10 @@ class AnthropicProvider {
|
|
|
275
280
|
|
|
276
281
|
/**
|
|
277
282
|
* @param {Record<string, any>} body
|
|
283
|
+
* @param {number} [timeoutMs=0] - Idle-socket timeout (ms); 0 disables. See BA-18 / provider-http.
|
|
278
284
|
* @returns {Promise<any>}
|
|
279
285
|
*/
|
|
280
|
-
_request(body) {
|
|
286
|
+
_request(body, timeoutMs = 0) {
|
|
281
287
|
return new Promise((resolve, reject) => {
|
|
282
288
|
const payload = JSON.stringify(body);
|
|
283
289
|
const url = new URL(this.baseUrl + '/messages');
|
|
@@ -314,6 +320,7 @@ class AnthropicProvider {
|
|
|
314
320
|
}
|
|
315
321
|
});
|
|
316
322
|
});
|
|
323
|
+
applyRequestTimeout(req, timeoutMs, 'AnthropicProvider');
|
|
317
324
|
req.on('error', reject);
|
|
318
325
|
req.write(payload);
|
|
319
326
|
req.end();
|
package/src/provider-gemini.d.ts
CHANGED
|
@@ -19,6 +19,10 @@ export type GeminiOptions = {
|
|
|
19
19
|
* - Attach the full upstream response to `err.body` on HTTP errors (off by default; `err.message` still carries the API error).
|
|
20
20
|
*/
|
|
21
21
|
exposeErrorBody?: boolean | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* - BA-18: request/idle timeout in ms. Bounds a silent or never-answering socket on inactivity so `generate()` rejects with a retryable `TimeoutError` (`code: 'ETIMEDOUT'`) instead of hanging until the OS TCP timeout (~2h). `0`/`Infinity` disables it (pre-BA-18 behaviour). Overridable per call via `generate(..., { timeoutMs })`.
|
|
24
|
+
*/
|
|
25
|
+
timeoutMs?: number | undefined;
|
|
22
26
|
};
|
|
23
27
|
/**
|
|
24
28
|
* @typedef {object} GeminiOptions
|
|
@@ -26,6 +30,7 @@ export type GeminiOptions = {
|
|
|
26
30
|
* @property {string} [model='gemini-2.5-flash'] - Model ID.
|
|
27
31
|
* @property {string} [baseUrl='https://generativelanguage.googleapis.com/v1beta'] - API base (override for proxies; posts to `${baseUrl}/models/${model}:generateContent`).
|
|
28
32
|
* @property {boolean} [exposeErrorBody=false] - Attach the full upstream response to `err.body` on HTTP errors (off by default; `err.message` still carries the API error).
|
|
33
|
+
* @property {number} [timeoutMs=600000] - BA-18: request/idle timeout in ms. Bounds a silent or never-answering socket on inactivity so `generate()` rejects with a retryable `TimeoutError` (`code: 'ETIMEDOUT'`) instead of hanging until the OS TCP timeout (~2h). `0`/`Infinity` disables it (pre-BA-18 behaviour). Overridable per call via `generate(..., { timeoutMs })`.
|
|
29
34
|
*/
|
|
30
35
|
/**
|
|
31
36
|
* Google Gemini provider (native `generateContent` API, NOT the OpenAI-compat endpoint — that endpoint
|
|
@@ -41,11 +46,12 @@ export class GeminiProvider {
|
|
|
41
46
|
model: string;
|
|
42
47
|
baseUrl: string;
|
|
43
48
|
exposeErrorBody: boolean;
|
|
49
|
+
timeoutMs: number | undefined;
|
|
44
50
|
/**
|
|
45
51
|
* Generate a response from the Gemini API.
|
|
46
52
|
* @param {Message[]} messages - Conversation messages (OpenAI format, auto-converted).
|
|
47
53
|
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
48
|
-
* @param {Record<string, any>} [options={}] - Options (temperature, maxTokens).
|
|
54
|
+
* @param {Record<string, any>} [options={}] - Options (temperature, maxTokens, timeoutMs — a per-call override of the constructor's `timeoutMs`; see BA-18).
|
|
49
55
|
* @returns {Promise<GenerateResult>}
|
|
50
56
|
* @throws {Error} `[GeminiProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response.
|
|
51
57
|
*/
|
|
@@ -66,8 +72,9 @@ export class GeminiProvider {
|
|
|
66
72
|
/**
|
|
67
73
|
* @param {string} path
|
|
68
74
|
* @param {Record<string, any>} body
|
|
75
|
+
* @param {number} [timeoutMs=0] - Idle-socket timeout (ms); 0 disables. See BA-18 / provider-http.
|
|
69
76
|
* @returns {Promise<any>}
|
|
70
77
|
*/
|
|
71
|
-
_request(path: string, body: Record<string, any
|
|
78
|
+
_request(path: string, body: Record<string, any>, timeoutMs?: number): Promise<any>;
|
|
72
79
|
_warnedInsecure: boolean | undefined;
|
|
73
80
|
}
|
package/src/provider-gemini.js
CHANGED
|
@@ -5,6 +5,7 @@ const http = require('http');
|
|
|
5
5
|
const { ProviderError } = require('./errors');
|
|
6
6
|
const { requestWithTemperatureFallback } = require('./provider-temperature');
|
|
7
7
|
const { normalizeStopReason } = require('./provider-stop-reason');
|
|
8
|
+
const { resolveTimeoutMs, applyRequestTimeout } = require('./provider-http');
|
|
8
9
|
|
|
9
10
|
/** @typedef {import('../types').Message} Message */
|
|
10
11
|
/** @typedef {import('../types').ToolDef} ToolDef */
|
|
@@ -23,6 +24,7 @@ function isLoopbackHost(hostname) {
|
|
|
23
24
|
* @property {string} [model='gemini-2.5-flash'] - Model ID.
|
|
24
25
|
* @property {string} [baseUrl='https://generativelanguage.googleapis.com/v1beta'] - API base (override for proxies; posts to `${baseUrl}/models/${model}:generateContent`).
|
|
25
26
|
* @property {boolean} [exposeErrorBody=false] - Attach the full upstream response to `err.body` on HTTP errors (off by default; `err.message` still carries the API error).
|
|
27
|
+
* @property {number} [timeoutMs=600000] - BA-18: request/idle timeout in ms. Bounds a silent or never-answering socket on inactivity so `generate()` rejects with a retryable `TimeoutError` (`code: 'ETIMEDOUT'`) instead of hanging until the OS TCP timeout (~2h). `0`/`Infinity` disables it (pre-BA-18 behaviour). Overridable per call via `generate(..., { timeoutMs })`.
|
|
26
28
|
*/
|
|
27
29
|
|
|
28
30
|
/**
|
|
@@ -39,13 +41,15 @@ class GeminiProvider {
|
|
|
39
41
|
this.model = options.model || 'gemini-2.5-flash';
|
|
40
42
|
this.baseUrl = options.baseUrl || 'https://generativelanguage.googleapis.com/v1beta';
|
|
41
43
|
this.exposeErrorBody = options.exposeErrorBody === true;
|
|
44
|
+
// BA-18: request/idle timeout (ms). Resolved at call time (default 600000; 0/Infinity disable).
|
|
45
|
+
this.timeoutMs = options.timeoutMs;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
/**
|
|
45
49
|
* Generate a response from the Gemini API.
|
|
46
50
|
* @param {Message[]} messages - Conversation messages (OpenAI format, auto-converted).
|
|
47
51
|
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
48
|
-
* @param {Record<string, any>} [options={}] - Options (temperature, maxTokens).
|
|
52
|
+
* @param {Record<string, any>} [options={}] - Options (temperature, maxTokens, timeoutMs — a per-call override of the constructor's `timeoutMs`; see BA-18).
|
|
49
53
|
* @returns {Promise<GenerateResult>}
|
|
50
54
|
* @throws {Error} `[GeminiProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response.
|
|
51
55
|
*/
|
|
@@ -107,8 +111,9 @@ class GeminiProvider {
|
|
|
107
111
|
|
|
108
112
|
// BA-10: graceful degrade if a model rejects a non-default `temperature` (Gemini nests it under
|
|
109
113
|
// generationConfig). Keyed off the API error text, so dormant on models that accept temperature.
|
|
114
|
+
const timeoutMs = resolveTimeoutMs(this.timeoutMs, options.timeoutMs);
|
|
110
115
|
const { data, temperatureDropped } = await requestWithTemperatureFallback({
|
|
111
|
-
request: () => this._request(`/models/${this.model}:generateContent`, body),
|
|
116
|
+
request: () => this._request(`/models/${this.model}:generateContent`, body, timeoutMs),
|
|
112
117
|
hadTemperature: () => body.generationConfig?.temperature != null,
|
|
113
118
|
stripTemperature: () => { if (body.generationConfig) delete body.generationConfig.temperature; },
|
|
114
119
|
warnOnce: () => this._warnTemperatureDropped(),
|
|
@@ -174,9 +179,10 @@ class GeminiProvider {
|
|
|
174
179
|
/**
|
|
175
180
|
* @param {string} path
|
|
176
181
|
* @param {Record<string, any>} body
|
|
182
|
+
* @param {number} [timeoutMs=0] - Idle-socket timeout (ms); 0 disables. See BA-18 / provider-http.
|
|
177
183
|
* @returns {Promise<any>}
|
|
178
184
|
*/
|
|
179
|
-
_request(path, body) {
|
|
185
|
+
_request(path, body, timeoutMs = 0) {
|
|
180
186
|
return new Promise((resolve, reject) => {
|
|
181
187
|
const url = new URL(this.baseUrl + path);
|
|
182
188
|
const transport = url.protocol === 'https:' ? https : http;
|
|
@@ -213,6 +219,7 @@ class GeminiProvider {
|
|
|
213
219
|
}
|
|
214
220
|
});
|
|
215
221
|
});
|
|
222
|
+
applyRequestTimeout(req, timeoutMs, 'GeminiProvider');
|
|
216
223
|
req.on('error', reject);
|
|
217
224
|
req.write(payload);
|
|
218
225
|
req.end();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BA-18 — shared request-timeout helper for the http(s)-based providers (Anthropic, OpenAI,
|
|
3
|
+
* Gemini, Ollama). They all build a `http.ClientRequest` with only `req.on('error')` wired, so a
|
|
4
|
+
* socket the server silently dropped — or a response that never starts — was bounded only by the
|
|
5
|
+
* OS TCP timeout (~2h on Linux). That presents to the caller as a hang, not a failure, so every
|
|
6
|
+
* retry/casualty policy above it is inert. This adds a finite, configurable idle bound in one
|
|
7
|
+
* place so the four providers cannot drift.
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_TIMEOUT_MS: 600000;
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the effective timeout in ms. A per-call value overrides the instance default, but `null`
|
|
12
|
+
* and `undefined` BOTH mean "inherit" — so a per-call `null` never shadows an instance-level
|
|
13
|
+
* disable (finding-2). The explicit opt-out idiom is `0` or `Infinity` → returns 0 (no bound, the
|
|
14
|
+
* pre-BA-18 behaviour). A NaN / negative / otherwise non-finite value is treated as a caller
|
|
15
|
+
* MISTAKE and falls back to {@link DEFAULT_TIMEOUT_MS}: it must never silently disable the safety
|
|
16
|
+
* bound, which would round optimistically back toward the ~2h hang BA-18 exists to prevent
|
|
17
|
+
* (finding-3; the disable-edge bug class).
|
|
18
|
+
* @param {number|undefined|null} instanceTimeout
|
|
19
|
+
* @param {number|undefined|null} [callTimeout]
|
|
20
|
+
* @returns {number} a finite positive ms bound, or 0 to disable
|
|
21
|
+
*/
|
|
22
|
+
export function resolveTimeoutMs(instanceTimeout: number | undefined | null, callTimeout?: number | undefined | null): number;
|
|
23
|
+
/**
|
|
24
|
+
* Bound an in-flight ClientRequest on socket INACTIVITY. On timeout, destroy the request with a
|
|
25
|
+
* retryable {@link TimeoutError} (`code: 'ETIMEDOUT'`) — `DEFAULT_RETRY_ON` classifies that as
|
|
26
|
+
* transient, and the provider's own `req.on('error', reject)` turns it into a rejected promise, so
|
|
27
|
+
* the caller regains control instead of hanging. Idle semantics (via `req.setTimeout`): the timer
|
|
28
|
+
* resets on any socket activity, so a slow-but-streaming response is NOT killed — only a silent or
|
|
29
|
+
* never-answering socket trips it. A `timeoutMs` of 0 is a no-op (bound disabled).
|
|
30
|
+
* @param {import('http').ClientRequest} req
|
|
31
|
+
* @param {number} timeoutMs - resolved bound; 0 disables
|
|
32
|
+
* @param {string} providerName - for the error message (e.g. 'AnthropicProvider')
|
|
33
|
+
*/
|
|
34
|
+
export function applyRequestTimeout(req: import("http").ClientRequest, timeoutMs: number, providerName: string): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { TimeoutError } = require('./errors');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* BA-18 — shared request-timeout helper for the http(s)-based providers (Anthropic, OpenAI,
|
|
7
|
+
* Gemini, Ollama). They all build a `http.ClientRequest` with only `req.on('error')` wired, so a
|
|
8
|
+
* socket the server silently dropped — or a response that never starts — was bounded only by the
|
|
9
|
+
* OS TCP timeout (~2h on Linux). That presents to the caller as a hang, not a failure, so every
|
|
10
|
+
* retry/casualty policy above it is inert. This adds a finite, configurable idle bound in one
|
|
11
|
+
* place so the four providers cannot drift.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// 10 minutes: safely above any single non-streaming completion (a big reasoning response is a few
|
|
15
|
+
// minutes at most), well below the ~2h OS TCP default. These requests are non-streaming, so a legit
|
|
16
|
+
// slow completion can have no socket activity until the whole body arrives (TTFB ≈ generation time)
|
|
17
|
+
// — the default must clear that, not a typical round-trip.
|
|
18
|
+
const DEFAULT_TIMEOUT_MS = 600000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the effective timeout in ms. A per-call value overrides the instance default, but `null`
|
|
22
|
+
* and `undefined` BOTH mean "inherit" — so a per-call `null` never shadows an instance-level
|
|
23
|
+
* disable (finding-2). The explicit opt-out idiom is `0` or `Infinity` → returns 0 (no bound, the
|
|
24
|
+
* pre-BA-18 behaviour). A NaN / negative / otherwise non-finite value is treated as a caller
|
|
25
|
+
* MISTAKE and falls back to {@link DEFAULT_TIMEOUT_MS}: it must never silently disable the safety
|
|
26
|
+
* bound, which would round optimistically back toward the ~2h hang BA-18 exists to prevent
|
|
27
|
+
* (finding-3; the disable-edge bug class).
|
|
28
|
+
* @param {number|undefined|null} instanceTimeout
|
|
29
|
+
* @param {number|undefined|null} [callTimeout]
|
|
30
|
+
* @returns {number} a finite positive ms bound, or 0 to disable
|
|
31
|
+
*/
|
|
32
|
+
function resolveTimeoutMs(instanceTimeout, callTimeout) {
|
|
33
|
+
const raw = callTimeout != null ? callTimeout : instanceTimeout; // null/undefined per-call → inherit
|
|
34
|
+
if (raw == null) return DEFAULT_TIMEOUT_MS; // absent on both → finite default
|
|
35
|
+
const n = Number(raw);
|
|
36
|
+
if (n === 0 || n === Infinity) return 0; // the explicit opt-out idiom → no bound
|
|
37
|
+
if (!Number.isFinite(n) || n < 0) return DEFAULT_TIMEOUT_MS; // NaN / negative / garbage → SAFE default, never a silent disable
|
|
38
|
+
return n; // finite positive bound
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Bound an in-flight ClientRequest on socket INACTIVITY. On timeout, destroy the request with a
|
|
43
|
+
* retryable {@link TimeoutError} (`code: 'ETIMEDOUT'`) — `DEFAULT_RETRY_ON` classifies that as
|
|
44
|
+
* transient, and the provider's own `req.on('error', reject)` turns it into a rejected promise, so
|
|
45
|
+
* the caller regains control instead of hanging. Idle semantics (via `req.setTimeout`): the timer
|
|
46
|
+
* resets on any socket activity, so a slow-but-streaming response is NOT killed — only a silent or
|
|
47
|
+
* never-answering socket trips it. A `timeoutMs` of 0 is a no-op (bound disabled).
|
|
48
|
+
* @param {import('http').ClientRequest} req
|
|
49
|
+
* @param {number} timeoutMs - resolved bound; 0 disables
|
|
50
|
+
* @param {string} providerName - for the error message (e.g. 'AnthropicProvider')
|
|
51
|
+
*/
|
|
52
|
+
function applyRequestTimeout(req, timeoutMs, providerName) {
|
|
53
|
+
if (!(timeoutMs > 0)) return;
|
|
54
|
+
req.setTimeout(timeoutMs, () => {
|
|
55
|
+
req.destroy(new TimeoutError(`[${providerName}] request timed out after ${timeoutMs}ms of socket inactivity`));
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { DEFAULT_TIMEOUT_MS, resolveTimeoutMs, applyRequestTimeout };
|
package/src/provider-ollama.d.ts
CHANGED
|
@@ -5,6 +5,10 @@ export type OllamaOptions = {
|
|
|
5
5
|
model?: string | undefined;
|
|
6
6
|
url?: string | undefined;
|
|
7
7
|
exposeErrorBody?: boolean | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* - BA-18: request/idle timeout in ms. Bounds a silent or never-answering socket on inactivity so `generate()` rejects with a retryable `TimeoutError` (`code: 'ETIMEDOUT'`) instead of hanging until the OS TCP timeout. `0`/`Infinity` disables it. Overridable per call via `generate(..., { timeoutMs })`. (A local Ollama that is loading a large model cold can be slow to first byte — raise this or disable it for very large local models.)
|
|
10
|
+
*/
|
|
11
|
+
timeoutMs?: number | undefined;
|
|
8
12
|
};
|
|
9
13
|
/** @typedef {import('../types').Message} Message */
|
|
10
14
|
/** @typedef {import('../types').ToolDef} ToolDef */
|
|
@@ -14,6 +18,7 @@ export type OllamaOptions = {
|
|
|
14
18
|
* @property {string} [model='llama3.2']
|
|
15
19
|
* @property {string} [url='http://localhost:11434']
|
|
16
20
|
* @property {boolean} [exposeErrorBody=false]
|
|
21
|
+
* @property {number} [timeoutMs=600000] - BA-18: request/idle timeout in ms. Bounds a silent or never-answering socket on inactivity so `generate()` rejects with a retryable `TimeoutError` (`code: 'ETIMEDOUT'`) instead of hanging until the OS TCP timeout. `0`/`Infinity` disables it. Overridable per call via `generate(..., { timeoutMs })`. (A local Ollama that is loading a large model cold can be slow to first byte — raise this or disable it for very large local models.)
|
|
17
22
|
*/
|
|
18
23
|
export class OllamaProvider {
|
|
19
24
|
/**
|
|
@@ -23,11 +28,12 @@ export class OllamaProvider {
|
|
|
23
28
|
model: string;
|
|
24
29
|
url: string;
|
|
25
30
|
exposeErrorBody: boolean;
|
|
31
|
+
timeoutMs: number | undefined;
|
|
26
32
|
/**
|
|
27
33
|
* Generate a response from a local Ollama instance.
|
|
28
34
|
* @param {Message[]} messages - Conversation messages.
|
|
29
35
|
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
30
|
-
* @param {Record<string, any>} [options={}] - Options (`temperature`, `maxTokens`).
|
|
36
|
+
* @param {Record<string, any>} [options={}] - Options (`temperature`, `maxTokens`, `timeoutMs` — a per-call override of the constructor's `timeoutMs`; see BA-18).
|
|
31
37
|
* @returns {Promise<GenerateResult>}
|
|
32
38
|
* @throws {Error} `[OllamaProvider] ...` — on HTTP errors or invalid JSON response.
|
|
33
39
|
*/
|
|
@@ -38,7 +44,8 @@ export class OllamaProvider {
|
|
|
38
44
|
/**
|
|
39
45
|
* @param {string} path
|
|
40
46
|
* @param {Record<string, any>} body
|
|
47
|
+
* @param {number} [timeoutMs=0] - Idle-socket timeout (ms); 0 disables. See BA-18 / provider-http.
|
|
41
48
|
* @returns {Promise<any>}
|
|
42
49
|
*/
|
|
43
|
-
_request(path: string, body: Record<string, any
|
|
50
|
+
_request(path: string, body: Record<string, any>, timeoutMs?: number): Promise<any>;
|
|
44
51
|
}
|
package/src/provider-ollama.js
CHANGED
|
@@ -4,6 +4,7 @@ const http = require('http');
|
|
|
4
4
|
const { ProviderError } = require('./errors');
|
|
5
5
|
const { requestWithTemperatureFallback } = require('./provider-temperature');
|
|
6
6
|
const { normalizeStopReason } = require('./provider-stop-reason');
|
|
7
|
+
const { resolveTimeoutMs, applyRequestTimeout } = require('./provider-http');
|
|
7
8
|
|
|
8
9
|
/** @typedef {import('../types').Message} Message */
|
|
9
10
|
/** @typedef {import('../types').ToolDef} ToolDef */
|
|
@@ -14,6 +15,7 @@ const { normalizeStopReason } = require('./provider-stop-reason');
|
|
|
14
15
|
* @property {string} [model='llama3.2']
|
|
15
16
|
* @property {string} [url='http://localhost:11434']
|
|
16
17
|
* @property {boolean} [exposeErrorBody=false]
|
|
18
|
+
* @property {number} [timeoutMs=600000] - BA-18: request/idle timeout in ms. Bounds a silent or never-answering socket on inactivity so `generate()` rejects with a retryable `TimeoutError` (`code: 'ETIMEDOUT'`) instead of hanging until the OS TCP timeout. `0`/`Infinity` disables it. Overridable per call via `generate(..., { timeoutMs })`. (A local Ollama that is loading a large model cold can be slow to first byte — raise this or disable it for very large local models.)
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
21
|
class OllamaProvider {
|
|
@@ -25,13 +27,15 @@ class OllamaProvider {
|
|
|
25
27
|
this.url = options.url || 'http://localhost:11434';
|
|
26
28
|
// See OpenAIProvider: attach full upstream body to err.body only on opt-in.
|
|
27
29
|
this.exposeErrorBody = options.exposeErrorBody === true;
|
|
30
|
+
// BA-18: request/idle timeout (ms). Resolved at call time (default 600000; 0/Infinity disable).
|
|
31
|
+
this.timeoutMs = options.timeoutMs;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
/**
|
|
31
35
|
* Generate a response from a local Ollama instance.
|
|
32
36
|
* @param {Message[]} messages - Conversation messages.
|
|
33
37
|
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
34
|
-
* @param {Record<string, any>} [options={}] - Options (`temperature`, `maxTokens`).
|
|
38
|
+
* @param {Record<string, any>} [options={}] - Options (`temperature`, `maxTokens`, `timeoutMs` — a per-call override of the constructor's `timeoutMs`; see BA-18).
|
|
35
39
|
* @returns {Promise<GenerateResult>}
|
|
36
40
|
* @throws {Error} `[OllamaProvider] ...` — on HTTP errors or invalid JSON response.
|
|
37
41
|
*/
|
|
@@ -64,8 +68,9 @@ class OllamaProvider {
|
|
|
64
68
|
|
|
65
69
|
// BA-10: graceful degrade if a model rejects a non-default `temperature` (Ollama nests it under
|
|
66
70
|
// `options`). Keyed off the API error text, so dormant on models that accept temperature.
|
|
71
|
+
const timeoutMs = resolveTimeoutMs(this.timeoutMs, options.timeoutMs);
|
|
67
72
|
const { data, temperatureDropped } = await requestWithTemperatureFallback({
|
|
68
|
-
request: () => this._request('/api/chat', body),
|
|
73
|
+
request: () => this._request('/api/chat', body, timeoutMs),
|
|
69
74
|
hadTemperature: () => body.options?.temperature != null,
|
|
70
75
|
stripTemperature: () => { if (body.options) delete body.options.temperature; },
|
|
71
76
|
warnOnce: () => this._warnTemperatureDropped(),
|
|
@@ -110,9 +115,10 @@ class OllamaProvider {
|
|
|
110
115
|
/**
|
|
111
116
|
* @param {string} path
|
|
112
117
|
* @param {Record<string, any>} body
|
|
118
|
+
* @param {number} [timeoutMs=0] - Idle-socket timeout (ms); 0 disables. See BA-18 / provider-http.
|
|
113
119
|
* @returns {Promise<any>}
|
|
114
120
|
*/
|
|
115
|
-
_request(path, body) {
|
|
121
|
+
_request(path, body, timeoutMs = 0) {
|
|
116
122
|
return new Promise((resolve, reject) => {
|
|
117
123
|
const url = new URL(this.url + path);
|
|
118
124
|
const payload = JSON.stringify(body);
|
|
@@ -141,6 +147,7 @@ class OllamaProvider {
|
|
|
141
147
|
}
|
|
142
148
|
});
|
|
143
149
|
});
|
|
150
|
+
applyRequestTimeout(req, timeoutMs, 'OllamaProvider');
|
|
144
151
|
req.on('error', reject);
|
|
145
152
|
req.write(payload);
|
|
146
153
|
req.end();
|
package/src/provider-openai.d.ts
CHANGED
|
@@ -14,6 +14,13 @@ export type OpenAIOptions = {
|
|
|
14
14
|
* debugging only.
|
|
15
15
|
*/
|
|
16
16
|
exposeErrorBody?: boolean | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* - BA-18: request/idle timeout in ms. Bounds a silent or
|
|
19
|
+
* never-answering socket on inactivity so `generate()` rejects with a retryable `TimeoutError`
|
|
20
|
+
* (`code: 'ETIMEDOUT'`) instead of hanging until the OS TCP timeout (~2h). `0`/`Infinity`
|
|
21
|
+
* disables it (pre-BA-18 behaviour). Overridable per call via `generate(..., { timeoutMs })`.
|
|
22
|
+
*/
|
|
23
|
+
timeoutMs?: number | undefined;
|
|
17
24
|
};
|
|
18
25
|
/**
|
|
19
26
|
* @typedef {object} OpenAIOptions
|
|
@@ -25,6 +32,10 @@ export type OpenAIOptions = {
|
|
|
25
32
|
* field in an error payload can't leak through logs that dump the error
|
|
26
33
|
* object; `err.message` still carries the API's error message. Turn on for
|
|
27
34
|
* debugging only.
|
|
35
|
+
* @property {number} [timeoutMs=600000] - BA-18: request/idle timeout in ms. Bounds a silent or
|
|
36
|
+
* never-answering socket on inactivity so `generate()` rejects with a retryable `TimeoutError`
|
|
37
|
+
* (`code: 'ETIMEDOUT'`) instead of hanging until the OS TCP timeout (~2h). `0`/`Infinity`
|
|
38
|
+
* disables it (pre-BA-18 behaviour). Overridable per call via `generate(..., { timeoutMs })`.
|
|
28
39
|
*/
|
|
29
40
|
export class OpenAIProvider {
|
|
30
41
|
/**
|
|
@@ -35,11 +46,12 @@ export class OpenAIProvider {
|
|
|
35
46
|
model: string;
|
|
36
47
|
baseUrl: string;
|
|
37
48
|
exposeErrorBody: boolean;
|
|
49
|
+
timeoutMs: number | undefined;
|
|
38
50
|
/**
|
|
39
51
|
* Generate a response from the OpenAI API.
|
|
40
52
|
* @param {Message[]} messages - Conversation messages.
|
|
41
53
|
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
42
|
-
* @param {Record<string, any>} [options={}] - Options (temperature, maxTokens).
|
|
54
|
+
* @param {Record<string, any>} [options={}] - Options (temperature, maxTokens, timeoutMs — a per-call override of the constructor's `timeoutMs`; see BA-18).
|
|
43
55
|
* @returns {Promise<GenerateResult>}
|
|
44
56
|
* @throws {Error} `[OpenAIProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response.
|
|
45
57
|
*/
|
|
@@ -60,8 +72,9 @@ export class OpenAIProvider {
|
|
|
60
72
|
/**
|
|
61
73
|
* @param {string} path
|
|
62
74
|
* @param {Record<string, any>} body
|
|
75
|
+
* @param {number} [timeoutMs=0] - Idle-socket timeout (ms); 0 disables. See BA-18 / provider-http.
|
|
63
76
|
* @returns {Promise<any>}
|
|
64
77
|
*/
|
|
65
|
-
_request(path: string, body: Record<string, any
|
|
78
|
+
_request(path: string, body: Record<string, any>, timeoutMs?: number): Promise<any>;
|
|
66
79
|
_warnedInsecure: boolean | undefined;
|
|
67
80
|
}
|
package/src/provider-openai.js
CHANGED
|
@@ -5,6 +5,7 @@ const http = require('http');
|
|
|
5
5
|
const { ProviderError } = require('./errors');
|
|
6
6
|
const { requestWithTemperatureFallback } = require('./provider-temperature');
|
|
7
7
|
const { normalizeStopReason } = require('./provider-stop-reason');
|
|
8
|
+
const { resolveTimeoutMs, applyRequestTimeout } = require('./provider-http');
|
|
8
9
|
|
|
9
10
|
/** @typedef {import('../types').Message} Message */
|
|
10
11
|
/** @typedef {import('../types').ToolDef} ToolDef */
|
|
@@ -27,6 +28,10 @@ function isLoopbackHost(hostname) {
|
|
|
27
28
|
* field in an error payload can't leak through logs that dump the error
|
|
28
29
|
* object; `err.message` still carries the API's error message. Turn on for
|
|
29
30
|
* debugging only.
|
|
31
|
+
* @property {number} [timeoutMs=600000] - BA-18: request/idle timeout in ms. Bounds a silent or
|
|
32
|
+
* never-answering socket on inactivity so `generate()` rejects with a retryable `TimeoutError`
|
|
33
|
+
* (`code: 'ETIMEDOUT'`) instead of hanging until the OS TCP timeout (~2h). `0`/`Infinity`
|
|
34
|
+
* disables it (pre-BA-18 behaviour). Overridable per call via `generate(..., { timeoutMs })`.
|
|
30
35
|
*/
|
|
31
36
|
|
|
32
37
|
class OpenAIProvider {
|
|
@@ -38,13 +43,15 @@ class OpenAIProvider {
|
|
|
38
43
|
this.model = options.model || 'gpt-4o-mini';
|
|
39
44
|
this.baseUrl = options.baseUrl || 'https://api.openai.com/v1';
|
|
40
45
|
this.exposeErrorBody = options.exposeErrorBody === true;
|
|
46
|
+
// BA-18: request/idle timeout (ms). Resolved at call time (default 600000; 0/Infinity disable).
|
|
47
|
+
this.timeoutMs = options.timeoutMs;
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
/**
|
|
44
51
|
* Generate a response from the OpenAI API.
|
|
45
52
|
* @param {Message[]} messages - Conversation messages.
|
|
46
53
|
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
47
|
-
* @param {Record<string, any>} [options={}] - Options (temperature, maxTokens).
|
|
54
|
+
* @param {Record<string, any>} [options={}] - Options (temperature, maxTokens, timeoutMs — a per-call override of the constructor's `timeoutMs`; see BA-18).
|
|
48
55
|
* @returns {Promise<GenerateResult>}
|
|
49
56
|
* @throws {Error} `[OpenAIProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response.
|
|
50
57
|
*/
|
|
@@ -65,8 +72,9 @@ class OpenAIProvider {
|
|
|
65
72
|
|
|
66
73
|
// BA-10: newer models (o1/gpt-5-class) reject a non-default `temperature` with a 400 — drop it and
|
|
67
74
|
// retry once. `temperatureDropped` flows back so an upstream receipt can report the effective value.
|
|
75
|
+
const timeoutMs = resolveTimeoutMs(this.timeoutMs, options.timeoutMs);
|
|
68
76
|
const { data, temperatureDropped } = await requestWithTemperatureFallback({
|
|
69
|
-
request: () => this._request('/chat/completions', body),
|
|
77
|
+
request: () => this._request('/chat/completions', body, timeoutMs),
|
|
70
78
|
hadTemperature: () => body.temperature != null,
|
|
71
79
|
stripTemperature: () => { delete body.temperature; },
|
|
72
80
|
warnOnce: () => this._warnTemperatureDropped(),
|
|
@@ -124,9 +132,10 @@ class OpenAIProvider {
|
|
|
124
132
|
/**
|
|
125
133
|
* @param {string} path
|
|
126
134
|
* @param {Record<string, any>} body
|
|
135
|
+
* @param {number} [timeoutMs=0] - Idle-socket timeout (ms); 0 disables. See BA-18 / provider-http.
|
|
127
136
|
* @returns {Promise<any>}
|
|
128
137
|
*/
|
|
129
|
-
_request(path, body) {
|
|
138
|
+
_request(path, body, timeoutMs = 0) {
|
|
130
139
|
return new Promise((resolve, reject) => {
|
|
131
140
|
const url = new URL(this.baseUrl + path);
|
|
132
141
|
const transport = url.protocol === 'https:' ? https : http;
|
|
@@ -168,6 +177,7 @@ class OpenAIProvider {
|
|
|
168
177
|
}
|
|
169
178
|
});
|
|
170
179
|
});
|
|
180
|
+
applyRequestTimeout(req, timeoutMs, 'OpenAIProvider');
|
|
171
181
|
req.on('error', reject);
|
|
172
182
|
req.write(payload);
|
|
173
183
|
req.end();
|