bare-agent 0.51.0 → 0.53.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 +1 -1
- package/bareagent.context.md +14 -5
- package/package.json +1 -1
- package/primitives.json +5 -5
- package/src/bareguard-adapter.d.ts +4 -0
- package/src/bareguard-adapter.js +38 -0
- package/src/loop.d.ts +4 -2
- package/src/loop.js +30 -20
- package/src/mcp-bridge.d.ts +29 -6
- package/src/mcp-bridge.js +192 -28
- package/src/provider-anthropic.d.ts +6 -7
- package/src/provider-anthropic.js +16 -11
- package/src/provider-clipipe.d.ts +11 -2
- package/src/provider-clipipe.js +22 -10
- package/src/provider-gemini.d.ts +6 -7
- package/src/provider-gemini.js +16 -11
- package/src/provider-ollama.d.ts +6 -7
- package/src/provider-ollama.js +16 -11
- package/src/provider-openai.d.ts +8 -9
- package/src/provider-openai.js +24 -17
- package/src/provider-temperature.d.ts +2 -2
- package/src/provider-temperature.js +5 -3
- package/src/thinking-mode.d.ts +75 -26
- package/src/thinking-mode.js +130 -71
- package/types/index.d.ts +16 -6
package/src/provider-gemini.d.ts
CHANGED
|
@@ -16,8 +16,8 @@ export type GeminiOptions = {
|
|
|
16
16
|
*/
|
|
17
17
|
baseUrl?: string | undefined;
|
|
18
18
|
/**
|
|
19
|
-
* - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right `generationConfig.thinkingConfig.thinkingBudget` request field for the provider/model,
|
|
20
|
-
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model —
|
|
19
|
+
* - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right `generationConfig.thinkingConfig.thinkingBudget` request field for the provider/model, tries the next known shape when a shape request fails with a 400/422 (not the temperature path), and the call ALWAYS RUNS: if every shape fails that way, ONE plain request without a thinking field decides — plain OK ⇒ `thinkingStatus:'unsupported'`, plain fails ⇒ that plain request's error is thrown as-is; other failures propagate after one request; a model measured as not switchable is sent plain directly. Only the winning shape is cached per instance+model ('unsupported' is never cached); warns once per status. Every reply is checked: `GenerateResult.thinkingStatus` is `'applied'`/`'unsupported'`/`'ignored'`/`'unknown'` (`'ignored'` = 'off' was accepted but the reply shows thinking anyway — warns once, result kept) and the legacy `thinkingApplied` is derived (`true`/`false`/`null`). Unset ⇒ request body byte-identical to before. Overridable per call via `generate(..., { thinkingMode })`; `null` suppresses an instance default. Any value but 'on'/'off' ⇒ `ValidationError`. A NEW field: it does not change the raw `thinking` option; setting both (after per-call override) ⇒ `ValidationError`. See `src/thinking-mode.js` / `thinkingSupport()`.
|
|
20
|
+
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model — just runs without it and reports `thinkingStatus:'unsupported'`, so the first member answers and the chain does not fall through).
|
|
21
21
|
*/
|
|
22
22
|
thinkingMode?: "on" | "off" | null | undefined;
|
|
23
23
|
/**
|
|
@@ -38,8 +38,8 @@ export type GeminiOptions = {
|
|
|
38
38
|
* @property {string} [apiKey] - Google AI Studio (Gemini) API key.
|
|
39
39
|
* @property {string} [model='gemini-2.5-flash'] - Model ID.
|
|
40
40
|
* @property {string} [baseUrl='https://generativelanguage.googleapis.com/v1beta'] - API base (override for proxies; posts to `${baseUrl}/models/${model}:generateContent`).
|
|
41
|
-
* @property {'on'|'off'|null} [thinkingMode] - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right `generationConfig.thinkingConfig.thinkingBudget` request field for the provider/model,
|
|
42
|
-
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model —
|
|
41
|
+
* @property {'on'|'off'|null} [thinkingMode] - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right `generationConfig.thinkingConfig.thinkingBudget` request field for the provider/model, tries the next known shape when a shape request fails with a 400/422 (not the temperature path), and the call ALWAYS RUNS: if every shape fails that way, ONE plain request without a thinking field decides — plain OK ⇒ `thinkingStatus:'unsupported'`, plain fails ⇒ that plain request's error is thrown as-is; other failures propagate after one request; a model measured as not switchable is sent plain directly. Only the winning shape is cached per instance+model ('unsupported' is never cached); warns once per status. Every reply is checked: `GenerateResult.thinkingStatus` is `'applied'`/`'unsupported'`/`'ignored'`/`'unknown'` (`'ignored'` = 'off' was accepted but the reply shows thinking anyway — warns once, result kept) and the legacy `thinkingApplied` is derived (`true`/`false`/`null`). Unset ⇒ request body byte-identical to before. Overridable per call via `generate(..., { thinkingMode })`; `null` suppresses an instance default. Any value but 'on'/'off' ⇒ `ValidationError`. A NEW field: it does not change the raw `thinking` option; setting both (after per-call override) ⇒ `ValidationError`. See `src/thinking-mode.js` / `thinkingSupport()`.
|
|
42
|
+
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model — just runs without it and reports `thinkingStatus:'unsupported'`, so the first member answers and the chain does not fall through).
|
|
43
43
|
* @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).
|
|
44
44
|
* @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'`, `context.bound: 'idle'`) instead of hanging until the OS TCP timeout (~2h). `0`/`Infinity` disables it (pre-BA-18 behaviour). Overridable per call via `generate(..., { timeoutMs })`.
|
|
45
45
|
* @property {number} [deadlineMs=0] - BA-19: TOTAL call-duration deadline in ms, beside `timeoutMs`. The idle bound resets on any socket activity, so a response that trickles a byte forever never trips it and hangs for hours. This is an absolute, non-resetting ceiling; on trip, `generate()` rejects with a TERMINAL `TimeoutError` (`code: 'EDEADLINE'`, `context.bound: 'deadline'`, `retryable: false`). DISABLED by default; `0`/`Infinity` disable. Overridable per call via `generate(..., { deadlineMs })`.
|
|
@@ -78,9 +78,8 @@ export class GeminiProvider {
|
|
|
78
78
|
* @throws {Error} `[GeminiProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response.
|
|
79
79
|
*/
|
|
80
80
|
generate(messages: Message[], tools?: ToolDef[], options?: Record<string, any>): Promise<GenerateResult>;
|
|
81
|
-
/** One-time warning that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
82
|
-
_warnTemperatureDropped(): void;
|
|
83
|
-
_warnedTempDropped: boolean | undefined;
|
|
81
|
+
/** One-time warning (per reason) that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
82
|
+
_warnTemperatureDropped(reason?: string): void;
|
|
84
83
|
/**
|
|
85
84
|
* Normalize Gemini `usageMetadata` to the neutral {@link Usage} shape. Like OpenAI, `promptTokenCount`
|
|
86
85
|
* INCLUDES the cached tokens (`cachedContentTokenCount`), so subtract for the uncached remainder
|
package/src/provider-gemini.js
CHANGED
|
@@ -7,7 +7,7 @@ const { requestWithTemperatureFallback } = require('./provider-temperature');
|
|
|
7
7
|
const { normalizeStopReason } = require('./provider-stop-reason');
|
|
8
8
|
const { resolveTimeoutMs, applyRequestBounds, guardResponseSettles } = require('./provider-http');
|
|
9
9
|
const { hasUsageSignal } = require('./provider-usage');
|
|
10
|
-
const { normalizeThinkingMode, resolveThinkingMode, requestWithThinkingMode,
|
|
10
|
+
const { normalizeThinkingMode, resolveThinkingMode, requestWithThinkingMode, assessThinking } = require('./thinking-mode');
|
|
11
11
|
|
|
12
12
|
// BA-24: raw Gemini usageMetadata fields. Any present (even 0) ⇒ a usage signal; none ⇒ null.
|
|
13
13
|
const GEMINI_USAGE_KEYS = ['promptTokenCount', 'candidatesTokenCount', 'thoughtsTokenCount', 'cachedContentTokenCount'];
|
|
@@ -28,8 +28,8 @@ function isLoopbackHost(hostname) {
|
|
|
28
28
|
* @property {string} [apiKey] - Google AI Studio (Gemini) API key.
|
|
29
29
|
* @property {string} [model='gemini-2.5-flash'] - Model ID.
|
|
30
30
|
* @property {string} [baseUrl='https://generativelanguage.googleapis.com/v1beta'] - API base (override for proxies; posts to `${baseUrl}/models/${model}:generateContent`).
|
|
31
|
-
* @property {'on'|'off'|null} [thinkingMode] - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right `generationConfig.thinkingConfig.thinkingBudget` request field for the provider/model,
|
|
32
|
-
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model —
|
|
31
|
+
* @property {'on'|'off'|null} [thinkingMode] - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right `generationConfig.thinkingConfig.thinkingBudget` request field for the provider/model, tries the next known shape when a shape request fails with a 400/422 (not the temperature path), and the call ALWAYS RUNS: if every shape fails that way, ONE plain request without a thinking field decides — plain OK ⇒ `thinkingStatus:'unsupported'`, plain fails ⇒ that plain request's error is thrown as-is; other failures propagate after one request; a model measured as not switchable is sent plain directly. Only the winning shape is cached per instance+model ('unsupported' is never cached); warns once per status. Every reply is checked: `GenerateResult.thinkingStatus` is `'applied'`/`'unsupported'`/`'ignored'`/`'unknown'` (`'ignored'` = 'off' was accepted but the reply shows thinking anyway — warns once, result kept) and the legacy `thinkingApplied` is derived (`true`/`false`/`null`). Unset ⇒ request body byte-identical to before. Overridable per call via `generate(..., { thinkingMode })`; `null` suppresses an instance default. Any value but 'on'/'off' ⇒ `ValidationError`. A NEW field: it does not change the raw `thinking` option; setting both (after per-call override) ⇒ `ValidationError`. See `src/thinking-mode.js` / `thinkingSupport()`.
|
|
32
|
+
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model — just runs without it and reports `thinkingStatus:'unsupported'`, so the first member answers and the chain does not fall through).
|
|
33
33
|
* @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).
|
|
34
34
|
* @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'`, `context.bound: 'idle'`) instead of hanging until the OS TCP timeout (~2h). `0`/`Infinity` disables it (pre-BA-18 behaviour). Overridable per call via `generate(..., { timeoutMs })`.
|
|
35
35
|
* @property {number} [deadlineMs=0] - BA-19: TOTAL call-duration deadline in ms, beside `timeoutMs`. The idle bound resets on any socket activity, so a response that trickles a byte forever never trips it and hangs for hours. This is an absolute, non-resetting ceiling; on trip, `generate()` rejects with a TERMINAL `TimeoutError` (`code: 'EDEADLINE'`, `context.bound: 'deadline'`, `retryable: false`). DISABLED by default; `0`/`Infinity` disable. Overridable per call via `generate(..., { deadlineMs })`.
|
|
@@ -132,15 +132,17 @@ class GeminiProvider {
|
|
|
132
132
|
// BA-10: graceful degrade if a model rejects a non-default `temperature` (Gemini nests it under
|
|
133
133
|
// generationConfig). Keyed off the API error text, so dormant on models that accept temperature.
|
|
134
134
|
const thinkingMode = resolveThinkingMode({ label: 'GeminiProvider', instanceMode: this.thinkingMode, options });
|
|
135
|
+
/** @type {{ unsupported: boolean }} */
|
|
136
|
+
const thinkingTrack = { unsupported: false };
|
|
135
137
|
const timeoutMs = resolveTimeoutMs(this.timeoutMs, options.timeoutMs);
|
|
136
138
|
const deadlineMs = resolveTimeoutMs(this.deadlineMs, options.deadlineMs, 0, 'deadlineMs');
|
|
137
139
|
const { data, temperatureDropped } = await requestWithTemperatureFallback({
|
|
138
140
|
request: () => thinkingMode
|
|
139
|
-
? requestWithThinkingMode({ kind: 'gemini', mode: thinkingMode, model: this.model,
|
|
141
|
+
? requestWithThinkingMode({ kind: 'gemini', mode: thinkingMode, model: this.model, body, cache: this._thinkingShapes, track: thinkingTrack, request: (b) => this._request(`/models/${this.model}:generateContent`, b, timeoutMs, deadlineMs) })
|
|
140
142
|
: this._request(`/models/${this.model}:generateContent`, body, timeoutMs, deadlineMs),
|
|
141
143
|
hadTemperature: () => body.generationConfig?.temperature != null,
|
|
142
144
|
stripTemperature: () => { if (body.generationConfig) delete body.generationConfig.temperature; },
|
|
143
|
-
warnOnce: () => this._warnTemperatureDropped(),
|
|
145
|
+
warnOnce: (reason) => this._warnTemperatureDropped(reason),
|
|
144
146
|
});
|
|
145
147
|
|
|
146
148
|
let text = '';
|
|
@@ -171,15 +173,18 @@ class GeminiProvider {
|
|
|
171
173
|
stopReason: normalizeStopReason(data.candidates?.[0]?.finishReason, 'gemini', { hasToolCalls: toolCalls.length > 0 }),
|
|
172
174
|
usage: this._normalizeUsage(data.usageMetadata),
|
|
173
175
|
...(temperatureDropped && { temperatureDropped: true }),
|
|
174
|
-
...(thinkingMode &&
|
|
176
|
+
...(thinkingMode && assessThinking({ kind: 'gemini', mode: thinkingMode, data, unsupported: thinkingTrack.unsupported, owner: this, label: 'GeminiProvider', model: this.model })),
|
|
175
177
|
};
|
|
176
178
|
}
|
|
177
179
|
|
|
178
|
-
/** One-time warning that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
179
|
-
_warnTemperatureDropped() {
|
|
180
|
-
|
|
181
|
-
this
|
|
182
|
-
|
|
180
|
+
/** One-time warning (per reason) that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
181
|
+
_warnTemperatureDropped(reason = 'unsupported') {
|
|
182
|
+
const flag = reason === 'thinking' ? '_warnedTempThinking' : '_warnedTempDropped';
|
|
183
|
+
if (this[flag]) return;
|
|
184
|
+
this[flag] = true;
|
|
185
|
+
console.warn(reason === 'thinking'
|
|
186
|
+
? `[GeminiProvider] '${this.model}': 'temperature' must be 1 while thinking is on — retrying without it. Further drops from this provider instance are silent.`
|
|
187
|
+
: `[GeminiProvider] '${this.model}' rejected a non-default 'temperature' (unsupported/deprecated) — retrying without it. Further drops from this provider instance are silent.`);
|
|
183
188
|
}
|
|
184
189
|
|
|
185
190
|
/**
|
package/src/provider-ollama.d.ts
CHANGED
|
@@ -5,8 +5,8 @@ export type OllamaOptions = {
|
|
|
5
5
|
model?: string | undefined;
|
|
6
6
|
url?: string | undefined;
|
|
7
7
|
/**
|
|
8
|
-
* - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right `think` (`/api/chat`; NOT live-verified against a real Ollama thinking model — reply signal `message.thinking`) request field for the provider/model,
|
|
9
|
-
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model —
|
|
8
|
+
* - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right `think` (`/api/chat`; NOT live-verified against a real Ollama thinking model — reply signal `message.thinking`) request field for the provider/model, tries the next known shape when a shape request fails with a 400/422 (not the temperature path), and the call ALWAYS RUNS: if every shape fails that way, ONE plain request without a thinking field decides — plain OK ⇒ `thinkingStatus:'unsupported'`, plain fails ⇒ that plain request's error is thrown as-is; other failures propagate after one request; a model measured as not switchable is sent plain directly. Only the winning shape is cached per instance+model ('unsupported' is never cached); warns once per status. Every reply is checked: `GenerateResult.thinkingStatus` is `'applied'`/`'unsupported'`/`'ignored'`/`'unknown'` (`'ignored'` = 'off' was accepted but the reply shows thinking anyway — warns once, result kept) and the legacy `thinkingApplied` is derived (`true`/`false`/`null`). Unset ⇒ request body byte-identical to before. Overridable per call via `generate(..., { thinkingMode })`; `null` suppresses an instance default. Any value but 'on'/'off' ⇒ `ValidationError`. A NEW field: it does not change the raw `thinking` option; setting both (after per-call override) ⇒ `ValidationError`. See `src/thinking-mode.js` / `thinkingSupport()`.
|
|
9
|
+
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model — just runs without it and reports `thinkingStatus:'unsupported'`, so the first member answers and the chain does not fall through).
|
|
10
10
|
*/
|
|
11
11
|
thinkingMode?: "on" | "off" | null | undefined;
|
|
12
12
|
exposeErrorBody?: boolean | undefined;
|
|
@@ -33,8 +33,8 @@ export type OllamaOptions = {
|
|
|
33
33
|
* @typedef {object} OllamaOptions
|
|
34
34
|
* @property {string} [model='llama3.2']
|
|
35
35
|
* @property {string} [url='http://localhost:11434']
|
|
36
|
-
* @property {'on'|'off'|null} [thinkingMode] - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right `think` (`/api/chat`; NOT live-verified against a real Ollama thinking model — reply signal `message.thinking`) request field for the provider/model,
|
|
37
|
-
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model —
|
|
36
|
+
* @property {'on'|'off'|null} [thinkingMode] - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right `think` (`/api/chat`; NOT live-verified against a real Ollama thinking model — reply signal `message.thinking`) request field for the provider/model, tries the next known shape when a shape request fails with a 400/422 (not the temperature path), and the call ALWAYS RUNS: if every shape fails that way, ONE plain request without a thinking field decides — plain OK ⇒ `thinkingStatus:'unsupported'`, plain fails ⇒ that plain request's error is thrown as-is; other failures propagate after one request; a model measured as not switchable is sent plain directly. Only the winning shape is cached per instance+model ('unsupported' is never cached); warns once per status. Every reply is checked: `GenerateResult.thinkingStatus` is `'applied'`/`'unsupported'`/`'ignored'`/`'unknown'` (`'ignored'` = 'off' was accepted but the reply shows thinking anyway — warns once, result kept) and the legacy `thinkingApplied` is derived (`true`/`false`/`null`). Unset ⇒ request body byte-identical to before. Overridable per call via `generate(..., { thinkingMode })`; `null` suppresses an instance default. Any value but 'on'/'off' ⇒ `ValidationError`. A NEW field: it does not change the raw `thinking` option; setting both (after per-call override) ⇒ `ValidationError`. See `src/thinking-mode.js` / `thinkingSupport()`.
|
|
37
|
+
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model — just runs without it and reports `thinkingStatus:'unsupported'`, so the first member answers and the chain does not fall through).
|
|
38
38
|
* @property {boolean} [exposeErrorBody=false]
|
|
39
39
|
* @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'`, `context.bound: 'idle'`) 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.)
|
|
40
40
|
* @property {number} [deadlineMs=0] - BA-19: TOTAL call-duration deadline in ms, beside `timeoutMs`. The idle bound resets on any socket activity, so a response that trickles a byte forever never trips it and hangs for hours. This is an absolute, non-resetting ceiling; on trip, `generate()` rejects with a TERMINAL `TimeoutError` (`code: 'EDEADLINE'`, `context.bound: 'deadline'`, `retryable: false`). DISABLED by default; `0`/`Infinity` disable. Overridable per call via `generate(..., { deadlineMs })`. (A cold large-model load is a legitimate long single call — leave this disabled or set it generously for such models.)
|
|
@@ -70,9 +70,8 @@ export class OllamaProvider {
|
|
|
70
70
|
* @throws {Error} `[OllamaProvider] ...` — on HTTP errors or invalid JSON response.
|
|
71
71
|
*/
|
|
72
72
|
generate(messages: Message[], tools?: ToolDef[], options?: Record<string, any>): Promise<GenerateResult>;
|
|
73
|
-
/** One-time warning that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
74
|
-
_warnTemperatureDropped(): void;
|
|
75
|
-
_warnedTempDropped: boolean | undefined;
|
|
73
|
+
/** One-time warning (per reason) that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
74
|
+
_warnTemperatureDropped(reason?: string): void;
|
|
76
75
|
/**
|
|
77
76
|
* @param {string} path
|
|
78
77
|
* @param {Record<string, any>} body
|
package/src/provider-ollama.js
CHANGED
|
@@ -7,7 +7,7 @@ const { normalizeStopReason } = require('./provider-stop-reason');
|
|
|
7
7
|
const { resolveTimeoutMs, applyRequestBounds, guardResponseSettles } = require('./provider-http');
|
|
8
8
|
const { hasUsageSignal } = require('./provider-usage');
|
|
9
9
|
const { parseToolCalls } = require('./provider-toolcalls');
|
|
10
|
-
const { normalizeThinkingMode, resolveThinkingMode, requestWithThinkingMode,
|
|
10
|
+
const { normalizeThinkingMode, resolveThinkingMode, requestWithThinkingMode, assessThinking } = require('./thinking-mode');
|
|
11
11
|
|
|
12
12
|
// BA-24: raw Ollama usage fields. Any present (even 0) ⇒ a usage signal; none ⇒ null (unpriceable).
|
|
13
13
|
const OLLAMA_USAGE_KEYS = ['prompt_eval_count', 'eval_count'];
|
|
@@ -20,8 +20,8 @@ const OLLAMA_USAGE_KEYS = ['prompt_eval_count', 'eval_count'];
|
|
|
20
20
|
* @typedef {object} OllamaOptions
|
|
21
21
|
* @property {string} [model='llama3.2']
|
|
22
22
|
* @property {string} [url='http://localhost:11434']
|
|
23
|
-
* @property {'on'|'off'|null} [thinkingMode] - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right `think` (`/api/chat`; NOT live-verified against a real Ollama thinking model — reply signal `message.thinking`) request field for the provider/model,
|
|
24
|
-
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model —
|
|
23
|
+
* @property {'on'|'off'|null} [thinkingMode] - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right `think` (`/api/chat`; NOT live-verified against a real Ollama thinking model — reply signal `message.thinking`) request field for the provider/model, tries the next known shape when a shape request fails with a 400/422 (not the temperature path), and the call ALWAYS RUNS: if every shape fails that way, ONE plain request without a thinking field decides — plain OK ⇒ `thinkingStatus:'unsupported'`, plain fails ⇒ that plain request's error is thrown as-is; other failures propagate after one request; a model measured as not switchable is sent plain directly. Only the winning shape is cached per instance+model ('unsupported' is never cached); warns once per status. Every reply is checked: `GenerateResult.thinkingStatus` is `'applied'`/`'unsupported'`/`'ignored'`/`'unknown'` (`'ignored'` = 'off' was accepted but the reply shows thinking anyway — warns once, result kept) and the legacy `thinkingApplied` is derived (`true`/`false`/`null`). Unset ⇒ request body byte-identical to before. Overridable per call via `generate(..., { thinkingMode })`; `null` suppresses an instance default. Any value but 'on'/'off' ⇒ `ValidationError`. A NEW field: it does not change the raw `thinking` option; setting both (after per-call override) ⇒ `ValidationError`. See `src/thinking-mode.js` / `thinkingSupport()`.
|
|
24
|
+
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model — just runs without it and reports `thinkingStatus:'unsupported'`, so the first member answers and the chain does not fall through).
|
|
25
25
|
* @property {boolean} [exposeErrorBody=false]
|
|
26
26
|
* @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'`, `context.bound: 'idle'`) 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.)
|
|
27
27
|
* @property {number} [deadlineMs=0] - BA-19: TOTAL call-duration deadline in ms, beside `timeoutMs`. The idle bound resets on any socket activity, so a response that trickles a byte forever never trips it and hangs for hours. This is an absolute, non-resetting ceiling; on trip, `generate()` rejects with a TERMINAL `TimeoutError` (`code: 'EDEADLINE'`, `context.bound: 'deadline'`, `retryable: false`). DISABLED by default; `0`/`Infinity` disable. Overridable per call via `generate(..., { deadlineMs })`. (A cold large-model load is a legitimate long single call — leave this disabled or set it generously for such models.)
|
|
@@ -94,15 +94,17 @@ class OllamaProvider {
|
|
|
94
94
|
// BA-10: graceful degrade if a model rejects a non-default `temperature` (Ollama nests it under
|
|
95
95
|
// `options`). Keyed off the API error text, so dormant on models that accept temperature.
|
|
96
96
|
const thinkingMode = resolveThinkingMode({ label: 'OllamaProvider', instanceMode: this.thinkingMode, options });
|
|
97
|
+
/** @type {{ unsupported: boolean }} */
|
|
98
|
+
const thinkingTrack = { unsupported: false };
|
|
97
99
|
const timeoutMs = resolveTimeoutMs(this.timeoutMs, options.timeoutMs);
|
|
98
100
|
const deadlineMs = resolveTimeoutMs(this.deadlineMs, options.deadlineMs, 0, 'deadlineMs');
|
|
99
101
|
const { data, temperatureDropped } = await requestWithTemperatureFallback({
|
|
100
102
|
request: () => thinkingMode
|
|
101
|
-
? requestWithThinkingMode({ kind: 'ollama', mode: thinkingMode, model: this.model,
|
|
103
|
+
? requestWithThinkingMode({ kind: 'ollama', mode: thinkingMode, model: this.model, body, cache: this._thinkingShapes, track: thinkingTrack, request: (b) => this._request('/api/chat', b, timeoutMs, deadlineMs) })
|
|
102
104
|
: this._request('/api/chat', body, timeoutMs, deadlineMs),
|
|
103
105
|
hadTemperature: () => body.options?.temperature != null,
|
|
104
106
|
stripTemperature: () => { if (body.options) delete body.options.temperature; },
|
|
105
|
-
warnOnce: () => this._warnTemperatureDropped(),
|
|
107
|
+
warnOnce: (reason) => this._warnTemperatureDropped(reason),
|
|
106
108
|
});
|
|
107
109
|
const msg = data.message || {};
|
|
108
110
|
|
|
@@ -136,15 +138,18 @@ class OllamaProvider {
|
|
|
136
138
|
? { inputTokens: data.prompt_eval_count || 0, outputTokens: data.eval_count || 0 }
|
|
137
139
|
: null,
|
|
138
140
|
...(temperatureDropped && { temperatureDropped: true }),
|
|
139
|
-
...(thinkingMode &&
|
|
141
|
+
...(thinkingMode && assessThinking({ kind: 'ollama', mode: thinkingMode, data, unsupported: thinkingTrack.unsupported, owner: this, label: 'OllamaProvider', model: this.model })),
|
|
140
142
|
};
|
|
141
143
|
}
|
|
142
144
|
|
|
143
|
-
/** One-time warning that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
144
|
-
_warnTemperatureDropped() {
|
|
145
|
-
|
|
146
|
-
this
|
|
147
|
-
|
|
145
|
+
/** One-time warning (per reason) that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
146
|
+
_warnTemperatureDropped(reason = 'unsupported') {
|
|
147
|
+
const flag = reason === 'thinking' ? '_warnedTempThinking' : '_warnedTempDropped';
|
|
148
|
+
if (this[flag]) return;
|
|
149
|
+
this[flag] = true;
|
|
150
|
+
console.warn(reason === 'thinking'
|
|
151
|
+
? `[OllamaProvider] '${this.model}': 'temperature' must be 1 while thinking is on — retrying without it. Further drops from this provider instance are silent.`
|
|
152
|
+
: `[OllamaProvider] '${this.model}' rejected a non-default 'temperature' (unsupported/deprecated) — retrying without it. Further drops from this provider instance are silent.`);
|
|
148
153
|
}
|
|
149
154
|
|
|
150
155
|
/**
|
package/src/provider-openai.d.ts
CHANGED
|
@@ -46,9 +46,9 @@ export type OpenAIOptions = {
|
|
|
46
46
|
*/
|
|
47
47
|
exposeMalformedArgs?: boolean | undefined;
|
|
48
48
|
/**
|
|
49
|
-
* - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right `reasoning_effort` (OpenAI) / `thinking` (DeepSeek — selected by model id `deepseek-*` or a `deepseek.com` baseUrl) request field for the provider/model,
|
|
50
|
-
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model —
|
|
51
|
-
* DeepSeek: `'on'` + a forced `toolChoice` (`'required'`/`{name}`)
|
|
49
|
+
* - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right `reasoning_effort` (OpenAI) / `thinking` (DeepSeek — selected by model id `deepseek-*` or a `deepseek.com` baseUrl) request field for the provider/model, tries the next known shape when a shape request fails with a 400/422 (not the temperature path), and the call ALWAYS RUNS: if every shape fails that way, ONE plain request without a thinking field decides — plain OK ⇒ `thinkingStatus:'unsupported'`, plain fails ⇒ that plain request's error is thrown as-is; other failures propagate after one request; a model measured as not switchable is sent plain directly. Only the winning shape is cached per instance+model ('unsupported' is never cached); warns once per status. Every reply is checked: `GenerateResult.thinkingStatus` is `'applied'`/`'unsupported'`/`'ignored'`/`'unknown'` (`'ignored'` = 'off' was accepted but the reply shows thinking anyway — warns once, result kept) and the legacy `thinkingApplied` is derived (`true`/`false`/`null`). Unset ⇒ request body byte-identical to before. Overridable per call via `generate(..., { thinkingMode })`; `null` suppresses an instance default. Any value but 'on'/'off' ⇒ `ValidationError`. A NEW field: it does not change the raw `thinking` option; setting both (after per-call override) ⇒ `ValidationError`. See `src/thinking-mode.js` / `thinkingSupport()`.
|
|
50
|
+
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model — just runs without it and reports `thinkingStatus:'unsupported'`, so the first member answers and the chain does not fall through).
|
|
51
|
+
* DeepSeek: `'on'` + a forced `toolChoice` (`'required'`/`{name}`) cannot work (the API 400s thinking mode on a forced tool_choice) ⇒ the call is sent with `thinking:{type:'disabled'}` (the only shape that works there) and reports `thinkingStatus:'unsupported'`.
|
|
52
52
|
*/
|
|
53
53
|
thinkingMode?: "on" | "off" | null | undefined;
|
|
54
54
|
/**
|
|
@@ -94,9 +94,9 @@ export type OpenAIOptions = {
|
|
|
94
94
|
* JSON fails to parse, also attach the FULL raw string (verbatim, uncapped) to `malformedToolCall.rawArguments`.
|
|
95
95
|
* Off by default, independent of `exposeErrorBody` — the raw blob is model-generated and may be large
|
|
96
96
|
* or sensitive, so it only reaches logs/audit rows if the caller opts in. See `src/provider-toolcalls.js`.
|
|
97
|
-
* @property {'on'|'off'|null} [thinkingMode] - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right `reasoning_effort` (OpenAI) / `thinking` (DeepSeek — selected by model id `deepseek-*` or a `deepseek.com` baseUrl) request field for the provider/model,
|
|
98
|
-
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model —
|
|
99
|
-
* DeepSeek: `'on'` + a forced `toolChoice` (`'required'`/`{name}`)
|
|
97
|
+
* @property {'on'|'off'|null} [thinkingMode] - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right `reasoning_effort` (OpenAI) / `thinking` (DeepSeek — selected by model id `deepseek-*` or a `deepseek.com` baseUrl) request field for the provider/model, tries the next known shape when a shape request fails with a 400/422 (not the temperature path), and the call ALWAYS RUNS: if every shape fails that way, ONE plain request without a thinking field decides — plain OK ⇒ `thinkingStatus:'unsupported'`, plain fails ⇒ that plain request's error is thrown as-is; other failures propagate after one request; a model measured as not switchable is sent plain directly. Only the winning shape is cached per instance+model ('unsupported' is never cached); warns once per status. Every reply is checked: `GenerateResult.thinkingStatus` is `'applied'`/`'unsupported'`/`'ignored'`/`'unknown'` (`'ignored'` = 'off' was accepted but the reply shows thinking anyway — warns once, result kept) and the legacy `thinkingApplied` is derived (`true`/`false`/`null`). Unset ⇒ request body byte-identical to before. Overridable per call via `generate(..., { thinkingMode })`; `null` suppresses an instance default. Any value but 'on'/'off' ⇒ `ValidationError`. A NEW field: it does not change the raw `thinking` option; setting both (after per-call override) ⇒ `ValidationError`. See `src/thinking-mode.js` / `thinkingSupport()`.
|
|
98
|
+
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model — just runs without it and reports `thinkingStatus:'unsupported'`, so the first member answers and the chain does not fall through).
|
|
99
|
+
* DeepSeek: `'on'` + a forced `toolChoice` (`'required'`/`{name}`) cannot work (the API 400s thinking mode on a forced tool_choice) ⇒ the call is sent with `thinking:{type:'disabled'}` (the only shape that works there) and reports `thinkingStatus:'unsupported'`.
|
|
100
100
|
* @property {any} [thinking] - Opt-in `thinking` request field (fwdloop DeepSeek ask — extends BA-7(b)'s Anthropic option), forwarded to `body.thinking`
|
|
101
101
|
* VERBATIM and unvalidated — e.g. `{ type: 'disabled' }`. Generalises the Anthropic provider's option to
|
|
102
102
|
* OpenAI-compatible endpoints. Motivating case: DeepSeek's OpenAI-compatible API (`baseUrl`
|
|
@@ -138,9 +138,8 @@ export class OpenAIProvider {
|
|
|
138
138
|
* @throws {Error} `[OpenAIProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response.
|
|
139
139
|
*/
|
|
140
140
|
generate(messages: Message[], tools?: ToolDef[], options?: Record<string, any>): Promise<GenerateResult>;
|
|
141
|
-
/** One-time warning that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
142
|
-
_warnTemperatureDropped(): void;
|
|
143
|
-
_warnedTempDropped: boolean | undefined;
|
|
141
|
+
/** One-time warning (per reason) that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
142
|
+
_warnTemperatureDropped(reason?: string): void;
|
|
144
143
|
/**
|
|
145
144
|
* Normalize OpenAI usage to the neutral {@link Usage} shape. OpenAI auto-caches prompt prefixes
|
|
146
145
|
* (>=1024 tokens) and reports the cached portion in `prompt_tokens_details.cached_tokens` —
|
package/src/provider-openai.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const https = require('https');
|
|
4
4
|
const http = require('http');
|
|
5
|
-
const { ProviderError
|
|
5
|
+
const { ProviderError } = require('./errors');
|
|
6
6
|
const { requestWithTemperatureFallback } = require('./provider-temperature');
|
|
7
7
|
const { normalizeStopReason } = require('./provider-stop-reason');
|
|
8
8
|
const { resolveTimeoutMs, applyRequestBounds, guardResponseSettles } = require('./provider-http');
|
|
9
9
|
const { hasUsageSignal } = require('./provider-usage');
|
|
10
10
|
const { parseToolCalls } = require('./provider-toolcalls');
|
|
11
|
-
const { normalizeThinkingMode, resolveThinkingMode, requestWithThinkingMode,
|
|
11
|
+
const { normalizeThinkingMode, resolveThinkingMode, requestWithThinkingMode, assessThinking, deepseekDisabledBody, isDeepSeek } = require('./thinking-mode');
|
|
12
12
|
|
|
13
13
|
// BA-24: raw OpenAI usage fields. Any present (even 0) ⇒ a usage signal; none ⇒ null (unpriceable).
|
|
14
14
|
const OPENAI_USAGE_KEYS = ['prompt_tokens', 'completion_tokens', 'prompt_tokens_details'];
|
|
@@ -77,9 +77,9 @@ function isLoopbackHost(hostname) {
|
|
|
77
77
|
* JSON fails to parse, also attach the FULL raw string (verbatim, uncapped) to `malformedToolCall.rawArguments`.
|
|
78
78
|
* Off by default, independent of `exposeErrorBody` — the raw blob is model-generated and may be large
|
|
79
79
|
* or sensitive, so it only reaches logs/audit rows if the caller opts in. See `src/provider-toolcalls.js`.
|
|
80
|
-
* @property {'on'|'off'|null} [thinkingMode] - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right `reasoning_effort` (OpenAI) / `thinking` (DeepSeek — selected by model id `deepseek-*` or a `deepseek.com` baseUrl) request field for the provider/model,
|
|
81
|
-
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model —
|
|
82
|
-
* DeepSeek: `'on'` + a forced `toolChoice` (`'required'`/`{name}`)
|
|
80
|
+
* @property {'on'|'off'|null} [thinkingMode] - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right `reasoning_effort` (OpenAI) / `thinking` (DeepSeek — selected by model id `deepseek-*` or a `deepseek.com` baseUrl) request field for the provider/model, tries the next known shape when a shape request fails with a 400/422 (not the temperature path), and the call ALWAYS RUNS: if every shape fails that way, ONE plain request without a thinking field decides — plain OK ⇒ `thinkingStatus:'unsupported'`, plain fails ⇒ that plain request's error is thrown as-is; other failures propagate after one request; a model measured as not switchable is sent plain directly. Only the winning shape is cached per instance+model ('unsupported' is never cached); warns once per status. Every reply is checked: `GenerateResult.thinkingStatus` is `'applied'`/`'unsupported'`/`'ignored'`/`'unknown'` (`'ignored'` = 'off' was accepted but the reply shows thinking anyway — warns once, result kept) and the legacy `thinkingApplied` is derived (`true`/`false`/`null`). Unset ⇒ request body byte-identical to before. Overridable per call via `generate(..., { thinkingMode })`; `null` suppresses an instance default. Any value but 'on'/'off' ⇒ `ValidationError`. A NEW field: it does not change the raw `thinking` option; setting both (after per-call override) ⇒ `ValidationError`. See `src/thinking-mode.js` / `thinkingSupport()`.
|
|
81
|
+
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model — just runs without it and reports `thinkingStatus:'unsupported'`, so the first member answers and the chain does not fall through).
|
|
82
|
+
* DeepSeek: `'on'` + a forced `toolChoice` (`'required'`/`{name}`) cannot work (the API 400s thinking mode on a forced tool_choice) ⇒ the call is sent with `thinking:{type:'disabled'}` (the only shape that works there) and reports `thinkingStatus:'unsupported'`.
|
|
83
83
|
* @property {any} [thinking] - Opt-in `thinking` request field (fwdloop DeepSeek ask — extends BA-7(b)'s Anthropic option), forwarded to `body.thinking`
|
|
84
84
|
* VERBATIM and unvalidated — e.g. `{ type: 'disabled' }`. Generalises the Anthropic provider's option to
|
|
85
85
|
* OpenAI-compatible endpoints. Motivating case: DeepSeek's OpenAI-compatible API (`baseUrl`
|
|
@@ -153,10 +153,12 @@ class OpenAIProvider {
|
|
|
153
153
|
// valid choice with nothing to force is dropped (documented), while absent ⇒ the API default 'auto'.
|
|
154
154
|
const toolChoice = toOpenAIToolChoice(options.toolChoice);
|
|
155
155
|
const thinkingMode = resolveThinkingMode({ label: 'OpenAIProvider', instanceMode: this.thinkingMode, options, instanceThinking: this.thinking, hasRawThinking: true });
|
|
156
|
+
/** @type {{ unsupported: boolean, reason?: string }} */
|
|
157
|
+
const thinkingTrack = { unsupported: false };
|
|
156
158
|
const thinkingKind = thinkingMode ? (isDeepSeek(this.model, this.baseUrl) ? 'deepseek' : 'openai') : null;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
// DeepSeek 'on' + a forced tool choice is impossible (the API 400s thinking mode on it): the call still
|
|
160
|
+
// runs, with the only shape that works there (thinking:{type:'disabled'}), and reports 'unsupported'.
|
|
161
|
+
const forcedDeepseekOn = thinkingKind === 'deepseek' && thinkingMode === 'on' && tools.length > 0 && toolChoice != null && toolChoice !== 'auto';
|
|
160
162
|
if (tools.length > 0) {
|
|
161
163
|
body.tools = tools.map(t => ({
|
|
162
164
|
type: 'function',
|
|
@@ -170,12 +172,14 @@ class OpenAIProvider {
|
|
|
170
172
|
const timeoutMs = resolveTimeoutMs(this.timeoutMs, options.timeoutMs);
|
|
171
173
|
const deadlineMs = resolveTimeoutMs(this.deadlineMs, options.deadlineMs, 0, 'deadlineMs');
|
|
172
174
|
const { data, temperatureDropped } = await requestWithTemperatureFallback({
|
|
173
|
-
request: () =>
|
|
174
|
-
?
|
|
175
|
+
request: () => forcedDeepseekOn
|
|
176
|
+
? (thinkingTrack.unsupported = true, thinkingTrack.reason = 'forced-tool', this._request('/chat/completions', deepseekDisabledBody(body), timeoutMs, deadlineMs))
|
|
177
|
+
: thinkingKind
|
|
178
|
+
? requestWithThinkingMode({ kind: /** @type {'openai'|'deepseek'} */ (thinkingKind), mode: /** @type {'on'|'off'} */ (thinkingMode), model: this.model, body, cache: this._thinkingShapes, track: thinkingTrack, request: (b) => this._request('/chat/completions', b, timeoutMs, deadlineMs) })
|
|
175
179
|
: this._request('/chat/completions', body, timeoutMs, deadlineMs),
|
|
176
180
|
hadTemperature: () => body.temperature != null,
|
|
177
181
|
stripTemperature: () => { delete body.temperature; },
|
|
178
|
-
warnOnce: () => this._warnTemperatureDropped(),
|
|
182
|
+
warnOnce: (reason) => this._warnTemperatureDropped(reason),
|
|
179
183
|
});
|
|
180
184
|
// BA-27: a successful 200 whose body carries no `choices` (some OpenAI-compat servers return a
|
|
181
185
|
// 4xx-shaped error object with HTTP 200) reached `data.choices[0]` as a bare TypeError with no
|
|
@@ -215,15 +219,18 @@ class OpenAIProvider {
|
|
|
215
219
|
stopReason: normalizeStopReason(choice?.finish_reason, 'openai', { hasToolCalls: toolCalls.length > 0 }),
|
|
216
220
|
usage: this._normalizeUsage(data.usage),
|
|
217
221
|
...(temperatureDropped && { temperatureDropped: true }),
|
|
218
|
-
...(thinkingKind &&
|
|
222
|
+
...(thinkingKind && assessThinking({ kind: /** @type {'openai'|'deepseek'} */ (thinkingKind), mode: /** @type {'on'|'off'} */ (thinkingMode), data, unsupported: thinkingTrack.unsupported, reason: thinkingTrack.reason, owner: this, label: 'OpenAIProvider', model: this.model })),
|
|
219
223
|
};
|
|
220
224
|
}
|
|
221
225
|
|
|
222
|
-
/** One-time warning that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
223
|
-
_warnTemperatureDropped() {
|
|
224
|
-
|
|
225
|
-
this
|
|
226
|
-
|
|
226
|
+
/** One-time warning (per reason) that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
227
|
+
_warnTemperatureDropped(reason = 'unsupported') {
|
|
228
|
+
const flag = reason === 'thinking' ? '_warnedTempThinking' : '_warnedTempDropped';
|
|
229
|
+
if (this[flag]) return;
|
|
230
|
+
this[flag] = true;
|
|
231
|
+
console.warn(reason === 'thinking'
|
|
232
|
+
? `[OpenAIProvider] '${this.model}': 'temperature' must be 1 while thinking is on — retrying without it. Further drops from this provider instance are silent.`
|
|
233
|
+
: `[OpenAIProvider] '${this.model}' rejected a non-default 'temperature' (unsupported/deprecated) — retrying without it. Further drops from this provider instance are silent.`);
|
|
227
234
|
}
|
|
228
235
|
|
|
229
236
|
/**
|
|
@@ -20,7 +20,7 @@ export function isTemperatureThinkingConflict(err: any): boolean;
|
|
|
20
20
|
* @param {() => boolean} opts.hadTemperature - was a temperature actually in the request body?
|
|
21
21
|
* @param {() => void} opts.stripTemperature - mutate the request body to remove the temperature.
|
|
22
22
|
* @param {boolean} [opts.thinkingOn] - a thinkingMode 'on' shape is being sent: ALSO treat a 400 naming both temperature and thinking as droppable (thinking wins). Default off ⇒ behavior unchanged.
|
|
23
|
-
* @param {() => void} [opts.warnOnce] - emit the one-time degrade warning (caller dedupes per instance).
|
|
23
|
+
* @param {(reason: 'unsupported'|'thinking') => void} [opts.warnOnce] - emit the one-time degrade warning for that reason (caller dedupes per instance AND reason).
|
|
24
24
|
* @returns {Promise<{ data: any, temperatureDropped: boolean }>}
|
|
25
25
|
*/
|
|
26
26
|
export function requestWithTemperatureFallback({ request, hadTemperature, stripTemperature, thinkingOn, warnOnce }: {
|
|
@@ -28,7 +28,7 @@ export function requestWithTemperatureFallback({ request, hadTemperature, stripT
|
|
|
28
28
|
hadTemperature: () => boolean;
|
|
29
29
|
stripTemperature: () => void;
|
|
30
30
|
thinkingOn?: boolean | undefined;
|
|
31
|
-
warnOnce?: (() => void) | undefined;
|
|
31
|
+
warnOnce?: ((reason: "unsupported" | "thinking") => void) | undefined;
|
|
32
32
|
}): Promise<{
|
|
33
33
|
data: any;
|
|
34
34
|
temperatureDropped: boolean;
|
|
@@ -58,16 +58,18 @@ function isTemperatureThinkingConflict(err) {
|
|
|
58
58
|
* @param {() => boolean} opts.hadTemperature - was a temperature actually in the request body?
|
|
59
59
|
* @param {() => void} opts.stripTemperature - mutate the request body to remove the temperature.
|
|
60
60
|
* @param {boolean} [opts.thinkingOn] - a thinkingMode 'on' shape is being sent: ALSO treat a 400 naming both temperature and thinking as droppable (thinking wins). Default off ⇒ behavior unchanged.
|
|
61
|
-
* @param {() => void} [opts.warnOnce] - emit the one-time degrade warning (caller dedupes per instance).
|
|
61
|
+
* @param {(reason: 'unsupported'|'thinking') => void} [opts.warnOnce] - emit the one-time degrade warning for that reason (caller dedupes per instance AND reason).
|
|
62
62
|
* @returns {Promise<{ data: any, temperatureDropped: boolean }>}
|
|
63
63
|
*/
|
|
64
64
|
async function requestWithTemperatureFallback({ request, hadTemperature, stripTemperature, thinkingOn, warnOnce }) {
|
|
65
65
|
try {
|
|
66
66
|
return { data: await request(), temperatureDropped: false };
|
|
67
67
|
} catch (err) {
|
|
68
|
-
|
|
68
|
+
// 'unsupported' wins when both match (the model refuses temperature outright); else the thinking conflict.
|
|
69
|
+
const reason = isTemperatureUnsupported(err) ? 'unsupported' : (thinkingOn === true && isTemperatureThinkingConflict(err)) ? 'thinking' : null;
|
|
70
|
+
if (reason && hadTemperature()) {
|
|
69
71
|
stripTemperature();
|
|
70
|
-
if (warnOnce) warnOnce();
|
|
72
|
+
if (warnOnce) warnOnce(reason);
|
|
71
73
|
return { data: await request(), temperatureDropped: true };
|
|
72
74
|
}
|
|
73
75
|
throw err;
|