@jxburros/ai-nugget 0.4.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +158 -382
- package/UPGRADING.md +182 -0
- package/dist/adapters/engines/anthropic.d.ts +7 -1
- package/dist/adapters/engines/anthropic.js +82 -37
- package/dist/adapters/engines/base.d.ts +29 -2
- package/dist/adapters/engines/base.js +52 -8
- package/dist/adapters/engines/google.d.ts +7 -1
- package/dist/adapters/engines/google.js +80 -29
- package/dist/adapters/engines/ollama.d.ts +2 -1
- package/dist/adapters/engines/ollama.js +57 -15
- package/dist/adapters/engines/openaiChat.d.ts +8 -1
- package/dist/adapters/engines/openaiChat.js +109 -29
- package/dist/adapters/profiles.d.ts +21 -1
- package/dist/adapters/profiles.js +12 -2
- package/dist/agent/loop.d.ts +45 -1
- package/dist/agent/loop.js +117 -12
- package/dist/cjs/adapters/engines/anthropic.js +326 -0
- package/dist/cjs/adapters/engines/base.js +147 -0
- package/dist/cjs/adapters/engines/google.js +291 -0
- package/dist/cjs/adapters/engines/ollama.js +205 -0
- package/dist/cjs/adapters/engines/openaiChat.js +312 -0
- package/dist/cjs/adapters/index.js +22 -0
- package/dist/cjs/adapters/profiles.js +125 -0
- package/dist/cjs/agent/index.js +18 -0
- package/dist/cjs/agent/loop.js +373 -0
- package/dist/cjs/agent/tools.js +51 -0
- package/dist/cjs/connect.js +27 -0
- package/dist/cjs/errors.js +110 -0
- package/dist/cjs/handler.js +586 -0
- package/dist/cjs/index.js +28 -0
- package/dist/cjs/json.js +121 -0
- package/dist/cjs/keys.js +81 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/policy.js +42 -0
- package/dist/cjs/reasoning.js +159 -0
- package/dist/cjs/redact.js +78 -0
- package/dist/cjs/tokens.js +22 -0
- package/dist/cjs/transport.js +172 -0
- package/dist/cjs/types.js +6 -0
- package/dist/cjs/util.js +126 -0
- package/dist/errors.js +25 -1
- package/dist/handler.d.ts +67 -1
- package/dist/handler.js +266 -61
- 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/redact.d.ts +18 -0
- package/dist/redact.js +29 -1
- package/dist/transport.d.ts +3 -1
- package/dist/transport.js +21 -1
- package/dist/types.d.ts +112 -2
- package/dist/util.d.ts +22 -0
- package/dist/util.js +59 -0
- package/nugget/VERSION.txt +2 -2
- package/nugget/dist/adapters/engines/anthropic.d.ts +14 -0
- package/nugget/dist/adapters/engines/anthropic.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/anthropic.js +323 -0
- package/nugget/dist/adapters/engines/anthropic.js.map +1 -0
- package/nugget/dist/adapters/engines/base.d.ts +50 -0
- package/nugget/dist/adapters/engines/base.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/base.js +137 -0
- package/nugget/dist/adapters/engines/base.js.map +1 -0
- package/nugget/dist/adapters/engines/google.d.ts +24 -0
- package/nugget/dist/adapters/engines/google.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/google.js +288 -0
- package/nugget/dist/adapters/engines/google.js.map +1 -0
- package/nugget/dist/adapters/engines/ollama.d.ts +11 -0
- package/nugget/dist/adapters/engines/ollama.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/ollama.js +202 -0
- package/nugget/dist/adapters/engines/ollama.js.map +1 -0
- package/nugget/dist/adapters/engines/openaiChat.d.ts +22 -0
- package/nugget/dist/adapters/engines/openaiChat.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/openaiChat.js +308 -0
- package/nugget/dist/adapters/engines/openaiChat.js.map +1 -0
- package/nugget/dist/adapters/index.d.ts +5 -0
- package/nugget/dist/adapters/index.d.ts.map +1 -0
- package/nugget/dist/adapters/index.js +17 -0
- package/nugget/dist/adapters/index.js.map +1 -0
- package/nugget/dist/adapters/profiles.d.ts +62 -0
- package/nugget/dist/adapters/profiles.d.ts.map +1 -0
- package/nugget/dist/adapters/profiles.js +121 -0
- package/nugget/dist/adapters/profiles.js.map +1 -0
- package/nugget/dist/agent/index.d.ts +3 -0
- package/nugget/dist/agent/index.d.ts.map +1 -0
- package/nugget/dist/agent/index.js +3 -0
- package/nugget/dist/agent/index.js.map +1 -0
- package/nugget/dist/agent/loop.d.ts +114 -0
- package/nugget/dist/agent/loop.d.ts.map +1 -0
- package/nugget/dist/agent/loop.js +371 -0
- package/nugget/dist/agent/loop.js.map +1 -0
- package/nugget/dist/agent/tools.d.ts +29 -0
- package/nugget/dist/agent/tools.d.ts.map +1 -0
- package/nugget/dist/agent/tools.js +48 -0
- package/nugget/dist/agent/tools.js.map +1 -0
- package/nugget/dist/cjs/adapters/engines/anthropic.js +326 -0
- package/nugget/dist/cjs/adapters/engines/base.js +147 -0
- package/nugget/dist/cjs/adapters/engines/google.js +291 -0
- package/nugget/dist/cjs/adapters/engines/ollama.js +205 -0
- package/nugget/dist/cjs/adapters/engines/openaiChat.js +312 -0
- package/nugget/dist/cjs/adapters/index.js +22 -0
- package/nugget/dist/cjs/adapters/profiles.js +125 -0
- package/nugget/dist/cjs/agent/index.js +18 -0
- package/nugget/dist/cjs/agent/loop.js +373 -0
- package/nugget/dist/cjs/agent/tools.js +51 -0
- package/nugget/dist/cjs/connect.js +27 -0
- package/nugget/dist/cjs/errors.js +110 -0
- package/nugget/dist/cjs/handler.js +586 -0
- package/nugget/dist/cjs/index.js +28 -0
- package/nugget/dist/cjs/json.js +121 -0
- package/nugget/dist/cjs/keys.js +81 -0
- package/nugget/dist/cjs/package.json +3 -0
- package/nugget/dist/cjs/policy.js +42 -0
- package/nugget/dist/cjs/reasoning.js +159 -0
- package/nugget/dist/cjs/redact.js +78 -0
- package/nugget/dist/cjs/tokens.js +22 -0
- package/nugget/dist/cjs/transport.js +172 -0
- package/nugget/dist/cjs/types.js +6 -0
- package/nugget/dist/cjs/util.js +126 -0
- package/nugget/dist/connect.d.ts +31 -0
- package/nugget/dist/connect.d.ts.map +1 -0
- package/nugget/dist/connect.js +25 -0
- package/nugget/dist/connect.js.map +1 -0
- package/nugget/dist/errors.d.ts +22 -0
- package/nugget/dist/errors.d.ts.map +1 -0
- package/nugget/dist/errors.js +104 -0
- package/nugget/dist/errors.js.map +1 -0
- package/nugget/dist/handler.d.ts +114 -0
- package/nugget/dist/handler.d.ts.map +1 -0
- package/nugget/dist/handler.js +582 -0
- package/nugget/dist/handler.js.map +1 -0
- package/nugget/dist/index.d.ts +13 -0
- package/nugget/dist/index.d.ts.map +1 -0
- package/nugget/dist/index.js +13 -0
- package/nugget/dist/index.js.map +1 -0
- package/nugget/dist/json.d.ts +7 -0
- package/nugget/dist/json.d.ts.map +1 -0
- package/nugget/dist/json.js +114 -0
- package/nugget/dist/json.js.map +1 -0
- package/nugget/dist/keys.d.ts +7 -0
- package/nugget/dist/keys.d.ts.map +1 -0
- package/nugget/dist/keys.js +75 -0
- package/nugget/dist/keys.js.map +1 -0
- package/nugget/dist/policy.d.ts +6 -0
- package/nugget/dist/policy.d.ts.map +1 -0
- package/nugget/dist/policy.js +37 -0
- package/nugget/dist/policy.js.map +1 -0
- 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/redact.d.ts +26 -0
- package/nugget/dist/redact.d.ts.map +1 -0
- package/nugget/dist/redact.js +74 -0
- package/nugget/dist/redact.js.map +1 -0
- package/nugget/dist/tokens.d.ts +5 -0
- package/nugget/dist/tokens.d.ts.map +1 -0
- package/nugget/dist/tokens.js +18 -0
- package/nugget/dist/tokens.js.map +1 -0
- package/nugget/dist/transport.d.ts +30 -0
- package/nugget/dist/transport.d.ts.map +1 -0
- package/nugget/dist/transport.js +165 -0
- package/nugget/dist/transport.js.map +1 -0
- package/nugget/dist/types.d.ts +301 -0
- package/nugget/dist/types.d.ts.map +1 -0
- package/nugget/dist/types.js +4 -0
- package/nugget/dist/types.js.map +1 -0
- package/nugget/dist/util.d.ts +31 -0
- package/nugget/dist/util.d.ts.map +1 -0
- package/nugget/dist/util.js +114 -0
- package/nugget/dist/util.js.map +1 -0
- package/nugget/src/adapters/engines/anthropic.ts +78 -37
- package/nugget/src/adapters/engines/base.ts +50 -7
- package/nugget/src/adapters/engines/google.ts +79 -29
- package/nugget/src/adapters/engines/ollama.ts +56 -17
- package/nugget/src/adapters/engines/openaiChat.ts +104 -30
- package/nugget/src/adapters/profiles.ts +53 -3
- package/nugget/src/agent/loop.ts +156 -13
- package/nugget/src/errors.ts +25 -1
- package/nugget/src/handler.ts +308 -59
- package/nugget/src/index.ts +1 -0
- package/nugget/src/reasoning.ts +174 -0
- package/nugget/src/redact.ts +32 -1
- package/nugget/src/transport.ts +20 -1
- package/nugget/src/types.ts +113 -1
- package/nugget/src/util.ts +64 -0
- package/package.json +18 -5
package/UPGRADING.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Upgrading
|
|
2
|
+
|
|
3
|
+
## 0.6.x → 0.7.0
|
|
4
|
+
|
|
5
|
+
**Backward-compatible for ordinary consumers.** Two additive changes and one
|
|
6
|
+
default-behaviour change worth knowing about:
|
|
7
|
+
|
|
8
|
+
- **Inline reasoning is stripped by default.** A model that writes
|
|
9
|
+
`<think>…</think>` (or `<thinking>`, `<reasoning>`, `<|begin_of_thought|>`)
|
|
10
|
+
into its answer no longer has that text in `delta` events or
|
|
11
|
+
`ChatResult.text`; it arrives as `{ type: 'reasoning', text }` events, the
|
|
12
|
+
same channel a provider-native thinking field already used. If your app
|
|
13
|
+
parsed or displayed those tags itself, either delete that code or pass
|
|
14
|
+
`new AIHandler({ stripInlineReasoning: false })` to keep the raw text.
|
|
15
|
+
|
|
16
|
+
- **`ChatRequest.reasoningEffort`** (and `AgentOptions.reasoningEffort`) — a
|
|
17
|
+
new optional field. Nothing is sent to any provider unless you set it. If you
|
|
18
|
+
already pass `reasoning_effort` / `thinking` / `thinkingConfig` / `think`
|
|
19
|
+
through `providerOptions`, that keeps winning on collision.
|
|
20
|
+
- **A new `context` stream event, `reasoning_effort_disabled_for_tools`** — the
|
|
21
|
+
`openaiChat` engine emits it when it retried a tool-using request with
|
|
22
|
+
`reasoning_effort: 'none'` after OpenAI refused the first attempt. Ignoring
|
|
23
|
+
it keeps the old behaviour minus the 400. If you *want* the hard failure
|
|
24
|
+
instead (to route to `/v1/responses` yourself), set `reasoningEffort: 'none'`
|
|
25
|
+
explicitly on tool-using turns — then no retry happens and any 400 is yours.
|
|
26
|
+
|
|
27
|
+
## 0.5.x → 0.6.0
|
|
28
|
+
|
|
29
|
+
Behavior-compatible for ordinary consumers, with **two things worth checking**.
|
|
30
|
+
|
|
31
|
+
### 1. `not_found` is a new `AIErrorKind`
|
|
32
|
+
|
|
33
|
+
404 and 410 responses now classify as `not_found` instead of
|
|
34
|
+
`invalid_request`, so a wrong `baseUrl`/model or a misrouted edge is
|
|
35
|
+
distinguishable from a malformed request body. It is non-retryable by default.
|
|
36
|
+
|
|
37
|
+
- **If you `switch` exhaustively over `AIErrorKind`** (with a `never` default),
|
|
38
|
+
TypeScript will require a new branch. Map it like `auth`/`key_unavailable` —
|
|
39
|
+
it's a *server-side configuration* problem, not the caller's fault:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
case 'not_found': return { status: 502, message: 'The AI service is not configured correctly.' };
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- **If you switch non-exhaustively** with a fallback, nothing breaks: a 404 that
|
|
46
|
+
used to produce "that request was invalid" now falls into your default branch
|
|
47
|
+
instead. That is the intended improvement, but check the resulting message
|
|
48
|
+
still reads correctly.
|
|
49
|
+
|
|
50
|
+
### 2. A startup notice when no policy is configured
|
|
51
|
+
|
|
52
|
+
`new AIHandler({ keySource })` with no `policy` now logs one line at
|
|
53
|
+
construction: every provider and model is allowed. `allowAllPolicy()` is still
|
|
54
|
+
the default — nothing is blocked. To silence it, say what you mean:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
new AIHandler({ keySource, policy: allowAllPolicy() }); // deliberate, no warning
|
|
58
|
+
new AIHandler({ keySource, silencePolicyWarning: true }); // same, if you prefer
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Also new, no action required
|
|
62
|
+
|
|
63
|
+
- **`Connection.provider` is now typed** `KnownProvider | (string & {})`. You get
|
|
64
|
+
autocomplete and typo-catching; arbitrary strings still compile and still
|
|
65
|
+
resolve through `openai-compat`.
|
|
66
|
+
- **`beforeCall` hooks get a scrubbed connection.** `info.connection.keyRef` is
|
|
67
|
+
masked for literal refs, and `info.resolved.headers` masks auth headers. If a
|
|
68
|
+
hook was reading `info.connection.keyRef.value` to get the key — don't; resolve
|
|
69
|
+
it through your `KeySource` instead.
|
|
70
|
+
- **`Idempotency-Key` on OpenAI requests.** One key per logical call, reused
|
|
71
|
+
across retries. A header you set yourself on `Connection.headers` wins.
|
|
72
|
+
- **New `context` stream events**: `json_mode_downgraded` (Google/Anthropic drop
|
|
73
|
+
JSON mode when tools are present) and `stream_anomaly` on all four engines
|
|
74
|
+
(previously `openaiChat` only). Both are informational; ignoring them keeps
|
|
75
|
+
the old behavior.
|
|
76
|
+
- **Documentation moved.** `README.md` is now a front door; reference material
|
|
77
|
+
lives under `docs/` (`providers`, `reliability`, `security`, `agent-loop`,
|
|
78
|
+
`recipes`, `integrations`, `distribution`). Deep links into the old README
|
|
79
|
+
anchors will need updating.
|
|
80
|
+
|
|
81
|
+
## 0.4.x → 0.5.0
|
|
82
|
+
|
|
83
|
+
**0.5.0 is backward-compatible.** Every change below is additive — new optional
|
|
84
|
+
fields, new methods, new exports, and a new `require` entry point. No existing
|
|
85
|
+
signature changed and no default behavior changed, so an app on 0.4.x can bump
|
|
86
|
+
without code edits and adopt the new capabilities as needed.
|
|
87
|
+
|
|
88
|
+
The numbers in brackets are the suggestion numbers from the AI Server Studio
|
|
89
|
+
production friction report (`docs/reviews/2026-08-09-ai-server-studio-friction-report.md`),
|
|
90
|
+
so you can trace each change back to the friction it resolves.
|
|
91
|
+
|
|
92
|
+
### Packaging
|
|
93
|
+
|
|
94
|
+
- **[1] Dual ESM + CommonJS build.** The package now exposes a `require`
|
|
95
|
+
condition (`dist/cjs/`) in addition to ESM. CommonJS hosts can
|
|
96
|
+
`const { AIHandler } = require('@jxburros/ai-nugget')` with no dynamic-import
|
|
97
|
+
shim. ESM consumers are unaffected.
|
|
98
|
+
- **[2] Vendorable compiled output.** `npm run build:nugget` now emits both
|
|
99
|
+
`nugget/src` (TypeScript) and `nugget/dist` (compiled ESM `.js` + `.d.ts`), so
|
|
100
|
+
bundlers that can't resolve `.ts` via `.js`-suffixed imports (e.g. Turbopack)
|
|
101
|
+
can vendor `nugget/dist`.
|
|
102
|
+
|
|
103
|
+
### Requests & providers
|
|
104
|
+
|
|
105
|
+
- **[4] `ChatRequest.providerOptions`.** A provider-native passthrough merged
|
|
106
|
+
into the outgoing request body (shallow top-level; one level deep into Ollama
|
|
107
|
+
`options` and Google `generationConfig`). Reach fields the nugget doesn't
|
|
108
|
+
model — Ollama `num_ctx`/`keep_alive`, OpenAI `reasoning_effort`, Anthropic
|
|
109
|
+
`thinking`, Google `safetySettings` — without waiting for a release.
|
|
110
|
+
- **[16] `Connection.idleTimeoutMs`.** An idle timeout for streams, distinct from
|
|
111
|
+
the total `timeoutMs`: it aborts only when no chunk arrives for that long, and
|
|
112
|
+
resets on every chunk — so a slow-but-healthy long local generation isn't
|
|
113
|
+
killed mid-stream.
|
|
114
|
+
- **[17] `reasoning` stream events.** Reasoning/thinking tokens (Anthropic
|
|
115
|
+
`thinking_delta`, OpenAI `reasoning_content`, Gemini `thought` parts, Ollama
|
|
116
|
+
`thinking`) surface as `{ type: 'reasoning', text }` instead of being dropped
|
|
117
|
+
or blended into `delta`.
|
|
118
|
+
- **[18] `listModels()` for Anthropic & Google.** Both now query the real
|
|
119
|
+
endpoints (`/v1/models`, `/v1beta/models`) instead of returning `[]`.
|
|
120
|
+
- **[19] Faster Ollama discovery.** `/api/show` probes run with bounded
|
|
121
|
+
concurrency instead of serially.
|
|
122
|
+
- **[20] New provider profiles + Azure api-version override.** Added `cerebras`,
|
|
123
|
+
`moonshot`, `cohere`, and `perplexity`. The Azure `api-version` is now
|
|
124
|
+
overridable per call via `providerOptions.apiVersion` (default unchanged).
|
|
125
|
+
- **[21] Safer URL joining** in the `openai-compat` engine (no accidental double
|
|
126
|
+
slash even if `baseUrl` ends in `/`).
|
|
127
|
+
- **[23] Embeddings.** `AIHandler.embed(conn, req)` plus `EmbedRequest` /
|
|
128
|
+
`EmbedResult`, implemented for Ollama (`/api/embed`) and OpenAI-compatible
|
|
129
|
+
providers (`/embeddings`) — through the same policy/key/telemetry pipeline as
|
|
130
|
+
chat. Providers without embeddings throw a typed `invalid_request` error.
|
|
131
|
+
|
|
132
|
+
### Structured output & typed results
|
|
133
|
+
|
|
134
|
+
- **[15] `AIHandler.chatParsed(conn, req, schema)`.** Requests JSON, extracts it,
|
|
135
|
+
and validates against any [Standard Schema](https://standardschema.dev)
|
|
136
|
+
validator (Zod / Valibot / ArkType) — with one automatic corrective retry on a
|
|
137
|
+
validation miss. No schema library is bundled; you pass your own.
|
|
138
|
+
|
|
139
|
+
### Agent loop (`@jxburros/ai-nugget/agent`)
|
|
140
|
+
|
|
141
|
+
- **[5] Option passthrough.** `AgentOptions` now forwards `temperature`,
|
|
142
|
+
`maxTokens`, `topP`, `stopSequences`, and `providerOptions` to every turn.
|
|
143
|
+
- **[6] Capability-aware `toolMode`.** `AgentOptions.modelCapabilities` — when
|
|
144
|
+
`toolMode: 'auto'`, a model advertising `"tools"` uses native tool-calling even
|
|
145
|
+
on a local runtime whose profile default is `promptJson`.
|
|
146
|
+
- **[7] No leaked directives + `tool_mode` event.** In `promptJson` mode, tool
|
|
147
|
+
directives are withheld from the visible `delta` stream, and a `tool_mode`
|
|
148
|
+
event discloses the resolved protocol.
|
|
149
|
+
- **[8] Parameter schemas in the `promptJson` prompt**, so the model isn't
|
|
150
|
+
guessing argument shape.
|
|
151
|
+
- **[9] `undefined` tool results no longer crash a run.**
|
|
152
|
+
- **[10] `AgentResult.error`** is populated when `stopReason === 'error'`.
|
|
153
|
+
- **[11] Structured tool errors.** A tool returning `{ ok: false, ... }` is
|
|
154
|
+
surfaced with `isError: true` so the model can recover in-turn.
|
|
155
|
+
- **[12] `AgentOptions.toolResult`.** Opt-in `maxChars` cap and `wrapUntrusted`
|
|
156
|
+
framing for tool results re-entering context.
|
|
157
|
+
- **[14] `AgentOptions.approvalMode: 'all'`.** Gate every tool (not just
|
|
158
|
+
`sideEffects` ones) so `tool_denied` is reachable without per-tool flags.
|
|
159
|
+
|
|
160
|
+
### Handler, telemetry & security
|
|
161
|
+
|
|
162
|
+
- **[3] `AIHandler.prewarm(conn)`.** A no-throw warm-up (health probe) to move
|
|
163
|
+
cold DNS/TLS/daemon-spin-up cost off the first real call.
|
|
164
|
+
- **[22] `CallInfo.resolved`.** The `beforeCall` hook now receives the resolved
|
|
165
|
+
connection (base URL + headers, key omitted) so it can validate the effective
|
|
166
|
+
endpoint — e.g. a request-time SSRF re-check — without hijacking `keySource`.
|
|
167
|
+
- **[24] `HandlerOptions.pricing`.** An optional cost estimator whose result is
|
|
168
|
+
recorded on `CallRecord.costUsd`.
|
|
169
|
+
|
|
170
|
+
### Tooling & hygiene
|
|
171
|
+
|
|
172
|
+
- **[25]** Assistant messages with both content parts and tool calls no longer
|
|
173
|
+
drop their text/image parts on replay (Anthropic & Google); dead `role:'tool'`
|
|
174
|
+
branches removed; the `403 → non-retryable` policy is documented; ESLint and
|
|
175
|
+
Vitest coverage are wired up (`npm run lint`, `npm run test:coverage`).
|
|
176
|
+
|
|
177
|
+
### Not changed (deliberately)
|
|
178
|
+
|
|
179
|
+
- No provider policy ships as a library default (blocklists/allowlists remain the
|
|
180
|
+
app's configuration at the seam).
|
|
181
|
+
- Zero runtime dependencies — everything above is implemented with the isomorphic
|
|
182
|
+
core (`fetch`/`ReadableStream`/`AbortController`/`TextDecoder`) only.
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import type { ChatRequest, ChatResult, ProviderAdapter, ResolvedConnection, StreamEvent } from '../../types.js';
|
|
1
|
+
import type { ChatRequest, ChatResult, ModelInfo, ProviderAdapter, ResolvedConnection, StreamEvent } from '../../types.js';
|
|
2
2
|
export declare class AnthropicAdapter implements ProviderAdapter {
|
|
3
3
|
readonly provider: string;
|
|
4
4
|
constructor(provider: string);
|
|
5
5
|
chat(conn: ResolvedConnection, req: ChatRequest): Promise<ChatResult>;
|
|
6
6
|
stream(conn: ResolvedConnection, req: ChatRequest): AsyncIterable<StreamEvent>;
|
|
7
|
+
/**
|
|
8
|
+
* Lists models from Anthropic's `/v1/models` endpoint (auth + version headers
|
|
9
|
+
* already applied to `conn.headers`). The endpoint does not report a context
|
|
10
|
+
* window, so `contextWindow` is left undefined rather than guessed.
|
|
11
|
+
*/
|
|
12
|
+
listModels(conn: ResolvedConnection): Promise<ModelInfo[]>;
|
|
7
13
|
}
|
|
8
14
|
//# sourceMappingURL=anthropic.d.ts.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { AIError } from '../../errors.js';
|
|
2
2
|
import { estimatedUsage } from '../../tokens.js';
|
|
3
|
-
import { postResponse, sseLines } from '../../transport.js';
|
|
4
|
-
import { asNumber, asRecord, asString, textFromMessages } from '../../util.js';
|
|
5
|
-
import { streamError, streamTimeout } from './base.js';
|
|
3
|
+
import { fetchJson, postResponse, sseLines } from '../../transport.js';
|
|
4
|
+
import { applyProviderOptions, REASONING_BUDGET_TOKENS, asNumber, asRecord, asString, joinUrl, textFromMessages } from '../../util.js';
|
|
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 {
|
|
8
8
|
provider;
|
|
@@ -29,11 +29,22 @@ export class AnthropicAdapter {
|
|
|
29
29
|
let inputTokens;
|
|
30
30
|
let outputTokens;
|
|
31
31
|
let stopReason;
|
|
32
|
+
let sawTerminal = false;
|
|
32
33
|
const emittedTools = [];
|
|
33
34
|
// Partial tool_use blocks keyed by content-block index (input_json_delta accumulation).
|
|
34
35
|
const blocks = new Map();
|
|
35
36
|
const timeout = streamTimeout(conn, req.signal);
|
|
36
37
|
yield { type: 'start', callId: '', provider: conn.provider, model: req.model };
|
|
38
|
+
// Anthropic JSON mode is implemented as a forced `json_output` tool, which
|
|
39
|
+
// cannot coexist with the caller's own tools — same silent-downgrade shape
|
|
40
|
+
// as Gemini, so it gets the same signal rather than quietly dropping.
|
|
41
|
+
if (req.responseFormat?.type === 'json' && req.tools?.length) {
|
|
42
|
+
yield {
|
|
43
|
+
type: 'context',
|
|
44
|
+
kind: 'json_mode_downgraded',
|
|
45
|
+
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
|
+
};
|
|
47
|
+
}
|
|
37
48
|
try {
|
|
38
49
|
const res = await postResponse(`${conn.baseUrl}/v1/messages`, body(req, jsonMode), conn.headers, timeout.signal, conn.provider);
|
|
39
50
|
const contentType = res.headers.get('content-type') ?? '';
|
|
@@ -43,6 +54,7 @@ export class AnthropicAdapter {
|
|
|
43
54
|
inputTokens = parsed.inputTokens;
|
|
44
55
|
outputTokens = parsed.outputTokens;
|
|
45
56
|
stopReason = parsed.stopReason;
|
|
57
|
+
sawTerminal = true;
|
|
46
58
|
if (jsonMode) {
|
|
47
59
|
text = jsonTextFrom(parsed.toolCalls, parsed.text);
|
|
48
60
|
if (text)
|
|
@@ -60,6 +72,7 @@ export class AnthropicAdapter {
|
|
|
60
72
|
}
|
|
61
73
|
else {
|
|
62
74
|
for await (const line of sseLines(res)) {
|
|
75
|
+
timeout.bump();
|
|
63
76
|
const record = asRecord(safeParse(line));
|
|
64
77
|
if (!record)
|
|
65
78
|
continue;
|
|
@@ -88,6 +101,12 @@ export class AnthropicAdapter {
|
|
|
88
101
|
yield { type: 'delta', text: piece };
|
|
89
102
|
}
|
|
90
103
|
}
|
|
104
|
+
else if (delta?.type === 'thinking_delta') {
|
|
105
|
+
// Extended-thinking tokens ride the reasoning channel, kept out of `text`.
|
|
106
|
+
const piece = asString(delta.thinking) ?? '';
|
|
107
|
+
if (piece)
|
|
108
|
+
yield { type: 'reasoning', text: piece };
|
|
109
|
+
}
|
|
91
110
|
else if (delta?.type === 'input_json_delta') {
|
|
92
111
|
const partial = blocks.get(index);
|
|
93
112
|
if (partial)
|
|
@@ -112,14 +131,21 @@ export class AnthropicAdapter {
|
|
|
112
131
|
}
|
|
113
132
|
}
|
|
114
133
|
}
|
|
134
|
+
else if (type === 'message_stop') {
|
|
135
|
+
sawTerminal = true;
|
|
136
|
+
}
|
|
115
137
|
else if (type === 'message_delta') {
|
|
116
138
|
const delta = asRecord(record.delta);
|
|
139
|
+
if (asString(delta?.stop_reason))
|
|
140
|
+
sawTerminal = true;
|
|
117
141
|
stopReason = asString(delta?.stop_reason) ?? stopReason;
|
|
118
142
|
const usage = asRecord(record.usage);
|
|
119
143
|
outputTokens = asNumber(usage?.output_tokens) ?? outputTokens;
|
|
120
144
|
}
|
|
121
145
|
}
|
|
122
146
|
}
|
|
147
|
+
if (!sawTerminal)
|
|
148
|
+
yield streamAnomaly('stream ended without a stop_reason or message_stop');
|
|
123
149
|
const hasTools = emittedTools.length > 0;
|
|
124
150
|
yield { type: 'done', result: {
|
|
125
151
|
text,
|
|
@@ -138,6 +164,24 @@ export class AnthropicAdapter {
|
|
|
138
164
|
timeout.done();
|
|
139
165
|
}
|
|
140
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Lists models from Anthropic's `/v1/models` endpoint (auth + version headers
|
|
169
|
+
* already applied to `conn.headers`). The endpoint does not report a context
|
|
170
|
+
* window, so `contextWindow` is left undefined rather than guessed.
|
|
171
|
+
*/
|
|
172
|
+
async listModels(conn) {
|
|
173
|
+
const { data } = await fetchJson(joinUrl(conn.baseUrl, '/v1/models'), {
|
|
174
|
+
method: 'GET',
|
|
175
|
+
headers: conn.headers,
|
|
176
|
+
timeoutMs: Math.min(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, 15_000),
|
|
177
|
+
provider: conn.provider,
|
|
178
|
+
});
|
|
179
|
+
const rows = Array.isArray(asRecord(data)?.data) ? asRecord(data).data : [];
|
|
180
|
+
return rows
|
|
181
|
+
.map((row) => asString(asRecord(row)?.id))
|
|
182
|
+
.filter((id) => Boolean(id))
|
|
183
|
+
.map((id) => ({ id, source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl } }));
|
|
184
|
+
}
|
|
141
185
|
}
|
|
142
186
|
function body(req, jsonMode) {
|
|
143
187
|
const system = req.messages.filter((m) => m.role === 'system').map((m) => textContent(m.content)).join('\n\n') || undefined;
|
|
@@ -166,7 +210,27 @@ function body(req, jsonMode) {
|
|
|
166
210
|
else if (req.toolChoice === 'none')
|
|
167
211
|
base.tool_choice = { type: 'none' };
|
|
168
212
|
}
|
|
169
|
-
|
|
213
|
+
if (req.reasoningEffort !== undefined) {
|
|
214
|
+
// First-class reasoning effort → extended thinking. Anthropic requires
|
|
215
|
+
// `budget_tokens < max_tokens` and rejects temperature/top_p alongside
|
|
216
|
+
// thinking, so the budget is fitted under max_tokens (raising max_tokens
|
|
217
|
+
// when the caller's value leaves no room) and the samplers are dropped.
|
|
218
|
+
if (req.reasoningEffort === 'none') {
|
|
219
|
+
base.thinking = { type: 'disabled' };
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
const budget = REASONING_BUDGET_TOKENS[req.reasoningEffort];
|
|
223
|
+
const maxTokens = typeof base.max_tokens === 'number' ? base.max_tokens : 4096;
|
|
224
|
+
if (maxTokens <= budget)
|
|
225
|
+
base.max_tokens = budget + 1024;
|
|
226
|
+
base.thinking = { type: 'enabled', budget_tokens: budget };
|
|
227
|
+
delete base.temperature;
|
|
228
|
+
delete base.top_p;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// providerOptions carries Anthropic-native fields (`thinking`, `metadata`,
|
|
232
|
+
// top-level `cache_control` extras, `service_tier`, …) without a release.
|
|
233
|
+
return applyProviderOptions(base, req.providerOptions);
|
|
170
234
|
}
|
|
171
235
|
function toAnthropicMessages(messages) {
|
|
172
236
|
const out = [];
|
|
@@ -188,29 +252,29 @@ function toAnthropicMessages(messages) {
|
|
|
188
252
|
return out;
|
|
189
253
|
}
|
|
190
254
|
function toAnthropicMessage(m) {
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
content: [{ type: 'tool_result', tool_use_id: m.toolCallId ?? '', content: typeof m.content === 'string' ? m.content : '' }],
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
// assistant turns that carried tool calls replay them as tool_use blocks.
|
|
255
|
+
// Tool-role messages are handled by toAnthropicMessages' batching loop and
|
|
256
|
+
// never reach here, so no tool branch is needed.
|
|
257
|
+
// assistant turns that carried tool calls replay them as tool_use blocks,
|
|
258
|
+
// preserving any text/image content parts that accompanied the calls.
|
|
199
259
|
if (m.role === 'assistant' && m.toolCalls?.length) {
|
|
200
|
-
const content = [];
|
|
201
|
-
if (typeof m.content === 'string' && m.content)
|
|
202
|
-
content.push({ type: 'text', text: m.content });
|
|
260
|
+
const content = [...contentBlocks(m.content)];
|
|
203
261
|
for (const call of m.toolCalls)
|
|
204
262
|
content.push({ type: 'tool_use', id: call.id, name: call.name, input: call.arguments ?? {} });
|
|
205
263
|
return { role: 'assistant', content };
|
|
206
264
|
}
|
|
207
265
|
return {
|
|
208
266
|
role: m.role === 'assistant' ? 'assistant' : 'user',
|
|
209
|
-
content: typeof m.content === 'string' ? m.content : m.content
|
|
210
|
-
? { type: 'image', source: { type: 'base64', media_type: part.mimeType ?? 'image/png', data: part.imageBase64 ?? '' } }
|
|
211
|
-
: { type: 'text', text: part.text ?? '' }),
|
|
267
|
+
content: typeof m.content === 'string' ? m.content : contentBlocks(m.content),
|
|
212
268
|
};
|
|
213
269
|
}
|
|
270
|
+
/** Maps text/image content — a string or a ContentPart[] — onto Anthropic content blocks. */
|
|
271
|
+
function contentBlocks(content) {
|
|
272
|
+
if (typeof content === 'string')
|
|
273
|
+
return content ? [{ type: 'text', text: content }] : [];
|
|
274
|
+
return content.map((part) => part.type === 'image'
|
|
275
|
+
? { type: 'image', source: { type: 'base64', media_type: part.mimeType ?? 'image/png', data: part.imageBase64 ?? '' } }
|
|
276
|
+
: { type: 'text', text: part.text ?? '' });
|
|
277
|
+
}
|
|
214
278
|
function textContent(content) {
|
|
215
279
|
if (typeof content === 'string')
|
|
216
280
|
return content;
|
|
@@ -256,23 +320,4 @@ function mapStop(stop, hasTools) {
|
|
|
256
320
|
return 'length';
|
|
257
321
|
return 'stop';
|
|
258
322
|
}
|
|
259
|
-
function parseArgs(raw) {
|
|
260
|
-
try {
|
|
261
|
-
return raw ? JSON.parse(raw) : {};
|
|
262
|
-
}
|
|
263
|
-
catch {
|
|
264
|
-
return {};
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
function safeParse(line) {
|
|
268
|
-
try {
|
|
269
|
-
return JSON.parse(line);
|
|
270
|
-
}
|
|
271
|
-
catch {
|
|
272
|
-
return undefined;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
function randomId() {
|
|
276
|
-
return globalThis.crypto?.randomUUID?.() ?? `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
277
|
-
}
|
|
278
323
|
//# sourceMappingURL=anthropic.js.map
|
|
@@ -12,12 +12,39 @@ export declare const DEFAULT_TIMEOUT_MS = 120000;
|
|
|
12
12
|
export declare function streamError(error: unknown, timeout: {
|
|
13
13
|
timedOut(): boolean;
|
|
14
14
|
}, provider: string): AIError;
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Create a timeout/abort scope covering the full stream lifetime, plus an
|
|
17
|
+
* optional idle timeout (`conn.idleTimeoutMs`) that the engine rearms via
|
|
18
|
+
* `bump()` on each received chunk.
|
|
19
|
+
*/
|
|
16
20
|
export declare function streamTimeout(conn: ResolvedConnection, signal?: AbortSignal): ReturnType<typeof withTimeout>;
|
|
17
21
|
export declare function listOpenModels(conn: ResolvedConnection, profile: ProviderProfile): Promise<ModelInfo[]>;
|
|
18
22
|
export declare function health(conn: ResolvedConnection, profile: ProviderProfile): Promise<{
|
|
19
23
|
ok: boolean;
|
|
20
24
|
detail?: string;
|
|
21
25
|
}>;
|
|
22
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Coerce a provider's tool-call `arguments` into an object. Providers disagree
|
|
28
|
+
* on the shape: OpenAI/Anthropic stream a JSON *string*, Google and Ollama send
|
|
29
|
+
* a native object — but Ollama-compatible backends (llama.cpp and friends)
|
|
30
|
+
* sometimes send a string too. Malformed JSON degrades to `{}` so a single bad
|
|
31
|
+
* tool call surfaces as an argument-validation error rather than a stream crash.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseArgs(raw: unknown): unknown;
|
|
34
|
+
/** JSON.parse that returns undefined instead of throwing — for per-line stream frames. */
|
|
35
|
+
export declare function safeParse(line: string): unknown;
|
|
36
|
+
/** Stable-enough id for a tool call a provider didn't give one for. */
|
|
37
|
+
export declare function randomId(): string;
|
|
38
|
+
/**
|
|
39
|
+
* A stream that ended without the provider's terminal marker (an OpenAI
|
|
40
|
+
* `finish_reason`, an Anthropic `message_delta.stop_reason`, a Google
|
|
41
|
+
* `finishReason`, an Ollama `done`) was almost certainly truncated. Every
|
|
42
|
+
* engine yields this so a dropped connection is diagnosable identically
|
|
43
|
+
* regardless of provider.
|
|
44
|
+
*/
|
|
45
|
+
export declare function streamAnomaly(reason?: string): {
|
|
46
|
+
type: 'context';
|
|
47
|
+
kind: string;
|
|
48
|
+
data: unknown;
|
|
49
|
+
};
|
|
23
50
|
//# sourceMappingURL=base.d.ts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AIError, fromUnknown } from '../../errors.js';
|
|
2
2
|
import { fetchJson, withTimeout } from '../../transport.js';
|
|
3
|
-
import { asRecord, asString } from '../../util.js';
|
|
3
|
+
import { asRecord, asString, joinUrl } from '../../util.js';
|
|
4
4
|
export const DEFAULT_TIMEOUT_MS = 120_000;
|
|
5
5
|
/**
|
|
6
6
|
* Maps a raw streaming failure onto a typed {@link AIError}. A fired timeout is
|
|
@@ -13,14 +13,18 @@ export function streamError(error, timeout, provider) {
|
|
|
13
13
|
return new AIError('Request timed out', { kind: 'timeout', provider });
|
|
14
14
|
return fromUnknown(error, provider);
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Create a timeout/abort scope covering the full stream lifetime, plus an
|
|
18
|
+
* optional idle timeout (`conn.idleTimeoutMs`) that the engine rearms via
|
|
19
|
+
* `bump()` on each received chunk.
|
|
20
|
+
*/
|
|
17
21
|
export function streamTimeout(conn, signal) {
|
|
18
|
-
return withTimeout(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, signal);
|
|
22
|
+
return withTimeout(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, signal, conn.idleTimeoutMs);
|
|
19
23
|
}
|
|
20
24
|
export async function listOpenModels(conn, profile) {
|
|
21
25
|
if (!profile.listModelsPath)
|
|
22
26
|
return [];
|
|
23
|
-
const { data } = await fetchJson(
|
|
27
|
+
const { data } = await fetchJson(joinUrl(conn.baseUrl, profile.listModelsPath), {
|
|
24
28
|
method: 'GET',
|
|
25
29
|
headers: conn.headers,
|
|
26
30
|
timeoutMs: conn.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
@@ -53,7 +57,7 @@ export async function health(conn, profile) {
|
|
|
53
57
|
if (!path)
|
|
54
58
|
return { ok: true };
|
|
55
59
|
try {
|
|
56
|
-
await fetchJson(
|
|
60
|
+
await fetchJson(joinUrl(conn.baseUrl, path), {
|
|
57
61
|
method: 'GET',
|
|
58
62
|
headers: conn.headers,
|
|
59
63
|
timeoutMs: Math.min(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, 10_000),
|
|
@@ -65,9 +69,49 @@ export async function health(conn, profile) {
|
|
|
65
69
|
return { ok: false, detail: error instanceof Error ? error.message : 'Health check failed' };
|
|
66
70
|
}
|
|
67
71
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Coerce a provider's tool-call `arguments` into an object. Providers disagree
|
|
74
|
+
* on the shape: OpenAI/Anthropic stream a JSON *string*, Google and Ollama send
|
|
75
|
+
* a native object — but Ollama-compatible backends (llama.cpp and friends)
|
|
76
|
+
* sometimes send a string too. Malformed JSON degrades to `{}` so a single bad
|
|
77
|
+
* tool call surfaces as an argument-validation error rather than a stream crash.
|
|
78
|
+
*/
|
|
79
|
+
export function parseArgs(raw) {
|
|
80
|
+
if (raw === undefined || raw === null)
|
|
81
|
+
return {};
|
|
82
|
+
if (typeof raw !== 'string')
|
|
83
|
+
return raw;
|
|
84
|
+
if (!raw.trim())
|
|
85
|
+
return {};
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(raw);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** JSON.parse that returns undefined instead of throwing — for per-line stream frames. */
|
|
94
|
+
export function safeParse(line) {
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(line);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Stable-enough id for a tool call a provider didn't give one for. */
|
|
103
|
+
export function randomId() {
|
|
104
|
+
return globalThis.crypto?.randomUUID?.() ?? `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* A stream that ended without the provider's terminal marker (an OpenAI
|
|
108
|
+
* `finish_reason`, an Anthropic `message_delta.stop_reason`, a Google
|
|
109
|
+
* `finishReason`, an Ollama `done`) was almost certainly truncated. Every
|
|
110
|
+
* engine yields this so a dropped connection is diagnosable identically
|
|
111
|
+
* regardless of provider.
|
|
112
|
+
*/
|
|
113
|
+
export function streamAnomaly(reason = 'stream ended without a terminal finish reason') {
|
|
114
|
+
return { type: 'context', kind: 'stream_anomaly', data: { reason } };
|
|
71
115
|
}
|
|
72
116
|
/**
|
|
73
117
|
* Pulls a context-window figure out of the many shapes providers use for it
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChatRequest, ChatResult, ProviderAdapter, ResolvedConnection, StreamEvent } from '../../types.js';
|
|
1
|
+
import type { ChatRequest, ChatResult, ModelInfo, ProviderAdapter, ResolvedConnection, StreamEvent } from '../../types.js';
|
|
2
2
|
export declare class GoogleAdapter implements ProviderAdapter {
|
|
3
3
|
readonly provider: string;
|
|
4
4
|
constructor(provider: string);
|
|
@@ -14,5 +14,11 @@ export declare class GoogleAdapter implements ProviderAdapter {
|
|
|
14
14
|
ok: boolean;
|
|
15
15
|
detail?: string;
|
|
16
16
|
}>;
|
|
17
|
+
/**
|
|
18
|
+
* Lists models from Google's `/v1beta/models`. Strips the `models/` name
|
|
19
|
+
* prefix, and maps `inputTokenLimit` → `contextWindow` and
|
|
20
|
+
* `supportedGenerationMethods` → `capabilities`.
|
|
21
|
+
*/
|
|
22
|
+
listModels(conn: ResolvedConnection): Promise<ModelInfo[]>;
|
|
17
23
|
}
|
|
18
24
|
//# sourceMappingURL=google.d.ts.map
|