bare-agent 0.48.0 → 0.49.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 +11 -3
- package/examples/with-bareguard.mjs +7 -4
- package/package.json +2 -2
- package/src/provider-anthropic.d.ts +4 -0
- package/src/provider-anthropic.js +2 -0
- package/src/provider-fallback.d.ts +5 -0
- package/src/provider-fallback.js +5 -0
- package/src/provider-openai.d.ts +25 -1
- package/src/provider-openai.js +19 -1
package/bareagent.context.md
CHANGED
|
@@ -448,7 +448,7 @@ const { createShellTools } = require('bare-agent/tools');
|
|
|
448
448
|
const gate = new Gate({
|
|
449
449
|
budget: { maxCostUsd: 0.50 },
|
|
450
450
|
limits: { maxTurns: 20 },
|
|
451
|
-
fs: { readScope: ['/
|
|
451
|
+
fs: { readScope: ['/srv/agent', '~/Projects'], deny: ['/etc'] }, // list REAL paths (bareguard >=0.19.2 throws on a symlinked scope root; on macOS /tmp is one — use fs.realpathSync(os.tmpdir()))
|
|
452
452
|
bash: { allow: ['ls', 'cat', 'grep', 'ps', 'df'] }, // argv[0] allowlist
|
|
453
453
|
audit: { path: './audit.jsonl' },
|
|
454
454
|
humanChannel: async (event) => ({ decision: 'deny' }), // wire to your UI
|
|
@@ -505,6 +505,14 @@ The blocks now ride the transcript on `Message.providerBlocks` (`{provider, mode
|
|
|
505
505
|
|
|
506
506
|
`AnthropicProvider({ thinking })` is a separate, opt-in knob, forwarded to `body.thinking` verbatim (e.g. `{type:'adaptive', display:'summarized'}` to surface the reasoning; the default `display` is `'omitted'`). **It does not "turn thinking on"** — it's already on. Use it to pin the mode or reach `display`/`effort`. It is passed through unvalidated on purpose: `budget_tokens` was removed from the API and now **400s** on sonnet-5/Opus 4.7+, so a library that reshaped this parameter would need a release every time Anthropic moved.
|
|
507
507
|
|
|
508
|
+
`OpenAIProvider({ thinking })` is the OpenAI-compatible counterpart: same verbatim/unvalidated forwarding to `body.thinking`, overridable per call (`loop.run(msgs, tools, { thinking })` / `generate(..., { thinking })`), and per-call `null` suppresses the constructor default. Needed for DeepSeek, which runs thinking mode by default and 400s "Thinking mode does not support this tool_choice" on a forced `toolChoice`:
|
|
509
|
+
|
|
510
|
+
```js
|
|
511
|
+
new OpenAIProvider({ baseUrl: 'https://api.deepseek.com', model: 'deepseek-flash', legacyMaxTokens: true, thinking: { type: 'disabled' } })
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
> **Fallback caveat:** per-call options (incl. `thinking`) are forwarded to EVERY provider in a `FallbackProvider` chain. For a mixed-vendor chain set `thinking` on each provider's constructor, not per-call.
|
|
515
|
+
|
|
508
516
|
> **Honesty note, and we mean it.** This is a **protocol/data-loss fix, not a capability fix.** A head-to-head with thinking fully preserved vs. stock bare-agent produced **indistinguishable** outcomes. Do not adopt 0.27 expecting better reasoning — you will not get it, and we have the measurement.
|
|
509
517
|
|
|
510
518
|
Legacy `wrapTool` / `wrapTools` are retained as deprecation shims (one-shot console warning, removal in 1.0). Migration: replace `wrapTools(tools)` at `loop.run()` with `filterTools(tools)` once upfront + `onLlmResult` / `onToolResult` on `new Loop({...})` to pick up LLM-cost recording and `_ctx` threading.
|
|
@@ -1427,7 +1435,7 @@ Mobile tools follow the observe-act pattern: action tools auto-return a fresh sn
|
|
|
1427
1435
|
|
|
1428
1436
|
**Zero baked-in allowlist.** The library ships the primitives; gating is bareguard's job via the standard `wireGate(gate)` wiring.
|
|
1429
1437
|
|
|
1430
|
-
> **Path canonicalization + symlink refusal, together.** bareguard
|
|
1438
|
+
> **Path canonicalization + symlink refusal, together.** bareguard >=0.19.0 already realpath-resolves the checked path and every scope root on each call, so a symlink under an allowed scope that resolves outside it is denied (`fs.<scope>.symlinkEscape`; dangling → `danglingSymlink`). bareguard 0.19.2 additionally requires the scope ROOTS themselves not be, or sit under, a symlink: `new Gate` throws otherwise, and a root swapped for a symlink later denies `fs.readScope.symlinkRoot` / `fs.writeScope.symlinkRoot` — list real paths (e.g. `fs.realpathSync(os.tmpdir())`; on macOS `/tmp` and `os.tmpdir()` are symlinks). The checked path must ALSO pass a lexical check (`within(path, scopeRoot)`) before the resolved one, so its lexical form must lie under a realpath'd scope root: with a symlinked `$HOME`, `~/x` expands (via `resolveToolPath`, which does not realpath) to `<symlink>/x` and is DENIED against a realpath'd home scope — fail-closed, a usability gap not a leak; pass the real path. bare-agent canonicalizes the path (`~` + resolve, via `resolveToolPath`) before the gate check and opens that same path — this closes a *different* gap (the gate judging a `~`/relative path in a different form than the one the tool opens), not the symlink-containment gap, which is bareguard's job. `noFollowSymlinks` refuses a symlinked final path *component* at open time. Together they narrow but do not close the check-then-open window, and neither covers hardlinks.
|
|
1431
1439
|
>
|
|
1432
1440
|
> **Opt-in symlink refusal:** `createShellTools({ noFollowSymlinks: true })` makes `shell_read`, `shell_write`, `shell_edit`, and `shell_grep` refuse to open a path whose **final** path component is a symlink — a file, a dir, or dangling — instead of silently following it. Refusal is a **thrown** error with `err.code === 'ELOOP'` (feeds the Loop's `maxIdenticalToolErrors` guard like any other tool error), and for `shell_write` the link's target is never created or touched. Scope is the final component **only** — a symlinked *parent* directory is not covered (per the canonicalization note above), and neither is the `cwd` option of `shell_run`/`shell_exec` — it only gets `~` expansion, never this flag or `resolveToolPath`'s full canonicalization, so a symlinked `cwd` is always followed. `shell_grep` refuses only a symlinked **root** path loudly, via the same atomic open every other tool uses (open-then-fstat, not a look-then-open `lstat`); a file that turns into a symlink partway through the walk is skipped silently, the same as any other read error. The check is atomic (`O_NOFOLLOW`) on linux/macOS; on Windows, which has no `O_NOFOLLOW`-equivalent open flag, it falls back to a non-atomic `lstat`-then-open check — a symlink planted in that gap slips through. A directory listing (`shell_read` on a dir, `shell_grep`'s root when it's a dir) additionally rechecks the path's `dev`+`ino` against the open handle right after listing — Node has no `fdopendir`, so the listing itself still goes by path, and this narrows (does not close) the window where a directory swapped in between the open and the listing would report a different directory's names; a swap-and-swap-back inside that narrow window still passes undetected. Default `false` — omitting the option is byte-identical to the pre-existing behavior.
|
|
1433
1441
|
>
|
|
@@ -1451,7 +1459,7 @@ const gate = new Gate({
|
|
|
1451
1459
|
// Hard-deny shell_exec for this agent. tools.denylist short-circuits before content checks.
|
|
1452
1460
|
tools: { denylist: ['shell_exec'] },
|
|
1453
1461
|
// fs scope for shell_read / shell_grep.
|
|
1454
|
-
fs: { readScope: ['/home/', '/
|
|
1462
|
+
fs: { readScope: ['/home/', '/srv/agent/'] }, // real paths only — see the symlink note above
|
|
1455
1463
|
audit: { path: './shell-audit.jsonl' },
|
|
1456
1464
|
humanChannel: async (event) => ({ decision: 'deny' }),
|
|
1457
1465
|
});
|
|
@@ -31,17 +31,20 @@ const { createShellTools } = require('bare-agent/tools');
|
|
|
31
31
|
// floor), a shell file action denies by default when its scope is unset (fs.readScope.unset /
|
|
32
32
|
// fs.writeScope.unset) — leaving writeScope out here would silently deny every shell_write /
|
|
33
33
|
// shell_edit call, not leave the primitive dead like on older bareguard.
|
|
34
|
-
//
|
|
34
|
+
// Scope roots are realpath'd: bareguard >=0.19.2 throws at `new Gate` on a root that is or
|
|
35
|
+
// contains a symlink (macOS /tmp and os.tmpdir() are symlinks), so list real paths.
|
|
36
|
+
// readScope is deliberately WIDER than writeScope here — read is fine anywhere under the tmp root,
|
|
35
37
|
// but writes are narrowed to one demo subdir, to show the two scopes are independent (a
|
|
36
38
|
// folder listed only in readScope is NOT writable, and vice versa). The demo dir is created
|
|
37
39
|
// below since fs.writeScope doesn't need the path to pre-exist, but the example's own
|
|
38
40
|
// shell_write call does.
|
|
39
|
-
const
|
|
41
|
+
const TMP = fs.realpathSync(os.tmpdir());
|
|
42
|
+
const writeDir = path.join(TMP, 'bare-agent-demo');
|
|
40
43
|
fs.mkdirSync(writeDir, { recursive: true });
|
|
41
44
|
const gate = new Gate({
|
|
42
45
|
budget: { maxCostUsd: 0.10 }, // hard USD cap
|
|
43
46
|
limits: { maxTurns: 20 }, // safety net on think/act cycles
|
|
44
|
-
fs: { readScope: [
|
|
47
|
+
fs: { readScope: [TMP], writeScope: [writeDir] }, // read: anywhere under the tmp root; write: one narrower demo dir
|
|
45
48
|
bash: { allow: ['ls', 'cat', 'echo', 'pwd'] }, // argv[0] allowlist for shell_run
|
|
46
49
|
audit: { path: './bareagent-audit.jsonl' },
|
|
47
50
|
// Required by bareguard: any ask/halt event flows through here.
|
|
@@ -87,7 +90,7 @@ const loop = new Loop({
|
|
|
87
90
|
|
|
88
91
|
// 4. Run. Pass the tools as-is — gating is via policy/onToolResult, not by wrapping execute().
|
|
89
92
|
const result = await loop.run(
|
|
90
|
-
[{ role: 'user', content:
|
|
93
|
+
[{ role: 'user', content: `List the contents of ${TMP} using shell_run with argv ["ls", "${TMP}"].` }],
|
|
91
94
|
tools,
|
|
92
95
|
);
|
|
93
96
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bare-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"files": [
|
|
5
5
|
"index.js",
|
|
6
6
|
"index.d.ts",
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
},
|
|
108
108
|
"devDependencies": {
|
|
109
109
|
"@types/node": "^22.19.19",
|
|
110
|
-
"bareguard": ">=0.19.
|
|
110
|
+
"bareguard": ">=0.19.2 <1.0.0",
|
|
111
111
|
"litectx": "^0.26.0",
|
|
112
112
|
"typescript": "^5.7.0"
|
|
113
113
|
}
|
|
@@ -24,6 +24,8 @@ export type AnthropicOptions = {
|
|
|
24
24
|
cacheMessages?: boolean | undefined;
|
|
25
25
|
/**
|
|
26
26
|
* - Opt-in extended thinking (BA-7), forwarded to `body.thinking` VERBATIM and unvalidated — e.g. `{ type: 'adaptive' }`, or `{ type: 'adaptive', display: 'summarized' }` to surface the reasoning (the default `display` is `'omitted'`). Deliberately opaque: this parameter has already broken once (`budget_tokens` was removed and now 400s on `claude-sonnet-5` / Opus 4.7+), and a library that reshapes it would need a release every time the API moves. Overridable per call via `generate(..., { thinking })`; pass `null` there to suppress an instance default.
|
|
27
|
+
* WARNING: per-call options (incl. `thinking`) are forwarded to EVERY provider in a `FallbackProvider` chain — for a mixed-vendor
|
|
28
|
+
* chain set `thinking` on each provider's CONSTRUCTOR instead of per-call.
|
|
27
29
|
*
|
|
28
30
|
* **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
31
|
*/
|
|
@@ -52,6 +54,8 @@ export type AnthropicOptions = {
|
|
|
52
54
|
* @property {boolean} [cacheSystem=false] - Opt-in prompt caching: send the system prompt with a `cache_control` breakpoint so Anthropic caches it. Anthropic does NOT auto-cache, so without this its cache tiers are always 0. Overridable per call via `generate(..., { cacheSystem })`. NOTE: on its own this rarely helps a tool loop — Anthropic's minimum cacheable prefix is 1024–4096 tokens (model-dependent) and a typical system persona is a few hundred, so it silently never caches. The transcript is where a tool loop's tokens actually live — see `cacheMessages`.
|
|
53
55
|
* @property {boolean} [cacheMessages=false] - Opt-in TRANSCRIPT caching (BA-1): roll a `cache_control` breakpoint onto the last content block of the last message, so Anthropic caches the whole conversation prefix and the loop stops re-buying it at full price every round. In a tool loop the transcript IS the tool results (file bodies from `shell_read`) and it always ENDS on one, which `_toAnthropicMessage` rebuilds from scratch — so no caller-side seam (`assemble` included) can reach it, and this has to live in the provider. Measured on `claude-sonnet-5` with a ~15k-token tool-result transcript (`poc/ba1-message-caching.mjs`): steady state **$0.0753 → $0.0110 per round, 6.8x cheaper**; round 1 pays a 1.25x cache WRITE once. Off by default — it changes the wire format, so adopters opt in. Overridable per call via `generate(..., { cacheMessages })`. **Interaction:** a destructive `trim`/stash fold that rewrites the transcript PREFIX invalidates the cache (the prefix is the cache key), so a fold must keep the head stable or you re-pay the write premium every round for nothing.
|
|
54
56
|
* @property {any} [thinking] - Opt-in extended thinking (BA-7), forwarded to `body.thinking` VERBATIM and unvalidated — e.g. `{ type: 'adaptive' }`, or `{ type: 'adaptive', display: 'summarized' }` to surface the reasoning (the default `display` is `'omitted'`). Deliberately opaque: this parameter has already broken once (`budget_tokens` was removed and now 400s on `claude-sonnet-5` / Opus 4.7+), and a library that reshapes it would need a release every time the API moves. Overridable per call via `generate(..., { thinking })`; pass `null` there to suppress an instance default.
|
|
57
|
+
* WARNING: per-call options (incl. `thinking`) are forwarded to EVERY provider in a `FallbackProvider` chain — for a mixed-vendor
|
|
58
|
+
* chain set `thinking` on each provider's CONSTRUCTOR instead of per-call.
|
|
55
59
|
*
|
|
56
60
|
* **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.
|
|
57
61
|
* @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).
|
|
@@ -30,6 +30,8 @@ function isLoopbackHost(hostname) {
|
|
|
30
30
|
* @property {boolean} [cacheSystem=false] - Opt-in prompt caching: send the system prompt with a `cache_control` breakpoint so Anthropic caches it. Anthropic does NOT auto-cache, so without this its cache tiers are always 0. Overridable per call via `generate(..., { cacheSystem })`. NOTE: on its own this rarely helps a tool loop — Anthropic's minimum cacheable prefix is 1024–4096 tokens (model-dependent) and a typical system persona is a few hundred, so it silently never caches. The transcript is where a tool loop's tokens actually live — see `cacheMessages`.
|
|
31
31
|
* @property {boolean} [cacheMessages=false] - Opt-in TRANSCRIPT caching (BA-1): roll a `cache_control` breakpoint onto the last content block of the last message, so Anthropic caches the whole conversation prefix and the loop stops re-buying it at full price every round. In a tool loop the transcript IS the tool results (file bodies from `shell_read`) and it always ENDS on one, which `_toAnthropicMessage` rebuilds from scratch — so no caller-side seam (`assemble` included) can reach it, and this has to live in the provider. Measured on `claude-sonnet-5` with a ~15k-token tool-result transcript (`poc/ba1-message-caching.mjs`): steady state **$0.0753 → $0.0110 per round, 6.8x cheaper**; round 1 pays a 1.25x cache WRITE once. Off by default — it changes the wire format, so adopters opt in. Overridable per call via `generate(..., { cacheMessages })`. **Interaction:** a destructive `trim`/stash fold that rewrites the transcript PREFIX invalidates the cache (the prefix is the cache key), so a fold must keep the head stable or you re-pay the write premium every round for nothing.
|
|
32
32
|
* @property {any} [thinking] - Opt-in extended thinking (BA-7), forwarded to `body.thinking` VERBATIM and unvalidated — e.g. `{ type: 'adaptive' }`, or `{ type: 'adaptive', display: 'summarized' }` to surface the reasoning (the default `display` is `'omitted'`). Deliberately opaque: this parameter has already broken once (`budget_tokens` was removed and now 400s on `claude-sonnet-5` / Opus 4.7+), and a library that reshapes it would need a release every time the API moves. Overridable per call via `generate(..., { thinking })`; pass `null` there to suppress an instance default.
|
|
33
|
+
* WARNING: per-call options (incl. `thinking`) are forwarded to EVERY provider in a `FallbackProvider` chain — for a mixed-vendor
|
|
34
|
+
* chain set `thinking` on each provider's CONSTRUCTOR instead of per-call.
|
|
33
35
|
*
|
|
34
36
|
* **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.
|
|
35
37
|
* @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).
|
|
@@ -21,6 +21,11 @@ export type FallbackOptions = {
|
|
|
21
21
|
* @property {(error: any, index: number) => boolean} [shouldFallback] - Return false to stop.
|
|
22
22
|
* @property {(error: any, fromIndex: number, toIndex: number) => void} [onFallback] - Callback.
|
|
23
23
|
*/
|
|
24
|
+
/**
|
|
25
|
+
* NOTE: per-call `generate()` options (incl. `thinking`, `toolChoice`) are forwarded UNCHANGED to EVERY provider in the chain. For a
|
|
26
|
+
* mixed-vendor chain (e.g. OpenAI-compat DeepSeek + Anthropic) set `thinking` on each provider's CONSTRUCTOR instead of per-call,
|
|
27
|
+
* or a value shaped for one vendor lands in another's `body.thinking` on fallback.
|
|
28
|
+
*/
|
|
24
29
|
export class FallbackProvider {
|
|
25
30
|
/**
|
|
26
31
|
* Provider that tries multiple providers in order.
|
package/src/provider-fallback.js
CHANGED
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
* @property {(error: any, fromIndex: number, toIndex: number) => void} [onFallback] - Callback.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* NOTE: per-call `generate()` options (incl. `thinking`, `toolChoice`) are forwarded UNCHANGED to EVERY provider in the chain. For a
|
|
16
|
+
* mixed-vendor chain (e.g. OpenAI-compat DeepSeek + Anthropic) set `thinking` on each provider's CONSTRUCTOR instead of per-call,
|
|
17
|
+
* or a value shaped for one vendor lands in another's `body.thinking` on fallback.
|
|
18
|
+
*/
|
|
14
19
|
class FallbackProvider {
|
|
15
20
|
/**
|
|
16
21
|
* Provider that tries multiple providers in order.
|
package/src/provider-openai.d.ts
CHANGED
|
@@ -46,6 +46,19 @@ export type OpenAIOptions = {
|
|
|
46
46
|
* unexpected string into logs by default" reasoning applies. See `src/provider-toolcalls.js`.
|
|
47
47
|
*/
|
|
48
48
|
exposeMalformedArgs?: boolean | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* - Opt-in `thinking` request field (fwdloop DeepSeek ask — extends BA-7(b)'s Anthropic option), forwarded to `body.thinking`
|
|
51
|
+
* VERBATIM and unvalidated — e.g. `{ type: 'disabled' }`. Generalises the Anthropic provider's option to
|
|
52
|
+
* OpenAI-compatible endpoints. Motivating case: DeepSeek's OpenAI-compatible API (`baseUrl`
|
|
53
|
+
* `https://api.deepseek.com`, model `deepseek-flash`) runs thinking mode by default and 400s "Thinking mode
|
|
54
|
+
* does not support this tool_choice" on a named or `'required'` `toolChoice`; `{ type: 'disabled' }` makes the
|
|
55
|
+
* forced tool call return 200. Deliberately opaque (no model sniffing, no free-form extra-body field) — a
|
|
56
|
+
* library that reshapes it would need a release every time a vendor moves. Unset ⇒ request body unchanged.
|
|
57
|
+
* Overridable per call via `generate(..., { thinking })`; pass `null` there to suppress an instance default.
|
|
58
|
+
* WARNING: per-call options (incl. `thinking`) are forwarded to EVERY provider in a `FallbackProvider` chain — for a mixed-vendor
|
|
59
|
+
* chain set `thinking` on each provider's CONSTRUCTOR instead of per-call.
|
|
60
|
+
*/
|
|
61
|
+
thinking?: any;
|
|
49
62
|
};
|
|
50
63
|
/**
|
|
51
64
|
* @typedef {object} OpenAIOptions
|
|
@@ -77,6 +90,16 @@ export type OpenAIOptions = {
|
|
|
77
90
|
* when longer) to `malformedToolCall.rawArguments`. Off by default, independent of `exposeErrorBody` —
|
|
78
91
|
* the raw blob is model-generated, not an upstream error body, but the same "don't leak an unbounded
|
|
79
92
|
* unexpected string into logs by default" reasoning applies. See `src/provider-toolcalls.js`.
|
|
93
|
+
* @property {any} [thinking] - Opt-in `thinking` request field (fwdloop DeepSeek ask — extends BA-7(b)'s Anthropic option), forwarded to `body.thinking`
|
|
94
|
+
* VERBATIM and unvalidated — e.g. `{ type: 'disabled' }`. Generalises the Anthropic provider's option to
|
|
95
|
+
* OpenAI-compatible endpoints. Motivating case: DeepSeek's OpenAI-compatible API (`baseUrl`
|
|
96
|
+
* `https://api.deepseek.com`, model `deepseek-flash`) runs thinking mode by default and 400s "Thinking mode
|
|
97
|
+
* does not support this tool_choice" on a named or `'required'` `toolChoice`; `{ type: 'disabled' }` makes the
|
|
98
|
+
* forced tool call return 200. Deliberately opaque (no model sniffing, no free-form extra-body field) — a
|
|
99
|
+
* library that reshapes it would need a release every time a vendor moves. Unset ⇒ request body unchanged.
|
|
100
|
+
* Overridable per call via `generate(..., { thinking })`; pass `null` there to suppress an instance default.
|
|
101
|
+
* WARNING: per-call options (incl. `thinking`) are forwarded to EVERY provider in a `FallbackProvider` chain — for a mixed-vendor
|
|
102
|
+
* chain set `thinking` on each provider's CONSTRUCTOR instead of per-call.
|
|
80
103
|
*/
|
|
81
104
|
export class OpenAIProvider {
|
|
82
105
|
/**
|
|
@@ -95,11 +118,12 @@ export class OpenAIProvider {
|
|
|
95
118
|
deadlineMs: number | undefined;
|
|
96
119
|
legacyMaxTokens: boolean;
|
|
97
120
|
exposeMalformedArgs: boolean;
|
|
121
|
+
thinking: any;
|
|
98
122
|
/**
|
|
99
123
|
* Generate a response from the OpenAI API.
|
|
100
124
|
* @param {Message[]} messages - Conversation messages.
|
|
101
125
|
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
102
|
-
* @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).
|
|
126
|
+
* @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)).
|
|
103
127
|
* @returns {Promise<GenerateResult>}
|
|
104
128
|
* @throws {Error} `[OpenAIProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response.
|
|
105
129
|
*/
|
package/src/provider-openai.js
CHANGED
|
@@ -77,6 +77,16 @@ function isLoopbackHost(hostname) {
|
|
|
77
77
|
* when longer) to `malformedToolCall.rawArguments`. Off by default, independent of `exposeErrorBody` —
|
|
78
78
|
* the raw blob is model-generated, not an upstream error body, but the same "don't leak an unbounded
|
|
79
79
|
* unexpected string into logs by default" reasoning applies. See `src/provider-toolcalls.js`.
|
|
80
|
+
* @property {any} [thinking] - Opt-in `thinking` request field (fwdloop DeepSeek ask — extends BA-7(b)'s Anthropic option), forwarded to `body.thinking`
|
|
81
|
+
* VERBATIM and unvalidated — e.g. `{ type: 'disabled' }`. Generalises the Anthropic provider's option to
|
|
82
|
+
* OpenAI-compatible endpoints. Motivating case: DeepSeek's OpenAI-compatible API (`baseUrl`
|
|
83
|
+
* `https://api.deepseek.com`, model `deepseek-flash`) runs thinking mode by default and 400s "Thinking mode
|
|
84
|
+
* does not support this tool_choice" on a named or `'required'` `toolChoice`; `{ type: 'disabled' }` makes the
|
|
85
|
+
* forced tool call return 200. Deliberately opaque (no model sniffing, no free-form extra-body field) — a
|
|
86
|
+
* library that reshapes it would need a release every time a vendor moves. Unset ⇒ request body unchanged.
|
|
87
|
+
* Overridable per call via `generate(..., { thinking })`; pass `null` there to suppress an instance default.
|
|
88
|
+
* WARNING: per-call options (incl. `thinking`) are forwarded to EVERY provider in a `FallbackProvider` chain — for a mixed-vendor
|
|
89
|
+
* chain set `thinking` on each provider's CONSTRUCTOR instead of per-call.
|
|
80
90
|
*/
|
|
81
91
|
|
|
82
92
|
class OpenAIProvider {
|
|
@@ -100,13 +110,16 @@ class OpenAIProvider {
|
|
|
100
110
|
this.legacyMaxTokens = options.legacyMaxTokens === true;
|
|
101
111
|
// BA-27 follow-up: expose the raw malformed tool-call arguments string. Off by default.
|
|
102
112
|
this.exposeMalformedArgs = options.exposeMalformedArgs === true;
|
|
113
|
+
// BA-7 (b) / fwdloop: forwarded to `body.thinking` VERBATIM — unvalidated, un-reshaped (mirrors the
|
|
114
|
+
// Anthropic provider). Null/unset ⇒ no `thinking` key, byte-identical to the pre-option body.
|
|
115
|
+
this.thinking = options.thinking != null ? options.thinking : null;
|
|
103
116
|
}
|
|
104
117
|
|
|
105
118
|
/**
|
|
106
119
|
* Generate a response from the OpenAI API.
|
|
107
120
|
* @param {Message[]} messages - Conversation messages.
|
|
108
121
|
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
109
|
-
* @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).
|
|
122
|
+
* @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)).
|
|
110
123
|
* @returns {Promise<GenerateResult>}
|
|
111
124
|
* @throws {Error} `[OpenAIProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response.
|
|
112
125
|
*/
|
|
@@ -121,6 +134,11 @@ class OpenAIProvider {
|
|
|
121
134
|
...(options.temperature != null && { temperature: options.temperature }),
|
|
122
135
|
...(options.maxTokens && { [maxTokensKey]: options.maxTokens }),
|
|
123
136
|
};
|
|
137
|
+
// BA-7 (b) / fwdloop: DeepSeek-style compat endpoints default to thinking mode and 400 on a forced
|
|
138
|
+
// tool_choice; the caller pins it (e.g. `{type:'disabled'}`). Set on `body` before the temperature
|
|
139
|
+
// fallback so the retry (which only deletes `temperature`) keeps it. Per-call `null` suppresses.
|
|
140
|
+
const thinking = options.thinking !== undefined ? options.thinking : this.thinking;
|
|
141
|
+
if (thinking) body.thinking = thinking;
|
|
124
142
|
// Ask 4 (fwdloop): validate the toolChoice SHAPE unconditionally so an invalid value ALWAYS throws
|
|
125
143
|
// (a silently-dropped force is the exact confusion this surfaces) — even when tools happen to be
|
|
126
144
|
// empty. Attach it only when tools are present: OpenAI 400s on a tool_choice with no tools, so a
|