bare-agent 0.50.0 → 0.51.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 +8 -0
- package/package.json +1 -1
- package/primitives.json +9 -0
- package/src/loop.d.ts +4 -2
- package/src/loop.js +21 -12
- package/src/provider-anthropic.d.ts +11 -1
- package/src/provider-anthropic.js +16 -2
- package/src/provider-clipipe.d.ts +5 -1
- package/src/provider-clipipe.js +10 -2
- package/src/provider-fallback.d.ts +3 -0
- package/src/provider-fallback.js +3 -0
- package/src/provider-gemini.d.ts +11 -1
- package/src/provider-gemini.js +13 -2
- package/src/provider-ollama.d.ts +11 -1
- package/src/provider-ollama.js +13 -2
- package/src/provider-openai.d.ts +13 -1
- package/src/provider-openai.js +20 -3
- package/src/provider-temperature.d.ts +9 -1
- package/src/provider-temperature.js +20 -3
- package/src/providers.d.ts +2 -1
- package/src/providers.js +3 -0
- package/src/thinking-mode.d.ts +142 -0
- package/src/thinking-mode.js +384 -0
- package/types/index.d.ts +9 -0
package/src/provider-ollama.js
CHANGED
|
@@ -7,6 +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, assessThinkingApplied } = require('./thinking-mode');
|
|
10
11
|
|
|
11
12
|
// BA-24: raw Ollama usage fields. Any present (even 0) ⇒ a usage signal; none ⇒ null (unpriceable).
|
|
12
13
|
const OLLAMA_USAGE_KEYS = ['prompt_eval_count', 'eval_count'];
|
|
@@ -19,6 +20,8 @@ const OLLAMA_USAGE_KEYS = ['prompt_eval_count', 'eval_count'];
|
|
|
19
20
|
* @typedef {object} OllamaOptions
|
|
20
21
|
* @property {string} [model='llama3.2']
|
|
21
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, retries the known alternative shapes if the API rejects the field by name, and THROWS a `ValidationError` (naming what it tried) if every shape is rejected — or before any request when the model is measured as not switchable. Every reply is checked: `GenerateResult.thinkingApplied` is `true`/`false`/`null` (`false` = 'off' was sent but the reply shows thinking happened — warns once per instance, result kept). 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 — throws `ValidationError`, which the chain treats like any failure and falls through).
|
|
22
25
|
* @property {boolean} [exposeErrorBody=false]
|
|
23
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.)
|
|
24
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.)
|
|
@@ -41,6 +44,10 @@ class OllamaProvider {
|
|
|
41
44
|
this.url = options.url || 'http://localhost:11434';
|
|
42
45
|
// See OpenAIProvider: attach full upstream body to err.body only on opt-in.
|
|
43
46
|
this.exposeErrorBody = options.exposeErrorBody === true;
|
|
47
|
+
// Normalized on/off switch (src/thinking-mode.js). Ollama has no raw `thinking` option to conflict with.
|
|
48
|
+
this.thinkingMode = normalizeThinkingMode(options.thinkingMode, 'OllamaProvider');
|
|
49
|
+
/** @type {Map<string, string>} shape that worked per mode+model (no re-probing) */
|
|
50
|
+
this._thinkingShapes = new Map();
|
|
44
51
|
// BA-18: request/idle timeout (ms). Resolved at call time (default 600000; 0/Infinity disable).
|
|
45
52
|
this.timeoutMs = options.timeoutMs;
|
|
46
53
|
// BA-19: total call-duration deadline (ms). Resolved at call time (default 0 = disabled).
|
|
@@ -53,7 +60,7 @@ class OllamaProvider {
|
|
|
53
60
|
* Generate a response from a local Ollama instance.
|
|
54
61
|
* @param {Message[]} messages - Conversation messages.
|
|
55
62
|
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
56
|
-
* @param {Record<string, any>} [options={}] - Options (`temperature`, `maxTokens`, `timeoutMs` — a per-call override of the constructor's `timeoutMs`, see BA-18; `deadlineMs` — a per-call override of the constructor's `deadlineMs`, see BA-19).
|
|
63
|
+
* @param {Record<string, any>} [options={}] - Options (`thinkingMode` — per-call override of the constructor's, `null` suppresses; `temperature`, `maxTokens`, `timeoutMs` — a per-call override of the constructor's `timeoutMs`, see BA-18; `deadlineMs` — a per-call override of the constructor's `deadlineMs`, see BA-19).
|
|
57
64
|
* @returns {Promise<GenerateResult>}
|
|
58
65
|
* @throws {Error} `[OllamaProvider] ...` — on HTTP errors or invalid JSON response.
|
|
59
66
|
*/
|
|
@@ -86,10 +93,13 @@ class OllamaProvider {
|
|
|
86
93
|
|
|
87
94
|
// BA-10: graceful degrade if a model rejects a non-default `temperature` (Ollama nests it under
|
|
88
95
|
// `options`). Keyed off the API error text, so dormant on models that accept temperature.
|
|
96
|
+
const thinkingMode = resolveThinkingMode({ label: 'OllamaProvider', instanceMode: this.thinkingMode, options });
|
|
89
97
|
const timeoutMs = resolveTimeoutMs(this.timeoutMs, options.timeoutMs);
|
|
90
98
|
const deadlineMs = resolveTimeoutMs(this.deadlineMs, options.deadlineMs, 0, 'deadlineMs');
|
|
91
99
|
const { data, temperatureDropped } = await requestWithTemperatureFallback({
|
|
92
|
-
request: () =>
|
|
100
|
+
request: () => thinkingMode
|
|
101
|
+
? requestWithThinkingMode({ kind: 'ollama', mode: thinkingMode, model: this.model, label: 'OllamaProvider', body, cache: this._thinkingShapes, request: (b) => this._request('/api/chat', b, timeoutMs, deadlineMs) })
|
|
102
|
+
: this._request('/api/chat', body, timeoutMs, deadlineMs),
|
|
93
103
|
hadTemperature: () => body.options?.temperature != null,
|
|
94
104
|
stripTemperature: () => { if (body.options) delete body.options.temperature; },
|
|
95
105
|
warnOnce: () => this._warnTemperatureDropped(),
|
|
@@ -126,6 +136,7 @@ class OllamaProvider {
|
|
|
126
136
|
? { inputTokens: data.prompt_eval_count || 0, outputTokens: data.eval_count || 0 }
|
|
127
137
|
: null,
|
|
128
138
|
...(temperatureDropped && { temperatureDropped: true }),
|
|
139
|
+
...(thinkingMode && { thinkingApplied: assessThinkingApplied({ kind: 'ollama', mode: thinkingMode, data, owner: this, label: 'OllamaProvider', model: this.model }) }),
|
|
129
140
|
};
|
|
130
141
|
}
|
|
131
142
|
|
package/src/provider-openai.d.ts
CHANGED
|
@@ -45,6 +45,12 @@ export type OpenAIOptions = {
|
|
|
45
45
|
* or sensitive, so it only reaches logs/audit rows if the caller opts in. See `src/provider-toolcalls.js`.
|
|
46
46
|
*/
|
|
47
47
|
exposeMalformedArgs?: boolean | undefined;
|
|
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, retries the known alternative shapes if the API rejects the field by name, and THROWS a `ValidationError` (naming what it tried) if every shape is rejected — or before any request when the model is measured as not switchable. Every reply is checked: `GenerateResult.thinkingApplied` is `true`/`false`/`null` (`false` = 'off' was sent but the reply shows thinking happened — warns once per instance, result kept). 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 — throws `ValidationError`, which the chain treats like any failure and falls through).
|
|
51
|
+
* DeepSeek: `'on'` + a forced `toolChoice` (`'required'`/`{name}`) ⇒ `ValidationError` before sending (the API 400s thinking mode on a forced tool_choice).
|
|
52
|
+
*/
|
|
53
|
+
thinkingMode?: "on" | "off" | null | undefined;
|
|
48
54
|
/**
|
|
49
55
|
* - Opt-in `thinking` request field (fwdloop DeepSeek ask — extends BA-7(b)'s Anthropic option), forwarded to `body.thinking`
|
|
50
56
|
* VERBATIM and unvalidated — e.g. `{ type: 'disabled' }`. Generalises the Anthropic provider's option to
|
|
@@ -88,6 +94,9 @@ export type OpenAIOptions = {
|
|
|
88
94
|
* JSON fails to parse, also attach the FULL raw string (verbatim, uncapped) to `malformedToolCall.rawArguments`.
|
|
89
95
|
* Off by default, independent of `exposeErrorBody` — the raw blob is model-generated and may be large
|
|
90
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, retries the known alternative shapes if the API rejects the field by name, and THROWS a `ValidationError` (naming what it tried) if every shape is rejected — or before any request when the model is measured as not switchable. Every reply is checked: `GenerateResult.thinkingApplied` is `true`/`false`/`null` (`false` = 'off' was sent but the reply shows thinking happened — warns once per instance, result kept). 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 — throws `ValidationError`, which the chain treats like any failure and falls through).
|
|
99
|
+
* DeepSeek: `'on'` + a forced `toolChoice` (`'required'`/`{name}`) ⇒ `ValidationError` before sending (the API 400s thinking mode on a forced tool_choice).
|
|
91
100
|
* @property {any} [thinking] - Opt-in `thinking` request field (fwdloop DeepSeek ask — extends BA-7(b)'s Anthropic option), forwarded to `body.thinking`
|
|
92
101
|
* VERBATIM and unvalidated — e.g. `{ type: 'disabled' }`. Generalises the Anthropic provider's option to
|
|
93
102
|
* OpenAI-compatible endpoints. Motivating case: DeepSeek's OpenAI-compatible API (`baseUrl`
|
|
@@ -117,11 +126,14 @@ export class OpenAIProvider {
|
|
|
117
126
|
legacyMaxTokens: boolean;
|
|
118
127
|
exposeMalformedArgs: boolean;
|
|
119
128
|
thinking: any;
|
|
129
|
+
thinkingMode: "on" | "off" | null;
|
|
130
|
+
/** @type {Map<string, string>} shape that worked per mode+model (no re-probing) */
|
|
131
|
+
_thinkingShapes: Map<string, string>;
|
|
120
132
|
/**
|
|
121
133
|
* Generate a response from the OpenAI API.
|
|
122
134
|
* @param {Message[]} messages - Conversation messages.
|
|
123
135
|
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
124
|
-
* @param {Record<string, any>} [options={}] - Options (temperature, maxTokens, timeoutMs — a per-call override of the constructor's `timeoutMs`, see BA-18; deadlineMs — a per-call override of the constructor's `deadlineMs`, see BA-19; toolChoice — `'auto'` | `'required'` | `{ name }`, forwarded as OpenAI `tool_choice`, applied only when `tools` are present; thinking — per-call override of the constructor's `thinking`, `null` suppresses an instance default, see BA-7 (b)).
|
|
136
|
+
* @param {Record<string, any>} [options={}] - Options (thinkingMode — per-call override of the constructor's, `null` suppresses; temperature, maxTokens, timeoutMs — a per-call override of the constructor's `timeoutMs`, see BA-18; deadlineMs — a per-call override of the constructor's `deadlineMs`, see BA-19; toolChoice — `'auto'` | `'required'` | `{ name }`, forwarded as OpenAI `tool_choice`, applied only when `tools` are present; thinking — per-call override of the constructor's `thinking`, `null` suppresses an instance default, see BA-7 (b)).
|
|
125
137
|
* @returns {Promise<GenerateResult>}
|
|
126
138
|
* @throws {Error} `[OpenAIProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response.
|
|
127
139
|
*/
|
package/src/provider-openai.js
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const https = require('https');
|
|
4
4
|
const http = require('http');
|
|
5
|
-
const { ProviderError } = require('./errors');
|
|
5
|
+
const { ProviderError, ValidationError } = 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, assessThinkingApplied, isDeepSeek } = require('./thinking-mode');
|
|
11
12
|
|
|
12
13
|
// BA-24: raw OpenAI usage fields. Any present (even 0) ⇒ a usage signal; none ⇒ null (unpriceable).
|
|
13
14
|
const OPENAI_USAGE_KEYS = ['prompt_tokens', 'completion_tokens', 'prompt_tokens_details'];
|
|
@@ -76,6 +77,9 @@ function isLoopbackHost(hostname) {
|
|
|
76
77
|
* JSON fails to parse, also attach the FULL raw string (verbatim, uncapped) to `malformedToolCall.rawArguments`.
|
|
77
78
|
* Off by default, independent of `exposeErrorBody` — the raw blob is model-generated and may be large
|
|
78
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, retries the known alternative shapes if the API rejects the field by name, and THROWS a `ValidationError` (naming what it tried) if every shape is rejected — or before any request when the model is measured as not switchable. Every reply is checked: `GenerateResult.thinkingApplied` is `true`/`false`/`null` (`false` = 'off' was sent but the reply shows thinking happened — warns once per instance, result kept). 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 — throws `ValidationError`, which the chain treats like any failure and falls through).
|
|
82
|
+
* DeepSeek: `'on'` + a forced `toolChoice` (`'required'`/`{name}`) ⇒ `ValidationError` before sending (the API 400s thinking mode on a forced tool_choice).
|
|
79
83
|
* @property {any} [thinking] - Opt-in `thinking` request field (fwdloop DeepSeek ask — extends BA-7(b)'s Anthropic option), forwarded to `body.thinking`
|
|
80
84
|
* VERBATIM and unvalidated — e.g. `{ type: 'disabled' }`. Generalises the Anthropic provider's option to
|
|
81
85
|
* OpenAI-compatible endpoints. Motivating case: DeepSeek's OpenAI-compatible API (`baseUrl`
|
|
@@ -112,13 +116,18 @@ class OpenAIProvider {
|
|
|
112
116
|
// BA-7 (b) / fwdloop: forwarded to `body.thinking` VERBATIM — unvalidated, un-reshaped (mirrors the
|
|
113
117
|
// Anthropic provider). Null/unset ⇒ no `thinking` key, byte-identical to the pre-option body.
|
|
114
118
|
this.thinking = options.thinking != null ? options.thinking : null;
|
|
119
|
+
// Normalized on/off switch (src/thinking-mode.js); conflict with raw `thinking` is a ValidationError.
|
|
120
|
+
this.thinkingMode = normalizeThinkingMode(options.thinkingMode, 'OpenAIProvider');
|
|
121
|
+
resolveThinkingMode({ label: 'OpenAIProvider', instanceMode: this.thinkingMode, options: {}, instanceThinking: this.thinking, hasRawThinking: true });
|
|
122
|
+
/** @type {Map<string, string>} shape that worked per mode+model (no re-probing) */
|
|
123
|
+
this._thinkingShapes = new Map();
|
|
115
124
|
}
|
|
116
125
|
|
|
117
126
|
/**
|
|
118
127
|
* Generate a response from the OpenAI API.
|
|
119
128
|
* @param {Message[]} messages - Conversation messages.
|
|
120
129
|
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
121
|
-
* @param {Record<string, any>} [options={}] - Options (temperature, maxTokens, timeoutMs — a per-call override of the constructor's `timeoutMs`, see BA-18; deadlineMs — a per-call override of the constructor's `deadlineMs`, see BA-19; toolChoice — `'auto'` | `'required'` | `{ name }`, forwarded as OpenAI `tool_choice`, applied only when `tools` are present; thinking — per-call override of the constructor's `thinking`, `null` suppresses an instance default, see BA-7 (b)).
|
|
130
|
+
* @param {Record<string, any>} [options={}] - Options (thinkingMode — per-call override of the constructor's, `null` suppresses; temperature, maxTokens, timeoutMs — a per-call override of the constructor's `timeoutMs`, see BA-18; deadlineMs — a per-call override of the constructor's `deadlineMs`, see BA-19; toolChoice — `'auto'` | `'required'` | `{ name }`, forwarded as OpenAI `tool_choice`, applied only when `tools` are present; thinking — per-call override of the constructor's `thinking`, `null` suppresses an instance default, see BA-7 (b)).
|
|
122
131
|
* @returns {Promise<GenerateResult>}
|
|
123
132
|
* @throws {Error} `[OpenAIProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response.
|
|
124
133
|
*/
|
|
@@ -143,6 +152,11 @@ class OpenAIProvider {
|
|
|
143
152
|
// empty. Attach it only when tools are present: OpenAI 400s on a tool_choice with no tools, so a
|
|
144
153
|
// valid choice with nothing to force is dropped (documented), while absent ⇒ the API default 'auto'.
|
|
145
154
|
const toolChoice = toOpenAIToolChoice(options.toolChoice);
|
|
155
|
+
const thinkingMode = resolveThinkingMode({ label: 'OpenAIProvider', instanceMode: this.thinkingMode, options, instanceThinking: this.thinking, hasRawThinking: true });
|
|
156
|
+
const thinkingKind = thinkingMode ? (isDeepSeek(this.model, this.baseUrl) ? 'deepseek' : 'openai') : null;
|
|
157
|
+
if (thinkingKind === 'deepseek' && thinkingMode === 'on' && tools.length > 0 && toolChoice != null && toolChoice !== 'auto') {
|
|
158
|
+
throw new ValidationError("[OpenAIProvider] thinkingMode 'on' cannot be combined with a forced toolChoice on DeepSeek (the API 400s: thinking mode does not support a forced tool_choice) — use thinkingMode 'off' or toolChoice 'auto'.", { context: { lib: 'bare-agent', provider: 'deepseek', model: this.model } });
|
|
159
|
+
}
|
|
146
160
|
if (tools.length > 0) {
|
|
147
161
|
body.tools = tools.map(t => ({
|
|
148
162
|
type: 'function',
|
|
@@ -156,7 +170,9 @@ class OpenAIProvider {
|
|
|
156
170
|
const timeoutMs = resolveTimeoutMs(this.timeoutMs, options.timeoutMs);
|
|
157
171
|
const deadlineMs = resolveTimeoutMs(this.deadlineMs, options.deadlineMs, 0, 'deadlineMs');
|
|
158
172
|
const { data, temperatureDropped } = await requestWithTemperatureFallback({
|
|
159
|
-
request: () =>
|
|
173
|
+
request: () => thinkingKind
|
|
174
|
+
? requestWithThinkingMode({ kind: /** @type {'openai'|'deepseek'} */ (thinkingKind), mode: /** @type {'on'|'off'} */ (thinkingMode), model: this.model, label: 'OpenAIProvider', body, cache: this._thinkingShapes, request: (b) => this._request('/chat/completions', b, timeoutMs, deadlineMs) })
|
|
175
|
+
: this._request('/chat/completions', body, timeoutMs, deadlineMs),
|
|
160
176
|
hadTemperature: () => body.temperature != null,
|
|
161
177
|
stripTemperature: () => { delete body.temperature; },
|
|
162
178
|
warnOnce: () => this._warnTemperatureDropped(),
|
|
@@ -199,6 +215,7 @@ class OpenAIProvider {
|
|
|
199
215
|
stopReason: normalizeStopReason(choice?.finish_reason, 'openai', { hasToolCalls: toolCalls.length > 0 }),
|
|
200
216
|
usage: this._normalizeUsage(data.usage),
|
|
201
217
|
...(temperatureDropped && { temperatureDropped: true }),
|
|
218
|
+
...(thinkingKind && { thinkingApplied: assessThinkingApplied({ kind: /** @type {'openai'|'deepseek'} */ (thinkingKind), mode: /** @type {'on'|'off'} */ (thinkingMode), data, owner: this, label: 'OpenAIProvider', model: this.model }) }),
|
|
202
219
|
};
|
|
203
220
|
}
|
|
204
221
|
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
* @returns {boolean}
|
|
5
5
|
*/
|
|
6
6
|
export function isTemperatureUnsupported(err: any): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Is this a 400 saying `temperature` conflicts with enabled thinking (names both)?
|
|
9
|
+
* @param {any} err
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
export function isTemperatureThinkingConflict(err: any): boolean;
|
|
7
13
|
/**
|
|
8
14
|
* Issue a provider request; if it 400s because `temperature` is unsupported AND a temperature was
|
|
9
15
|
* actually sent, strip it and retry ONCE. Returns whether the temperature was dropped so the caller
|
|
@@ -13,13 +19,15 @@ export function isTemperatureUnsupported(err: any): boolean;
|
|
|
13
19
|
* @param {() => Promise<any>} opts.request - issues the API call (rejects `ProviderError` on 4xx).
|
|
14
20
|
* @param {() => boolean} opts.hadTemperature - was a temperature actually in the request body?
|
|
15
21
|
* @param {() => void} opts.stripTemperature - mutate the request body to remove the temperature.
|
|
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.
|
|
16
23
|
* @param {() => void} [opts.warnOnce] - emit the one-time degrade warning (caller dedupes per instance).
|
|
17
24
|
* @returns {Promise<{ data: any, temperatureDropped: boolean }>}
|
|
18
25
|
*/
|
|
19
|
-
export function requestWithTemperatureFallback({ request, hadTemperature, stripTemperature, warnOnce }: {
|
|
26
|
+
export function requestWithTemperatureFallback({ request, hadTemperature, stripTemperature, thinkingOn, warnOnce }: {
|
|
20
27
|
request: () => Promise<any>;
|
|
21
28
|
hadTemperature: () => boolean;
|
|
22
29
|
stripTemperature: () => void;
|
|
30
|
+
thinkingOn?: boolean | undefined;
|
|
23
31
|
warnOnce?: (() => void) | undefined;
|
|
24
32
|
}): Promise<{
|
|
25
33
|
data: any;
|
|
@@ -32,6 +32,22 @@ function isTemperatureUnsupported(err) {
|
|
|
32
32
|
return !!err && err.status === 400 && TEMP_NAMED.test(msg) && TEMP_UNSUPPORTED.test(msg);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// Anthropic with thinking ENABLED rejects any non-default temperature ("`temperature` may only be set to 1
|
|
36
|
+
// when thinking is enabled" — live-confirmed 2026-10-11 on haiku-4-5; sonnet-5 appends "or in adaptive mode"). Thinking-on wins: the
|
|
37
|
+
// caller asked for thinking explicitly, so the temperature is what gets dropped. Narrow: a 400 that names
|
|
38
|
+
// BOTH words (no `.*`, so ReDoS-safe); only consulted when the caller passes `thinkingOn`.
|
|
39
|
+
const THINKING_NAMED = /thinking/i;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Is this a 400 saying `temperature` conflicts with enabled thinking (names both)?
|
|
43
|
+
* @param {any} err
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
function isTemperatureThinkingConflict(err) {
|
|
47
|
+
const msg = err && typeof err.message === 'string' ? err.message : '';
|
|
48
|
+
return !!err && err.status === 400 && TEMP_NAMED.test(msg) && THINKING_NAMED.test(msg);
|
|
49
|
+
}
|
|
50
|
+
|
|
35
51
|
/**
|
|
36
52
|
* Issue a provider request; if it 400s because `temperature` is unsupported AND a temperature was
|
|
37
53
|
* actually sent, strip it and retry ONCE. Returns whether the temperature was dropped so the caller
|
|
@@ -41,14 +57,15 @@ function isTemperatureUnsupported(err) {
|
|
|
41
57
|
* @param {() => Promise<any>} opts.request - issues the API call (rejects `ProviderError` on 4xx).
|
|
42
58
|
* @param {() => boolean} opts.hadTemperature - was a temperature actually in the request body?
|
|
43
59
|
* @param {() => void} opts.stripTemperature - mutate the request body to remove the temperature.
|
|
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.
|
|
44
61
|
* @param {() => void} [opts.warnOnce] - emit the one-time degrade warning (caller dedupes per instance).
|
|
45
62
|
* @returns {Promise<{ data: any, temperatureDropped: boolean }>}
|
|
46
63
|
*/
|
|
47
|
-
async function requestWithTemperatureFallback({ request, hadTemperature, stripTemperature, warnOnce }) {
|
|
64
|
+
async function requestWithTemperatureFallback({ request, hadTemperature, stripTemperature, thinkingOn, warnOnce }) {
|
|
48
65
|
try {
|
|
49
66
|
return { data: await request(), temperatureDropped: false };
|
|
50
67
|
} catch (err) {
|
|
51
|
-
if (isTemperatureUnsupported(err) && hadTemperature()) {
|
|
68
|
+
if ((isTemperatureUnsupported(err) || (thinkingOn === true && isTemperatureThinkingConflict(err))) && hadTemperature()) {
|
|
52
69
|
stripTemperature();
|
|
53
70
|
if (warnOnce) warnOnce();
|
|
54
71
|
return { data: await request(), temperatureDropped: true };
|
|
@@ -57,4 +74,4 @@ async function requestWithTemperatureFallback({ request, hadTemperature, stripTe
|
|
|
57
74
|
}
|
|
58
75
|
}
|
|
59
76
|
|
|
60
|
-
module.exports = { isTemperatureUnsupported, requestWithTemperatureFallback };
|
|
77
|
+
module.exports = { isTemperatureUnsupported, isTemperatureThinkingConflict, requestWithTemperatureFallback };
|
package/src/providers.d.ts
CHANGED
|
@@ -5,4 +5,5 @@ import { OllamaProvider } from "./provider-ollama";
|
|
|
5
5
|
import { CLIPipeProvider } from "./provider-clipipe";
|
|
6
6
|
import { FallbackProvider } from "./provider-fallback";
|
|
7
7
|
import { JevProvider } from "./provider-jev";
|
|
8
|
-
|
|
8
|
+
import { thinkingSupport } from "./thinking-mode";
|
|
9
|
+
export { OpenAIProvider as OpenAI, AnthropicProvider as Anthropic, GeminiProvider as Gemini, OllamaProvider as Ollama, CLIPipeProvider as CLIPipe, FallbackProvider as Fallback, JevProvider as Jev, OpenAIProvider, AnthropicProvider, GeminiProvider, OllamaProvider, CLIPipeProvider, FallbackProvider, JevProvider, thinkingSupport };
|
package/src/providers.js
CHANGED
|
@@ -7,6 +7,7 @@ const { OllamaProvider } = require('./provider-ollama');
|
|
|
7
7
|
const { CLIPipeProvider } = require('./provider-clipipe');
|
|
8
8
|
const { FallbackProvider } = require('./provider-fallback');
|
|
9
9
|
const { JevProvider } = require('./provider-jev');
|
|
10
|
+
const { thinkingSupport } = require('./thinking-mode');
|
|
10
11
|
|
|
11
12
|
module.exports = {
|
|
12
13
|
// Short names (canonical — used throughout docs and the integration guide)
|
|
@@ -26,4 +27,6 @@ module.exports = {
|
|
|
26
27
|
CLIPipeProvider,
|
|
27
28
|
FallbackProvider,
|
|
28
29
|
JevProvider,
|
|
30
|
+
// Pure lookup (no network): what bare-agent knows about switching a model's thinking on/off.
|
|
31
|
+
thinkingSupport,
|
|
29
32
|
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
export type LadderKind = "anthropic" | "openai" | "deepseek" | "gemini" | "ollama";
|
|
2
|
+
export type Shape = {
|
|
3
|
+
/**
|
|
4
|
+
* - Short stable key (the MODELS table refers to shapes by this).
|
|
5
|
+
*/
|
|
6
|
+
key: string;
|
|
7
|
+
/**
|
|
8
|
+
* - Human-readable request-field description (error messages, thinkingSupport).
|
|
9
|
+
*/
|
|
10
|
+
id: string;
|
|
11
|
+
/**
|
|
12
|
+
* - Returns a NEW body with
|
|
13
|
+
* the field set (copy-on-write; never mutates `body`), or `null` when the shape cannot apply to this body.
|
|
14
|
+
*/
|
|
15
|
+
apply: (body: Record<string, any>) => (Record<string, any> | null);
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* What bare-agent knows about switching a model's thinking on/off, with no network. Measured models
|
|
19
|
+
* answer true/false; a model not in the measured table on a provider with a shape ladder answers
|
|
20
|
+
* 'unverified' (the ladder will try the shapes at runtime and fail loudly if all are rejected);
|
|
21
|
+
* clipipe and unknown providers answer false.
|
|
22
|
+
* @param {'anthropic'|'openai'|'deepseek'|'gemini'|'ollama'|'clipipe'|string} provider
|
|
23
|
+
* @param {string} [model]
|
|
24
|
+
* @returns {{ switchable: boolean|'unverified', off: string|null, on: string|null, note: string }}
|
|
25
|
+
* @when you need to know, before building a request, whether a provider/model can have thinking turned on or off and which request field does it — pure lookup, no network
|
|
26
|
+
* @fails returns switchable:false for clipipe and unknown providers (never throws); 'unverified' means not measured, not unsupported; Ollama's shapes are not live-verified.
|
|
27
|
+
* @category providers
|
|
28
|
+
* @example
|
|
29
|
+
* thinkingSupport('anthropic', 'claude-opus-5-5'); // { switchable: false, off: null, on: "thinking:{type:'adaptive'}", note: '...' }
|
|
30
|
+
*/
|
|
31
|
+
export function thinkingSupport(provider: "anthropic" | "openai" | "deepseek" | "gemini" | "ollama" | "clipipe" | string, model?: string): {
|
|
32
|
+
switchable: boolean | "unverified";
|
|
33
|
+
off: string | null;
|
|
34
|
+
on: string | null;
|
|
35
|
+
note: string;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Validate a `thinkingMode` value. `undefined`/`null` ⇒ `null` (unset); anything but 'on'/'off' throws.
|
|
39
|
+
* @param {any} value
|
|
40
|
+
* @param {string} label - provider name for the message
|
|
41
|
+
* @returns {'on'|'off'|null}
|
|
42
|
+
*/
|
|
43
|
+
export function normalizeThinkingMode(value: any, label: string): "on" | "off" | null;
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the EFFECTIVE mode for one call and reject the raw-`thinking` conflict (no silent precedence).
|
|
46
|
+
* Per-call overrides the instance default; `null` suppresses it.
|
|
47
|
+
* @param {object} p
|
|
48
|
+
* @param {string} p.label
|
|
49
|
+
* @param {any} p.instanceMode
|
|
50
|
+
* @param {Record<string, any>} p.options - per-call options
|
|
51
|
+
* @param {any} [p.instanceThinking] - the instance's raw `thinking` (providers that have one)
|
|
52
|
+
* @param {boolean} [p.hasRawThinking] - whether this provider has a raw `thinking` option at all
|
|
53
|
+
* @returns {'on'|'off'|null}
|
|
54
|
+
*/
|
|
55
|
+
export function resolveThinkingMode({ label, instanceMode, options, instanceThinking, hasRawThinking }: {
|
|
56
|
+
label: string;
|
|
57
|
+
instanceMode: any;
|
|
58
|
+
options: Record<string, any>;
|
|
59
|
+
instanceThinking?: any;
|
|
60
|
+
hasRawThinking?: boolean | undefined;
|
|
61
|
+
}): "on" | "off" | null;
|
|
62
|
+
/**
|
|
63
|
+
* Send a request with the `thinkingMode` field applied, walking the shape ladder on a 400 that names
|
|
64
|
+
* the field. Order: the instance cache (a shape that already worked for this mode+model), else the
|
|
65
|
+
* table's known-good shape, else the ladder's first; then the remaining shapes in ladder order. A
|
|
66
|
+
* known-not-switchable model throws BEFORE any request. Every shape rejected ⇒ a loud ValidationError.
|
|
67
|
+
* A 400 that does not name the field (or any non-400) is an ordinary error and propagates untouched.
|
|
68
|
+
*
|
|
69
|
+
* @param {object} p
|
|
70
|
+
* @param {LadderKind} p.kind
|
|
71
|
+
* @param {'on'|'off'} p.mode
|
|
72
|
+
* @param {string} p.model
|
|
73
|
+
* @param {string} p.label - provider class name
|
|
74
|
+
* @param {Record<string, any>} p.body - the base body (NOT mutated; each attempt gets a patched copy)
|
|
75
|
+
* @param {(body: Record<string, any>) => Promise<any>} p.request
|
|
76
|
+
* @param {Map<string, string>} p.cache - per provider instance
|
|
77
|
+
* @returns {Promise<any>} the reply data of the accepted shape
|
|
78
|
+
*/
|
|
79
|
+
export function requestWithThinkingMode({ kind, mode, model, label, body, request, cache }: {
|
|
80
|
+
kind: LadderKind;
|
|
81
|
+
mode: "on" | "off";
|
|
82
|
+
model: string;
|
|
83
|
+
label: string;
|
|
84
|
+
body: Record<string, any>;
|
|
85
|
+
request: (body: Record<string, any>) => Promise<any>;
|
|
86
|
+
cache: Map<string, string>;
|
|
87
|
+
}): Promise<any>;
|
|
88
|
+
/**
|
|
89
|
+
* The `thinkingApplied` verdict for a reply whose request was accepted. 'on' ⇒ `true` (an adaptive
|
|
90
|
+
* model may legitimately choose not to think, so 'on' is never flagged). 'off' ⇒ `true` when the reply
|
|
91
|
+
* shows zero thinking, `false` when thinking happened anyway, `null` when the signal is unreadable.
|
|
92
|
+
* On `false` it warns ONCE per `owner` (the provider instance) and never throws — the round is paid.
|
|
93
|
+
* @param {object} p
|
|
94
|
+
* @param {LadderKind} p.kind
|
|
95
|
+
* @param {'on'|'off'} p.mode
|
|
96
|
+
* @param {any} p.data
|
|
97
|
+
* @param {any} p.owner - provider instance (holds the warn-once flag)
|
|
98
|
+
* @param {string} p.label
|
|
99
|
+
* @param {string} p.model
|
|
100
|
+
* @returns {boolean|null}
|
|
101
|
+
*/
|
|
102
|
+
export function assessThinkingApplied({ kind, mode, data, owner, label, model }: {
|
|
103
|
+
kind: LadderKind;
|
|
104
|
+
mode: "on" | "off";
|
|
105
|
+
data: any;
|
|
106
|
+
owner: any;
|
|
107
|
+
label: string;
|
|
108
|
+
model: string;
|
|
109
|
+
}): boolean | null;
|
|
110
|
+
/**
|
|
111
|
+
* Read the thinking signal off a raw reply: 'thinking' | 'none' | 'unknown'. "Absent field ⇒ none" is
|
|
112
|
+
* used ONLY where measured: Gemini thoughtsTokenCount (given usageMetadata is present), DeepSeek
|
|
113
|
+
* reasoning_content, Ollama message.thinking. Anthropic counts thinking/redacted_thinking blocks.
|
|
114
|
+
* OpenAI reasoning_tokens absent ⇒ 'unknown'.
|
|
115
|
+
* @param {LadderKind} kind
|
|
116
|
+
* @param {any} data - raw provider reply
|
|
117
|
+
* @returns {'thinking'|'none'|'unknown'}
|
|
118
|
+
*/
|
|
119
|
+
export function readThinkingSignal(kind: LadderKind, data: any): "thinking" | "none" | "unknown";
|
|
120
|
+
/**
|
|
121
|
+
* @param {LadderKind} kind
|
|
122
|
+
* @param {any} err
|
|
123
|
+
* @returns {boolean}
|
|
124
|
+
*/
|
|
125
|
+
export function isShapeRejection(kind: LadderKind, err: any): boolean;
|
|
126
|
+
/**
|
|
127
|
+
* @param {any} model
|
|
128
|
+
* @param {any} baseUrl
|
|
129
|
+
* @returns {boolean}
|
|
130
|
+
*/
|
|
131
|
+
export function isDeepSeek(model: any, baseUrl: any): boolean;
|
|
132
|
+
/**
|
|
133
|
+
* Longest-prefix lookup of a model's measured facts. A dated/`-latest` suffix of a listed id counts
|
|
134
|
+
* (`claude-haiku-4-5-20251001`); any other suffix does NOT (so `gpt-5-chat` never inherits `gpt-5`).
|
|
135
|
+
* @param {LadderKind} kind
|
|
136
|
+
* @param {string} model
|
|
137
|
+
* @returns {{ off: string|null, on: string|null }|null}
|
|
138
|
+
*/
|
|
139
|
+
export function lookupModel(kind: LadderKind, model: string): {
|
|
140
|
+
off: string | null;
|
|
141
|
+
on: string | null;
|
|
142
|
+
} | null;
|