@jxburros/ai-nugget 0.6.0 → 0.7.3
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 +92 -3
- package/UPGRADING.md +43 -1
- package/dist/adapters/engines/anthropic.js +98 -10
- package/dist/adapters/engines/google.js +8 -1
- package/dist/adapters/engines/ollama.js +6 -0
- package/dist/adapters/engines/openaiChat.d.ts +6 -0
- package/dist/adapters/engines/openaiChat.js +51 -1
- package/dist/agent/loop.d.ts +3 -1
- package/dist/agent/loop.js +4 -1
- package/dist/cjs/adapters/engines/anthropic.js +96 -8
- package/dist/cjs/adapters/engines/google.js +7 -0
- package/dist/cjs/adapters/engines/ollama.js +6 -0
- package/dist/cjs/adapters/engines/openaiChat.js +52 -1
- package/dist/cjs/agent/loop.js +4 -1
- package/dist/cjs/handler.js +28 -3
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/reasoning.js +159 -0
- package/dist/cjs/util.js +14 -0
- package/dist/handler.d.ts +6 -0
- package/dist/handler.js +28 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/reasoning.d.ts +45 -0
- package/dist/reasoning.js +154 -0
- package/dist/types.d.ts +25 -0
- package/dist/util.d.ts +7 -0
- package/dist/util.js +13 -0
- package/nugget/VERSION.txt +2 -2
- package/nugget/dist/adapters/engines/anthropic.d.ts.map +1 -1
- package/nugget/dist/adapters/engines/anthropic.js +98 -10
- package/nugget/dist/adapters/engines/anthropic.js.map +1 -1
- package/nugget/dist/adapters/engines/google.js +8 -1
- package/nugget/dist/adapters/engines/google.js.map +1 -1
- package/nugget/dist/adapters/engines/ollama.js +6 -0
- package/nugget/dist/adapters/engines/ollama.js.map +1 -1
- package/nugget/dist/adapters/engines/openaiChat.d.ts +6 -0
- package/nugget/dist/adapters/engines/openaiChat.d.ts.map +1 -1
- package/nugget/dist/adapters/engines/openaiChat.js +51 -1
- package/nugget/dist/adapters/engines/openaiChat.js.map +1 -1
- package/nugget/dist/agent/loop.d.ts +3 -1
- package/nugget/dist/agent/loop.d.ts.map +1 -1
- package/nugget/dist/agent/loop.js +4 -1
- package/nugget/dist/agent/loop.js.map +1 -1
- package/nugget/dist/cjs/adapters/engines/anthropic.js +96 -8
- package/nugget/dist/cjs/adapters/engines/google.js +7 -0
- package/nugget/dist/cjs/adapters/engines/ollama.js +6 -0
- package/nugget/dist/cjs/adapters/engines/openaiChat.js +52 -1
- package/nugget/dist/cjs/agent/loop.js +4 -1
- package/nugget/dist/cjs/handler.js +28 -3
- package/nugget/dist/cjs/index.js +1 -0
- package/nugget/dist/cjs/reasoning.js +159 -0
- package/nugget/dist/cjs/util.js +14 -0
- package/nugget/dist/handler.d.ts +6 -0
- package/nugget/dist/handler.d.ts.map +1 -1
- package/nugget/dist/handler.js +28 -3
- package/nugget/dist/handler.js.map +1 -1
- package/nugget/dist/index.d.ts +1 -0
- package/nugget/dist/index.d.ts.map +1 -1
- package/nugget/dist/index.js +1 -0
- package/nugget/dist/index.js.map +1 -1
- package/nugget/dist/reasoning.d.ts +45 -0
- package/nugget/dist/reasoning.d.ts.map +1 -0
- package/nugget/dist/reasoning.js +154 -0
- package/nugget/dist/reasoning.js.map +1 -0
- package/nugget/dist/types.d.ts +25 -0
- package/nugget/dist/types.d.ts.map +1 -1
- package/nugget/dist/types.js.map +1 -1
- package/nugget/dist/util.d.ts +7 -0
- package/nugget/dist/util.d.ts.map +1 -1
- package/nugget/dist/util.js +13 -0
- package/nugget/dist/util.js.map +1 -1
- package/nugget/src/adapters/engines/anthropic.ts +87 -10
- package/nugget/src/adapters/engines/google.ts +8 -1
- package/nugget/src/adapters/engines/ollama.ts +5 -0
- package/nugget/src/adapters/engines/openaiChat.ts +46 -1
- package/nugget/src/agent/loop.ts +7 -2
- package/nugget/src/handler.ts +30 -3
- package/nugget/src/index.ts +1 -0
- package/nugget/src/reasoning.ts +174 -0
- package/nugget/src/types.ts +27 -0
- package/nugget/src/util.ts +14 -0
- package/package.json +12 -5
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# AI Nugget
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<img src="assets/ai-nugget-logo.png" alt="AI Nugget official logo" width="180" />
|
|
4
|
+
|
|
5
|
+
**Version 0.7.3** · [What's changed in 0.7.x](#whats-changed-in-07x) · [Changelog](./CHANGELOG.md) · [Upgrading](./UPGRADING.md)
|
|
4
6
|
|
|
5
7
|
A small, zero-dependency, isomorphic TypeScript nugget for talking to AI model
|
|
6
8
|
providers through one pipeline:
|
|
@@ -24,6 +26,13 @@ Requires Node `^20.19.0` or `>=22.12.0` ([why](./docs/distribution.md#requiremen
|
|
|
24
26
|
npm install @jxburros/ai-nugget
|
|
25
27
|
```
|
|
26
28
|
|
|
29
|
+
> **0.7.1–0.7.3 are not on npm yet.** The registry's `latest` is still 0.7.0
|
|
30
|
+
> until the 0.7.3 Release run publishes, so the command above may install 0.7.0
|
|
31
|
+
> (without the 0.7.1 Anthropic fixes or the 0.7.2 `/agent` types fix). Until
|
|
32
|
+
> then, pin a commit from this repository
|
|
33
|
+
> (`npm install github:jxburros/AI-Nugget#<commit>`) or vendor `nugget/` — see
|
|
34
|
+
> [distribution paths](./docs/distribution.md#distribution-paths).
|
|
35
|
+
|
|
27
36
|
```ts
|
|
28
37
|
import { AIHandler, envKeySource } from '@jxburros/ai-nugget';
|
|
29
38
|
|
|
@@ -50,7 +59,9 @@ No key? Point at a local model instead — same code, no `keyRef`:
|
|
|
50
59
|
const conn = { id: 'local', provider: 'ollama', baseUrl: 'http://127.0.0.1:11434' };
|
|
51
60
|
```
|
|
52
61
|
|
|
53
|
-
CommonJS works too: `const { AIHandler } = require('@jxburros/ai-nugget')`.
|
|
62
|
+
CommonJS works too: `const { AIHandler } = require('@jxburros/ai-nugget')`. TypeScript
|
|
63
|
+
finds the root and `/agent` types under `node10` (classic `module: commonjs`)
|
|
64
|
+
and `bundler` resolution, and under `node16`/`nodenext` from ESM files.
|
|
54
65
|
|
|
55
66
|
For a running server, copy a starter from
|
|
56
67
|
[`examples/integrations/`](./examples/integrations/) — Express, Next.js,
|
|
@@ -108,12 +119,76 @@ new AIHandler({
|
|
|
108
119
|
- **Seams** — `KeySource` (env/literal/memory/chain + ref parsing), `Redactor`,
|
|
109
120
|
neutral `GovernancePolicy` (`blocklistPolicy`/`allowlistPolicy`/`composePolicies`),
|
|
110
121
|
`TelemetrySink`, and an optional `pricing` hook. `providerOptions` passes
|
|
111
|
-
provider-native fields through without waiting for a library release
|
|
122
|
+
provider-native fields through without waiting for a library release;
|
|
123
|
+
`reasoningEffort` is one word for "how hard should the model think", mapped
|
|
124
|
+
per provider ([docs](./docs/providers.md#reasoning-effort)).
|
|
112
125
|
- **Agent layer** (`@jxburros/ai-nugget/agent`) — `defineTool`, `runAgent()`
|
|
113
126
|
model↔tool loop over the full pipeline, streamed `AgentEvent`s, budgets with
|
|
114
127
|
honest `stopReason`s, an `ApprovalGate` for side-effecting tools, and
|
|
115
128
|
`native`/`promptJson`/`auto` tool modes.
|
|
116
129
|
|
|
130
|
+
## Anthropic compatibility
|
|
131
|
+
|
|
132
|
+
Claude Opus/Sonnet/Haiku 5.5 requests omit unsupported sampling parameters.
|
|
133
|
+
Modern Claude JSON schemas use `output_config.format`; requests without a schema
|
|
134
|
+
use a JSON instruction and emit `json_mode_downgraded`. Older models retain the
|
|
135
|
+
forced-tool fallback. Explicit forced-tool choices on models that reject them
|
|
136
|
+
fail with `invalid_request` rather than silently changing the choice.
|
|
137
|
+
|
|
138
|
+
Anthropic results include opaque `providerState`. Custom tool loops must copy it
|
|
139
|
+
onto the corresponding assistant `ChatMessage`; `runAgent` does this automatically.
|
|
140
|
+
Keep it unchanged and private: it contains signed thinking blocks needed for replay,
|
|
141
|
+
not display or telemetry data. Replay is limited to the same provider and model.
|
|
142
|
+
See [provider details](./docs/providers.md#anthropic-compatibility).
|
|
143
|
+
|
|
144
|
+
## What's changed in 0.7.x
|
|
145
|
+
|
|
146
|
+
### 0.7.3
|
|
147
|
+
|
|
148
|
+
**Release-pipeline fix.** Package contents are identical to 0.7.2; 0.7.3 exists
|
|
149
|
+
because the 0.7.1 and 0.7.2 Release runs failed before publishing (see the
|
|
150
|
+
[changelog](./CHANGELOG.md)).
|
|
151
|
+
|
|
152
|
+
### 0.7.2
|
|
153
|
+
|
|
154
|
+
**`/agent` types under classic `node10` resolution.** A `typesVersions` entry
|
|
155
|
+
lets TypeScript projects on `module: commonjs` (default `node10` resolution)
|
|
156
|
+
resolve `@jxburros/ai-nugget/agent`. Otherwise documentation only — notably the
|
|
157
|
+
[JX Runtime guidance](./docs/providers.md#pointing-at-a-jx-runtime-server) for
|
|
158
|
+
JX 1.5, including the `stream_options` opt-in for real streamed token counts.
|
|
159
|
+
No runtime behavior changed.
|
|
160
|
+
|
|
161
|
+
### 0.7.1
|
|
162
|
+
|
|
163
|
+
**Anthropic correctness for modern Claude models.** Unsupported sampling
|
|
164
|
+
parameters are omitted, JSON schemas use native structured output, and signed
|
|
165
|
+
thinking is preserved for tool-loop replay through the new opaque
|
|
166
|
+
`providerState` on results (`runAgent` copies it for you; custom tool loops must
|
|
167
|
+
copy it onto the assistant message). SSE API errors become typed failures.
|
|
168
|
+
Details in [Anthropic compatibility](#anthropic-compatibility),
|
|
169
|
+
[UPGRADING.md](./UPGRADING.md#070--071), and the [changelog](./CHANGELOG.md).
|
|
170
|
+
|
|
171
|
+
### 0.7.0
|
|
172
|
+
|
|
173
|
+
**0.7.0 is backward-compatible for ordinary consumers** — the first two changes are additive; the third changes what `delta` carries for reasoning models (see UPGRADING). Full detail in the
|
|
174
|
+
[changelog](./CHANGELOG.md) and [UPGRADING.md](./UPGRADING.md#06x--070).
|
|
175
|
+
|
|
176
|
+
- **`ChatRequest.reasoningEffort`** (`none` / `minimal` / `low` / `medium` /
|
|
177
|
+
`high`) — one vocabulary for reasoning effort, mapped onto OpenAI
|
|
178
|
+
`reasoning_effort`, Anthropic `thinking`, Google `thinkingConfig`, and Ollama
|
|
179
|
+
`think`; forwarded by the agent loop via `AgentOptions.reasoningEffort`.
|
|
180
|
+
Never sent unless you set it. See [Reasoning effort](./docs/providers.md#reasoning-effort).
|
|
181
|
+
- **OpenAI reasoning models can call tools again.** `/chat/completions` refuses
|
|
182
|
+
function tools unless `reasoning_effort` is `'none'`; the `openaiChat` engine
|
|
183
|
+
now retries that one request with it set and emits a
|
|
184
|
+
`reasoning_effort_disabled_for_tools` context event so the change is visible.
|
|
185
|
+
Learned from AI Server Studio's Build 3 shakedown, where every tool-using
|
|
186
|
+
turn on `gpt-5.x`/`gpt-6` 400'd.
|
|
187
|
+
- **Inline `<think>` reasoning never reaches `delta` or `result.text`.** The
|
|
188
|
+
handler strips it on every engine and emits it as `reasoning` events
|
|
189
|
+
(`stripInlineReasoning: false` opts out). Ported from the stripper AI Server
|
|
190
|
+
Studio had to run at every call site. See [Inline reasoning](./docs/providers.md#inline-reasoning-is-stripped-at-the-seam).
|
|
191
|
+
|
|
117
192
|
## What's changed since 0.5.0
|
|
118
193
|
|
|
119
194
|
**0.6.0 is backward-compatible** — every change below is additive. The one thing
|
|
@@ -177,3 +252,17 @@ seam), and not a UI. The governance seam ships neutral: it is where an app *can*
|
|
|
177
252
|
enforce rules, not a place the library imposes its own.
|
|
178
253
|
|
|
179
254
|
<!-- GitHub Pages deployment is configured in .github/workflows/pages.yml. -->
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Project Stewardship
|
|
259
|
+
|
|
260
|
+
This project is developed by **Jeffrey** through **JX Holdings, LLC**. GitHub: [`jxburros`](https://github.com/jxburros).
|
|
261
|
+
|
|
262
|
+
## Unified release procedure
|
|
263
|
+
|
|
264
|
+
`release.json` is the authoritative release manifest. Keep configured embedded versions synchronized and advance affected apps (patch by default) before merging changes into main. The first adoption establishes a baseline; later PRs must pass **Release version validation**.
|
|
265
|
+
|
|
266
|
+
Use **Actions → Release → Run workflow** from **main**, choose stable or preview, and leave **dry_run** enabled first. Only apps ahead of their published version in their own namespace are selected. Build/test/sign all required packages before publishing. Tag pushes and merges no longer publish migrated apps automatically. After inspecting the dry run, dispatch with dry_run disabled and satisfy configured approvals. Published versions are never overwritten; use a new version for a correction.
|
|
267
|
+
|
|
268
|
+
See [.release/README.md](.release/README.md) for this repository's inventory, credentials, compatibility, and validation limits. Shared implementation: [JX Holdings release infrastructure](https://github.com/JX-Holdings-LLC/release-infrastructure).
|
package/UPGRADING.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Upgrading
|
|
2
2
|
|
|
3
|
+
## 0.7.2 → 0.7.3
|
|
4
|
+
|
|
5
|
+
No changes: 0.7.3 republishes the 0.7.2 package contents after a release-pipeline fix.
|
|
6
|
+
|
|
7
|
+
## 0.7.1 → 0.7.2
|
|
8
|
+
|
|
9
|
+
No code changes required. `package.json` gains a `typesVersions` entry so
|
|
10
|
+
TypeScript on classic `node10` resolution can find the `/agent` types; consumers
|
|
11
|
+
on `node16`/`nodenext`/`bundler` resolve exactly as before.
|
|
12
|
+
|
|
13
|
+
## 0.7.0 → 0.7.1
|
|
14
|
+
|
|
15
|
+
Custom Anthropic tool loops should copy `result.providerState` onto their assistant
|
|
16
|
+
messages to preserve signed thinking; `runAgent` already does this. Sampling fields
|
|
17
|
+
are omitted on known modern Claude models. Native JSON schemas replace forced JSON
|
|
18
|
+
tools there; schema-less requests disclose an instruction-only downgrade. Explicit
|
|
19
|
+
unsupported forced-tool and thinking-off choices fail with `invalid_request`.
|
|
20
|
+
|
|
21
|
+
## 0.6.x → 0.7.0
|
|
22
|
+
|
|
23
|
+
**Backward-compatible for ordinary consumers.** Two additive changes and one
|
|
24
|
+
default-behaviour change worth knowing about:
|
|
25
|
+
|
|
26
|
+
- **Inline reasoning is stripped by default.** A model that writes
|
|
27
|
+
`<think>…</think>` (or `<thinking>`, `<reasoning>`, `<|begin_of_thought|>`)
|
|
28
|
+
into its answer no longer has that text in `delta` events or
|
|
29
|
+
`ChatResult.text`; it arrives as `{ type: 'reasoning', text }` events, the
|
|
30
|
+
same channel a provider-native thinking field already used. If your app
|
|
31
|
+
parsed or displayed those tags itself, either delete that code or pass
|
|
32
|
+
`new AIHandler({ stripInlineReasoning: false })` to keep the raw text.
|
|
33
|
+
|
|
34
|
+
- **`ChatRequest.reasoningEffort`** (and `AgentOptions.reasoningEffort`) — a
|
|
35
|
+
new optional field. Nothing is sent to any provider unless you set it. If you
|
|
36
|
+
already pass `reasoning_effort` / `thinking` / `thinkingConfig` / `think`
|
|
37
|
+
through `providerOptions`, that keeps winning on collision.
|
|
38
|
+
- **A new `context` stream event, `reasoning_effort_disabled_for_tools`** — the
|
|
39
|
+
`openaiChat` engine emits it when it retried a tool-using request with
|
|
40
|
+
`reasoning_effort: 'none'` after OpenAI refused the first attempt. Ignoring
|
|
41
|
+
it keeps the old behaviour minus the 400. If you *want* the hard failure
|
|
42
|
+
instead (to route to `/v1/responses` yourself), set `reasoningEffort: 'none'`
|
|
43
|
+
explicitly on tool-using turns — then no retry happens and any 400 is yours.
|
|
44
|
+
|
|
3
45
|
## 0.5.x → 0.6.0
|
|
4
46
|
|
|
5
47
|
Behavior-compatible for ordinary consumers, with **two things worth checking**.
|
|
@@ -62,7 +104,7 @@ signature changed and no default behavior changed, so an app on 0.4.x can bump
|
|
|
62
104
|
without code edits and adopt the new capabilities as needed.
|
|
63
105
|
|
|
64
106
|
The numbers in brackets are the suggestion numbers from the AI Server Studio
|
|
65
|
-
production friction report (`docs/
|
|
107
|
+
production friction report ([`docs/archive/2026-08-09-ai-server-studio-friction-report.md`](./docs/archive/2026-08-09-ai-server-studio-friction-report.md)),
|
|
66
108
|
so you can trace each change back to the friction it resolves.
|
|
67
109
|
|
|
68
110
|
### Packaging
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { AIError } from '../../errors.js';
|
|
1
|
+
import { AIError, classify } from '../../errors.js';
|
|
2
2
|
import { estimatedUsage } from '../../tokens.js';
|
|
3
3
|
import { fetchJson, postResponse, sseLines } from '../../transport.js';
|
|
4
|
-
import { applyProviderOptions, asNumber, asRecord, asString, joinUrl, textFromMessages } from '../../util.js';
|
|
4
|
+
import { applyProviderOptions, REASONING_BUDGET_TOKENS, asNumber, asRecord, asString, joinUrl, textFromMessages } from '../../util.js';
|
|
5
5
|
import { DEFAULT_TIMEOUT_MS, parseArgs, randomId, safeParse, streamAnomaly, streamError, streamTimeout } from './base.js';
|
|
6
6
|
const JSON_MODE_TOOL = 'json_output';
|
|
7
7
|
export class AnthropicAdapter {
|
|
@@ -23,7 +23,8 @@ export class AnthropicAdapter {
|
|
|
23
23
|
}
|
|
24
24
|
async *stream(conn, req) {
|
|
25
25
|
const started = Date.now();
|
|
26
|
-
const
|
|
26
|
+
const nativeJson = modernModel(req.model);
|
|
27
|
+
const jsonMode = req.responseFormat?.type === 'json' && !nativeJson && !req.tools?.length;
|
|
27
28
|
let firstTokenMs = null;
|
|
28
29
|
let text = '';
|
|
29
30
|
let inputTokens;
|
|
@@ -31,6 +32,7 @@ export class AnthropicAdapter {
|
|
|
31
32
|
let stopReason;
|
|
32
33
|
let sawTerminal = false;
|
|
33
34
|
const emittedTools = [];
|
|
35
|
+
const responseBlocks = new Map();
|
|
34
36
|
// Partial tool_use blocks keyed by content-block index (input_json_delta accumulation).
|
|
35
37
|
const blocks = new Map();
|
|
36
38
|
const timeout = streamTimeout(conn, req.signal);
|
|
@@ -38,19 +40,29 @@ export class AnthropicAdapter {
|
|
|
38
40
|
// Anthropic JSON mode is implemented as a forced `json_output` tool, which
|
|
39
41
|
// cannot coexist with the caller's own tools — same silent-downgrade shape
|
|
40
42
|
// as Gemini, so it gets the same signal rather than quietly dropping.
|
|
41
|
-
if (req.responseFormat?.type === 'json' && req.tools?.length) {
|
|
43
|
+
if (req.responseFormat?.type === 'json' && !nativeJson && req.tools?.length) {
|
|
42
44
|
yield {
|
|
43
45
|
type: 'context',
|
|
44
46
|
kind: 'json_mode_downgraded',
|
|
45
47
|
data: { reason: 'Anthropic JSON mode uses a forced tool and cannot be combined with caller tools; the request was sent without JSON mode', provider: conn.provider },
|
|
46
48
|
};
|
|
47
49
|
}
|
|
50
|
+
if (req.responseFormat?.type === 'json' && nativeJson && !req.responseFormat.schema) {
|
|
51
|
+
yield { type: 'context', kind: 'json_mode_downgraded', data: { provider: conn.provider, reason: 'Claude native JSON outputs require a schema; using a JSON instruction without constrained decoding.' } };
|
|
52
|
+
}
|
|
48
53
|
try {
|
|
49
54
|
const res = await postResponse(`${conn.baseUrl}/v1/messages`, body(req, jsonMode), conn.headers, timeout.signal, conn.provider);
|
|
50
55
|
const contentType = res.headers.get('content-type') ?? '';
|
|
51
56
|
if (!contentType.includes('text/event-stream')) {
|
|
52
57
|
const data = await res.json();
|
|
53
58
|
const parsed = parseResponse(data);
|
|
59
|
+
const content = asRecord(data)?.content;
|
|
60
|
+
if (Array.isArray(content))
|
|
61
|
+
content.forEach((block, i) => {
|
|
62
|
+
const record = asRecord(block);
|
|
63
|
+
if (record)
|
|
64
|
+
responseBlocks.set(i, record);
|
|
65
|
+
});
|
|
54
66
|
inputTokens = parsed.inputTokens;
|
|
55
67
|
outputTokens = parsed.outputTokens;
|
|
56
68
|
stopReason = parsed.stopReason;
|
|
@@ -77,7 +89,12 @@ export class AnthropicAdapter {
|
|
|
77
89
|
if (!record)
|
|
78
90
|
continue;
|
|
79
91
|
const type = asString(record.type);
|
|
80
|
-
if (type === '
|
|
92
|
+
if (type === 'error') {
|
|
93
|
+
const error = asRecord(record.error);
|
|
94
|
+
const statuses = { overloaded_error: 529, rate_limit_error: 429, authentication_error: 401, permission_error: 403, invalid_request_error: 400, not_found_error: 404, api_error: 500, billing_error: 402, timeout_error: 504, request_too_large: 413 };
|
|
95
|
+
throw classify(statuses[asString(error?.type) ?? ''] ?? 500, JSON.stringify(record), conn.provider);
|
|
96
|
+
}
|
|
97
|
+
else if (type === 'message_start') {
|
|
81
98
|
const usage = asRecord(asRecord(record.message)?.usage);
|
|
82
99
|
inputTokens = asNumber(usage?.input_tokens) ?? inputTokens;
|
|
83
100
|
outputTokens = asNumber(usage?.output_tokens) ?? outputTokens;
|
|
@@ -85,6 +102,8 @@ export class AnthropicAdapter {
|
|
|
85
102
|
else if (type === 'content_block_start') {
|
|
86
103
|
const index = asNumber(record.index) ?? 0;
|
|
87
104
|
const block = asRecord(record.content_block);
|
|
105
|
+
if (block)
|
|
106
|
+
responseBlocks.set(index, { ...block });
|
|
88
107
|
if (block?.type === 'tool_use') {
|
|
89
108
|
blocks.set(index, { id: asString(block.id) ?? randomId(), name: asString(block.name) ?? 'unknown', raw: '' });
|
|
90
109
|
}
|
|
@@ -92,6 +111,14 @@ export class AnthropicAdapter {
|
|
|
92
111
|
else if (type === 'content_block_delta') {
|
|
93
112
|
const index = asNumber(record.index) ?? 0;
|
|
94
113
|
const delta = asRecord(record.delta);
|
|
114
|
+
const block = responseBlocks.get(index);
|
|
115
|
+
if (block && delta) {
|
|
116
|
+
const field = delta.type === 'text_delta' ? 'text' : delta.type === 'thinking_delta' ? 'thinking' : delta.type === 'signature_delta' ? 'signature' : undefined;
|
|
117
|
+
if (field)
|
|
118
|
+
block[field] = String(block[field] ?? '') + String(delta[field] ?? '');
|
|
119
|
+
if (delta.type === 'citations_delta')
|
|
120
|
+
block.citations = [...(Array.isArray(block.citations) ? block.citations : []), delta.citation];
|
|
121
|
+
}
|
|
95
122
|
if (delta?.type === 'text_delta') {
|
|
96
123
|
const piece = asString(delta.text) ?? '';
|
|
97
124
|
if (piece) {
|
|
@@ -118,6 +145,9 @@ export class AnthropicAdapter {
|
|
|
118
145
|
const partial = blocks.get(index);
|
|
119
146
|
if (partial) {
|
|
120
147
|
const call = { id: partial.id, name: partial.name, raw: partial.raw, arguments: parseArgs(partial.raw) };
|
|
148
|
+
const block = responseBlocks.get(index);
|
|
149
|
+
if (block)
|
|
150
|
+
block.input = call.arguments;
|
|
121
151
|
blocks.delete(index);
|
|
122
152
|
if (jsonMode && call.name === JSON_MODE_TOOL) {
|
|
123
153
|
text = jsonTextFrom([call], text);
|
|
@@ -149,6 +179,7 @@ export class AnthropicAdapter {
|
|
|
149
179
|
const hasTools = emittedTools.length > 0;
|
|
150
180
|
yield { type: 'done', result: {
|
|
151
181
|
text,
|
|
182
|
+
providerState: { provider: conn.provider, model: req.model, content: [...responseBlocks.entries()].sort(([a], [b]) => a - b).map(([, block]) => block) },
|
|
152
183
|
toolCalls: hasTools ? emittedTools : undefined,
|
|
153
184
|
finishReason: mapStop(stopReason, hasTools),
|
|
154
185
|
usage: inputTokens !== undefined || outputTokens !== undefined ? { inputTokens, outputTokens, estimated: false } : estimatedUsage(textFromMessages(req.messages), text),
|
|
@@ -192,9 +223,15 @@ function body(req, jsonMode) {
|
|
|
192
223
|
top_p: req.topP,
|
|
193
224
|
stop_sequences: req.stopSequences,
|
|
194
225
|
system,
|
|
195
|
-
messages: toAnthropicMessages(req.messages.filter((m) => m.role !== 'system')),
|
|
226
|
+
messages: toAnthropicMessages(req.messages.filter((m) => m.role !== 'system'), req.model),
|
|
196
227
|
stream: true,
|
|
197
228
|
};
|
|
229
|
+
if (req.responseFormat?.type === 'json' && modernModel(req.model) && req.responseFormat.schema) {
|
|
230
|
+
base.output_config = { format: { type: 'json_schema', schema: schemaFor(req) } };
|
|
231
|
+
}
|
|
232
|
+
if (req.responseFormat?.type === 'json' && modernModel(req.model) && !req.responseFormat.schema) {
|
|
233
|
+
base.system = [system, 'Return only valid JSON, without markdown fences or a preamble.'].filter(Boolean).join('\n\n');
|
|
234
|
+
}
|
|
198
235
|
if (jsonMode) {
|
|
199
236
|
// Forced-tool JSON mode: Anthropic has no response_format, so we make the
|
|
200
237
|
// model answer through a single required tool whose input is the JSON we want.
|
|
@@ -210,16 +247,51 @@ function body(req, jsonMode) {
|
|
|
210
247
|
else if (req.toolChoice === 'none')
|
|
211
248
|
base.tool_choice = { type: 'none' };
|
|
212
249
|
}
|
|
250
|
+
if (req.reasoningEffort !== undefined) {
|
|
251
|
+
// First-class reasoning effort → extended thinking. Anthropic requires
|
|
252
|
+
// `budget_tokens < max_tokens` and rejects temperature/top_p alongside
|
|
253
|
+
// thinking, so the budget is fitted under max_tokens (raising max_tokens
|
|
254
|
+
// when the caller's value leaves no room) and the samplers are dropped.
|
|
255
|
+
if (modernModel(req.model)) {
|
|
256
|
+
const none = req.reasoningEffort === 'none';
|
|
257
|
+
if (none && alwaysThinking(req.model))
|
|
258
|
+
throw new AIError('This Claude model cannot disable thinking; choose a reasoning effort instead.', { kind: 'invalid_request', provider: 'anthropic' });
|
|
259
|
+
base.thinking = { type: none ? (/^claude-sonnet-5-5(?:-|$)/.test(req.model) ? 'between_tools' : 'disabled') : 'adaptive' };
|
|
260
|
+
base.output_config = { ...asRecord(base.output_config), effort: none ? 'low' : req.reasoningEffort === 'minimal' ? 'low' : req.reasoningEffort };
|
|
261
|
+
}
|
|
262
|
+
else if (req.reasoningEffort === 'none') {
|
|
263
|
+
base.thinking = { type: 'disabled' };
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
const budget = REASONING_BUDGET_TOKENS[req.reasoningEffort];
|
|
267
|
+
const maxTokens = typeof base.max_tokens === 'number' ? base.max_tokens : 4096;
|
|
268
|
+
if (maxTokens <= budget)
|
|
269
|
+
base.max_tokens = budget + 1024;
|
|
270
|
+
base.thinking = { type: 'enabled', budget_tokens: budget };
|
|
271
|
+
delete base.temperature;
|
|
272
|
+
delete base.top_p;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
213
275
|
// providerOptions carries Anthropic-native fields (`thinking`, `metadata`,
|
|
214
276
|
// top-level `cache_control` extras, `service_tier`, …) without a release.
|
|
215
|
-
|
|
277
|
+
const result = applyProviderOptions(base, req.providerOptions, ['output_config']);
|
|
278
|
+
if (modernModel(req.model)) {
|
|
279
|
+
delete result.temperature;
|
|
280
|
+
delete result.top_p;
|
|
281
|
+
delete result.top_k;
|
|
282
|
+
}
|
|
283
|
+
const choice = asRecord(result.tool_choice);
|
|
284
|
+
if (noForcedTools(req.model) && (choice?.type === 'tool' || choice?.type === 'any')) {
|
|
285
|
+
throw new AIError('This Claude model does not support forced tool choice; use auto or structured outputs.', { kind: 'invalid_request', provider: 'anthropic' });
|
|
286
|
+
}
|
|
287
|
+
return result;
|
|
216
288
|
}
|
|
217
|
-
function toAnthropicMessages(messages) {
|
|
289
|
+
function toAnthropicMessages(messages, model) {
|
|
218
290
|
const out = [];
|
|
219
291
|
for (let index = 0; index < messages.length; index += 1) {
|
|
220
292
|
const message = messages[index];
|
|
221
293
|
if (message.role !== 'tool') {
|
|
222
|
-
out.push(toAnthropicMessage(message));
|
|
294
|
+
out.push(toAnthropicMessage(message, model));
|
|
223
295
|
continue;
|
|
224
296
|
}
|
|
225
297
|
const content = [];
|
|
@@ -233,7 +305,10 @@ function toAnthropicMessages(messages) {
|
|
|
233
305
|
}
|
|
234
306
|
return out;
|
|
235
307
|
}
|
|
236
|
-
function toAnthropicMessage(m) {
|
|
308
|
+
function toAnthropicMessage(m, model) {
|
|
309
|
+
if (m.role === 'assistant' && m.providerState?.provider === 'anthropic' && m.providerState.model === model) {
|
|
310
|
+
return { role: 'assistant', content: m.providerState.content };
|
|
311
|
+
}
|
|
237
312
|
// Tool-role messages are handled by toAnthropicMessages' batching loop and
|
|
238
313
|
// never reach here, so no tool branch is needed.
|
|
239
314
|
// assistant turns that carried tool calls replay them as tool_use blocks,
|
|
@@ -298,8 +373,21 @@ function jsonTextFrom(toolCalls, fallback) {
|
|
|
298
373
|
function mapStop(stop, hasTools) {
|
|
299
374
|
if (stop === 'tool_use' && hasTools)
|
|
300
375
|
return 'tool';
|
|
376
|
+
if (stop === 'refusal')
|
|
377
|
+
return 'content_filter';
|
|
301
378
|
if (stop === 'max_tokens')
|
|
302
379
|
return 'length';
|
|
303
380
|
return 'stop';
|
|
304
381
|
}
|
|
382
|
+
/** Models released after Opus 4.6 use adaptive effort and reject sampling knobs.
|
|
383
|
+
* Unknown/custom IDs retain legacy behavior rather than guessing capabilities. */
|
|
384
|
+
function modernModel(model) {
|
|
385
|
+
return /^claude-(?:opus-(?:4-[78]|5(?:-5)?)|sonnet-5(?:-5)?|haiku-5-5|(?:fable|mythos)-5(?:-1)?|mythos-preview)(?:-\d{8}|-latest)?$/.test(model);
|
|
386
|
+
}
|
|
387
|
+
function alwaysThinking(model) {
|
|
388
|
+
return /^claude-(?:fable-|mythos-|opus-5-5(?:-|$))/.test(model);
|
|
389
|
+
}
|
|
390
|
+
function noForcedTools(model) {
|
|
391
|
+
return /^claude-(?:(?:opus|sonnet)-5-5(?:-|$)|(?:fable|mythos)-5-1(?:-|$))/.test(model);
|
|
392
|
+
}
|
|
305
393
|
//# sourceMappingURL=anthropic.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AIError } from '../../errors.js';
|
|
2
2
|
import { estimatedUsage } from '../../tokens.js';
|
|
3
3
|
import { fetchJson, postResponse, sseLines } from '../../transport.js';
|
|
4
|
-
import { applyProviderOptions, asNumber, asRecord, asString, joinUrl, textFromMessages } from '../../util.js';
|
|
4
|
+
import { applyProviderOptions, REASONING_BUDGET_TOKENS, asNumber, asRecord, asString, joinUrl, textFromMessages } from '../../util.js';
|
|
5
5
|
import { DEFAULT_TIMEOUT_MS, randomId, safeParse, streamAnomaly, streamError, streamTimeout } from './base.js';
|
|
6
6
|
export class GoogleAdapter {
|
|
7
7
|
provider;
|
|
@@ -184,6 +184,13 @@ function body(req) {
|
|
|
184
184
|
config.allowedFunctionNames = [req.toolChoice.name];
|
|
185
185
|
payload.toolConfig = { functionCallingConfig: config };
|
|
186
186
|
}
|
|
187
|
+
if (req.reasoningEffort !== undefined) {
|
|
188
|
+
// First-class reasoning effort → Gemini's thinking budget. 0 disables
|
|
189
|
+
// thinking on models that allow it; others treat it as a minimum.
|
|
190
|
+
payload.generationConfig.thinkingConfig = {
|
|
191
|
+
thinkingBudget: REASONING_BUDGET_TOKENS[req.reasoningEffort],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
187
194
|
// providerOptions reaches Google-native fields: top-level `safetySettings`,
|
|
188
195
|
// `cachedContent`, and `generationConfig` extras (`thinkingConfig`,
|
|
189
196
|
// `responseModalities`, …) merged one level deep into generationConfig.
|
|
@@ -172,6 +172,12 @@ function body(req) {
|
|
|
172
172
|
format: req.responseFormat?.type === 'json' ? (req.responseFormat.schema ?? 'json') : undefined,
|
|
173
173
|
tools: req.tools?.map((tool) => ({ type: 'function', function: { name: tool.name, description: tool.description, parameters: tool.parameters } })),
|
|
174
174
|
};
|
|
175
|
+
// First-class reasoning effort → Ollama's `think` flag (reasoning arrives in
|
|
176
|
+
// `message.thinking`, which this engine already routes to `reasoning` events).
|
|
177
|
+
// Ollama has no graded effort for most models, so anything but `'none'` is
|
|
178
|
+
// `true`. Only sent when asked for: a model without thinking support rejects it.
|
|
179
|
+
if (req.reasoningEffort !== undefined)
|
|
180
|
+
base.think = req.reasoningEffort !== 'none';
|
|
175
181
|
// providerOptions reaches Ollama's real request fields — `options.num_ctx`,
|
|
176
182
|
// `options.num_keep`, top-level `keep_alive`, `think`, etc. `options` is
|
|
177
183
|
// merged one level deep so num_ctx joins the samplers above rather than
|
|
@@ -13,4 +13,10 @@ export declare class OpenAIChatAdapter implements ProviderAdapter {
|
|
|
13
13
|
}>;
|
|
14
14
|
embed(conn: ResolvedConnection, req: EmbedRequest): Promise<EmbedResult>;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* True when a `/chat/completions` 400 is the "function tools with reasoning_effort
|
|
18
|
+
* are not supported" refusal AND the request actually carried tools AND the caller
|
|
19
|
+
* did not already pin `reasoning_effort` (in which case the choice is theirs).
|
|
20
|
+
*/
|
|
21
|
+
export declare function shouldRetryWithoutReasoningEffort(error: unknown, req: ChatRequest): boolean;
|
|
16
22
|
//# sourceMappingURL=openaiChat.d.ts.map
|
|
@@ -34,7 +34,34 @@ export class OpenAIChatAdapter {
|
|
|
34
34
|
const timeout = streamTimeout(conn, req.signal);
|
|
35
35
|
yield { type: 'start', callId: '', provider: conn.provider, model: req.model };
|
|
36
36
|
try {
|
|
37
|
-
|
|
37
|
+
let res;
|
|
38
|
+
try {
|
|
39
|
+
res = await postResponse(urlFor(conn, this.profile, req), openAiBody(req, this.profile), conn.headers, timeout.signal, conn.provider);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
// OpenAI's `/chat/completions` refuses function tools on a reasoning model
|
|
43
|
+
// unless `reasoning_effort` is `'none'` ("Function tools with reasoning_effort
|
|
44
|
+
// are not supported … use /v1/responses or set reasoning_effort to 'none'").
|
|
45
|
+
// The nugget never sets `reasoning_effort` itself, so the provider default
|
|
46
|
+
// applies and every tool-using turn 400s. Rather than make each app learn
|
|
47
|
+
// this the hard way, retry the one request once with the effort disabled
|
|
48
|
+
// and say so on the stream. Reactive on purpose: sending
|
|
49
|
+
// `reasoning_effort` up front would 400 on non-reasoning models instead.
|
|
50
|
+
// A caller-requested effort is overridden too — the alternative is a hard
|
|
51
|
+
// 400 with no tool call at all — and the context event says so.
|
|
52
|
+
if (!shouldRetryWithoutReasoningEffort(error, req))
|
|
53
|
+
throw error;
|
|
54
|
+
yield {
|
|
55
|
+
type: 'context',
|
|
56
|
+
kind: 'reasoning_effort_disabled_for_tools',
|
|
57
|
+
data: {
|
|
58
|
+
reason: 'provider rejected function tools with reasoning_effort; retried with reasoning_effort: none',
|
|
59
|
+
requested: req.providerOptions?.reasoning_effort ?? req.reasoningEffort ?? null,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
const retryReq = { ...req, providerOptions: { ...req.providerOptions, reasoning_effort: 'none' } };
|
|
63
|
+
res = await postResponse(urlFor(conn, this.profile, retryReq), openAiBody(retryReq, this.profile), conn.headers, timeout.signal, conn.provider);
|
|
64
|
+
}
|
|
38
65
|
const contentType = res.headers.get('content-type') ?? '';
|
|
39
66
|
if (!contentType.includes('text/event-stream')) {
|
|
40
67
|
// Server ignored stream:true (or is a buffered gateway) — recover the whole body.
|
|
@@ -169,6 +196,10 @@ function openAiBody(req, profile) {
|
|
|
169
196
|
};
|
|
170
197
|
if (req.maxTokens !== undefined)
|
|
171
198
|
body[profile.quirks?.maxTokensParam ?? 'max_tokens'] = req.maxTokens;
|
|
199
|
+
// First-class reasoning effort → OpenAI's `reasoning_effort`. Only sent when
|
|
200
|
+
// asked for: non-reasoning models reject the parameter outright.
|
|
201
|
+
if (req.reasoningEffort !== undefined)
|
|
202
|
+
body.reasoning_effort = req.reasoningEffort;
|
|
172
203
|
// providerOptions carries OpenAI-native fields the nugget doesn't model
|
|
173
204
|
// (`reasoning_effort`, `parallel_tool_calls`, `seed`, `logprobs`, …). `apiVersion`
|
|
174
205
|
// is consumed by urlFor for Azure and stripped here so it never hits the body.
|
|
@@ -255,4 +286,23 @@ function mapFinish(finish, hasToolCalls) {
|
|
|
255
286
|
return 'content_filter';
|
|
256
287
|
return 'stop';
|
|
257
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* True when a `/chat/completions` 400 is the "function tools with reasoning_effort
|
|
291
|
+
* are not supported" refusal AND the request actually carried tools AND the caller
|
|
292
|
+
* did not already pin `reasoning_effort` (in which case the choice is theirs).
|
|
293
|
+
*/
|
|
294
|
+
export function shouldRetryWithoutReasoningEffort(error, req) {
|
|
295
|
+
if (!(error instanceof AIError) || error.status !== 400)
|
|
296
|
+
return false;
|
|
297
|
+
if (!req.tools || req.tools.length === 0)
|
|
298
|
+
return false;
|
|
299
|
+
// Already `'none'` (from either knob) — the refusal is about something else.
|
|
300
|
+
const effective = req.providerOptions && 'reasoning_effort' in req.providerOptions
|
|
301
|
+
? req.providerOptions.reasoning_effort
|
|
302
|
+
: req.reasoningEffort;
|
|
303
|
+
if (effective === 'none')
|
|
304
|
+
return false;
|
|
305
|
+
const text = `${error.raw ?? ''} ${error.message}`.toLowerCase();
|
|
306
|
+
return text.includes('reasoning_effort') && text.includes('tool');
|
|
307
|
+
}
|
|
258
308
|
//# sourceMappingURL=openaiChat.js.map
|
package/dist/agent/loop.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AIHandler } from '../handler.js';
|
|
2
|
-
import type { AIErrorKind, ChatMessage, Connection, StreamEvent, ToolCall, Usage } from '../types.js';
|
|
2
|
+
import type { AIErrorKind, ChatMessage, Connection, ReasoningEffort, StreamEvent, ToolCall, Usage } from '../types.js';
|
|
3
3
|
import type { ToolSpec } from './tools.js';
|
|
4
4
|
export interface AgentOptions {
|
|
5
5
|
handler: AIHandler;
|
|
@@ -30,6 +30,8 @@ export interface AgentOptions {
|
|
|
30
30
|
maxTokens?: number;
|
|
31
31
|
topP?: number;
|
|
32
32
|
stopSequences?: string[];
|
|
33
|
+
/** Reasoning effort forwarded to every turn — see {@link ChatRequest.reasoningEffort}. */
|
|
34
|
+
reasoningEffort?: ReasoningEffort;
|
|
33
35
|
/** Provider-native passthrough forwarded to every turn — see {@link ChatRequest.providerOptions}. */
|
|
34
36
|
providerOptions?: Record<string, unknown>;
|
|
35
37
|
budget?: {
|
package/dist/agent/loop.js
CHANGED
|
@@ -32,6 +32,7 @@ async function* run(opts, resolveResult) {
|
|
|
32
32
|
yield emit(opts, { type: 'step_start', step });
|
|
33
33
|
const calls = [];
|
|
34
34
|
let stepText = '';
|
|
35
|
+
let providerState;
|
|
35
36
|
let streamFailure;
|
|
36
37
|
// In promptJson mode a tool directive must not leak into the visible
|
|
37
38
|
// stream, so directive-looking text is withheld from `delta` events (still
|
|
@@ -46,6 +47,7 @@ async function* run(opts, resolveResult) {
|
|
|
46
47
|
maxTokens: opts.maxTokens,
|
|
47
48
|
topP: opts.topP,
|
|
48
49
|
stopSequences: opts.stopSequences,
|
|
50
|
+
reasoningEffort: opts.reasoningEffort,
|
|
49
51
|
providerOptions: opts.providerOptions,
|
|
50
52
|
signal: agentSignal.signal,
|
|
51
53
|
metadata: { ...opts.metadata, agentStep: step },
|
|
@@ -64,6 +66,7 @@ async function* run(opts, resolveResult) {
|
|
|
64
66
|
if (event.type === 'error')
|
|
65
67
|
streamFailure = event.error;
|
|
66
68
|
if (event.type === 'done') {
|
|
69
|
+
providerState = event.result.providerState;
|
|
67
70
|
usage = usage ? mergeUsage(usage, event.result.usage) : event.result.usage;
|
|
68
71
|
if (toolMode === 'promptJson' && calls.length === 0)
|
|
69
72
|
calls.push(...callsFromPromptJson(stepText));
|
|
@@ -84,7 +87,7 @@ async function* run(opts, resolveResult) {
|
|
|
84
87
|
finalText = stepText;
|
|
85
88
|
messages.push(toolMode === 'promptJson'
|
|
86
89
|
? { role: 'assistant', content: stepText }
|
|
87
|
-
: { role: 'assistant', content: stepText, toolCalls: calls.length ? calls : undefined });
|
|
90
|
+
: { role: 'assistant', content: stepText, providerState, toolCalls: calls.length ? calls : undefined });
|
|
88
91
|
if (calls.length === 0)
|
|
89
92
|
return yield* yieldDone('finished');
|
|
90
93
|
for (const call of calls) {
|