@moxxy/plugin-provider-anthropic 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Moxxy (moxxy.ai)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # @moxxy/plugin-provider-anthropic
2
+
3
+ The **Anthropic (Claude)** `LLMProvider` for [moxxy](https://moxxy.ai) — streams
4
+ the Messages API with tool use and adaptive extended thinking, and translates
5
+ moxxy's `ProviderRequest`/event shape ↔ the Anthropic wire format. Register it
6
+ into a [`Session`](https://www.npmjs.com/package/@moxxy/core) to route turns to
7
+ Claude.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm i @moxxy/core @moxxy/sdk @moxxy/mode-default @moxxy/plugin-provider-anthropic
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ import { Session, runTurn, autoAllowResolver } from '@moxxy/core';
19
+ import defaultModePlugin from '@moxxy/mode-default';
20
+ import anthropicPlugin from '@moxxy/plugin-provider-anthropic';
21
+
22
+ const session = new Session({ cwd: process.cwd(), permissionResolver: autoAllowResolver });
23
+ session.pluginHost.registerStatic(defaultModePlugin);
24
+ session.pluginHost.registerStatic(anthropicPlugin);
25
+
26
+ session.providers.setActive('anthropic', {
27
+ apiKey: process.env.ANTHROPIC_API_KEY,
28
+ // model: 'claude-opus-4-8', // optional — defaults to the built-in catalog
29
+ });
30
+
31
+ for await (const e of runTurn(session, 'Write a haiku about TypeScript.')) {
32
+ if (e.type === 'assistant_chunk') process.stdout.write(e.delta);
33
+ }
34
+ ```
35
+
36
+ ## Config
37
+
38
+ | field | meaning |
39
+ |---|---|
40
+ | `apiKey` | your Anthropic API key (or wire it through the Session's `secretResolver`) |
41
+ | `model` / `models` | pick / override the advertised model catalog |
42
+
43
+ Mix it with [`@moxxy/plugin-provider-openai`](https://www.npmjs.com/package/@moxxy/plugin-provider-openai):
44
+ register both and `setActive('anthropic' | 'openai', …)` to switch vendors per
45
+ turn.
46
+
47
+ ## Exports
48
+
49
+ `default` / `anthropicPlugin` (register this), `anthropicProviderDef`,
50
+ `AnthropicProvider`, `anthropicModels`, `validateKey`.
51
+
52
+ ## License
53
+
54
+ MIT
@@ -0,0 +1,9 @@
1
+ import { AnthropicProvider, anthropicModels, type AnthropicProviderConfig } from './provider.js';
2
+ export { AnthropicProvider, anthropicModels };
3
+ export type { AnthropicProviderConfig };
4
+ export { toAnthropicMessages, toAnthropicTools } from './translate.js';
5
+ export { validateKey, type ValidateKeyDeps, type ValidationResult } from './validate.js';
6
+ export declare const anthropicProviderDef: import("@moxxy/sdk").ProviderDef;
7
+ export declare const anthropicPlugin: import("@moxxy/sdk").Plugin;
8
+ export default anthropicPlugin;
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,KAAK,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAEjG,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,CAAC;AAC9C,YAAY,EAAE,uBAAuB,EAAE,CAAC;AACxC,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvE,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAIzF,eAAO,MAAM,oBAAoB,kCAK/B,CAAC;AAEH,eAAO,MAAM,eAAe,6BAI1B,CAAC;AAEH,eAAe,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ import { defineProvider, definePlugin } from '@moxxy/sdk';
2
+ import { AnthropicProvider, anthropicModels } from './provider.js';
3
+ export { AnthropicProvider, anthropicModels };
4
+ export { toAnthropicMessages, toAnthropicTools } from './translate.js';
5
+ export { validateKey } from './validate.js';
6
+ import { validateKey as validateAnthropicKey } from './validate.js';
7
+ export const anthropicProviderDef = defineProvider({
8
+ name: 'anthropic',
9
+ models: [...anthropicModels],
10
+ createClient: (config) => new AnthropicProvider(config),
11
+ validateKey: (key) => validateAnthropicKey(key),
12
+ });
13
+ export const anthropicPlugin = definePlugin({
14
+ name: '@moxxy/plugin-provider-anthropic',
15
+ version: '0.0.0',
16
+ providers: [anthropicProviderDef],
17
+ });
18
+ export default anthropicPlugin;
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAgC,MAAM,eAAe,CAAC;AAEjG,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,CAAC;AAE9C,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvE,OAAO,EAAE,WAAW,EAA+C,MAAM,eAAe,CAAC;AAEzF,OAAO,EAAE,WAAW,IAAI,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAEpE,MAAM,CAAC,MAAM,oBAAoB,GAAG,cAAc,CAAC;IACjD,IAAI,EAAE,WAAW;IACjB,MAAM,EAAE,CAAC,GAAG,eAAe,CAAC;IAC5B,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,iBAAiB,CAAC,MAAiC,CAAC;IAClF,WAAW,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,oBAAoB,CAAC,GAAG,CAAC;CAChD,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,eAAe,GAAG,YAAY,CAAC;IAC1C,IAAI,EAAE,kCAAkC;IACxC,OAAO,EAAE,OAAO;IAChB,SAAS,EAAE,CAAC,oBAAoB,CAAC;CAClC,CAAC,CAAC;AAEH,eAAe,eAAe,CAAC"}
@@ -0,0 +1,99 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import type { LLMProvider, ModelDescriptor, ProviderEvent, ProviderRequest } from '@moxxy/sdk';
3
+ export interface AnthropicProviderConfig {
4
+ readonly apiKey?: string;
5
+ readonly baseURL?: string;
6
+ readonly defaultModel?: string;
7
+ readonly client?: Anthropic;
8
+ /**
9
+ * Override the reported provider name (used in error context). Defaults
10
+ * to `'anthropic'`. The `claude-code` subscription provider reuses this
11
+ * class with `name: 'claude-code'` so errors/metering attribute correctly.
12
+ */
13
+ readonly name?: string;
14
+ /**
15
+ * Override the advertised model catalog. Defaults to the Anthropic catalog
16
+ * ({@link anthropicModels}). An Anthropic-compatible vendor that reuses this
17
+ * class (e.g. `@moxxy/plugin-provider-zai`'s GLM Coding Plan path, which
18
+ * points `baseURL` at z.ai's Anthropic Messages endpoint) passes its own
19
+ * descriptors so context-window lookups (compaction/elision budgets) and
20
+ * capability gating run against the vendor's models, not Claude's.
21
+ */
22
+ readonly models?: ReadonlyArray<ModelDescriptor>;
23
+ /**
24
+ * OAuth (Claude subscription) mode. When set, the client authenticates
25
+ * with `Authorization: Bearer <oauthToken>` instead of an `x-api-key`,
26
+ * and the request/response is otherwise the standard Messages API. Used
27
+ * by `@moxxy/plugin-provider-claude-code`; the plain `anthropic` provider
28
+ * leaves all of these unset and behaves exactly as before.
29
+ */
30
+ readonly oauthToken?: string;
31
+ /** `anthropic-beta` values sent with every OAuth-mode request (joined by `,`). */
32
+ readonly oauthBeta?: ReadonlyArray<string>;
33
+ /**
34
+ * Text injected as the FIRST system block in OAuth mode. Claude rejects
35
+ * subscription tokens unless the system prompt leads with the Claude Code
36
+ * identity line, so the provider prepends this ahead of the real system
37
+ * prompt (which follows as the next block).
38
+ */
39
+ readonly systemPreamble?: string;
40
+ /** Epoch-ms expiry of `oauthToken`, if known — drives proactive refresh. */
41
+ readonly oauthExpiresAt?: number;
42
+ /**
43
+ * Refresh callback. Returns a fresh access token (and its expiry). Invoked
44
+ * proactively just before a request when the current token is near expiry,
45
+ * and once more reactively if a request still comes back 401. Omit for
46
+ * non-refreshable tokens (e.g. a pasted `claude setup-token`).
47
+ */
48
+ readonly oauthRefresh?: () => Promise<{
49
+ readonly token: string;
50
+ readonly expiresAt?: number;
51
+ }>;
52
+ }
53
+ export declare const anthropicModels: ReadonlyArray<ModelDescriptor>;
54
+ export declare class AnthropicProvider implements LLMProvider {
55
+ readonly name: string;
56
+ readonly models: ReadonlyArray<ModelDescriptor>;
57
+ private client;
58
+ private readonly defaultModel;
59
+ private readonly baseURL?;
60
+ private readonly oauth?;
61
+ private oauthToken?;
62
+ private oauthExpiresAt?;
63
+ constructor(config?: AnthropicProviderConfig);
64
+ /**
65
+ * Build an Anthropic client in OAuth (bearer) mode. `apiKey: null` stops
66
+ * the SDK from falling back to `ANTHROPIC_API_KEY` and suppresses the
67
+ * `x-api-key` header (its `apiKeyAuth()` returns `{}` when apiKey is null),
68
+ * so only `Authorization: Bearer <token>` goes out, plus the beta header.
69
+ */
70
+ private makeOauthClient;
71
+ /** Proactively refresh the bearer token when it's within the skew window. */
72
+ private ensureFreshOauth;
73
+ /** Force a token refresh and rebuild the client with the new bearer. */
74
+ private refreshOauthNow;
75
+ /**
76
+ * Build the `system` request field. In OAuth mode the Claude Code identity
77
+ * preamble MUST lead, so we always emit block form with the preamble first;
78
+ * the real system prompt follows (carrying the cache breakpoint if set).
79
+ * In apiKey mode the behaviour is unchanged: a bare string, upgraded to a
80
+ * single cache-marked block only when a system cache hint is present.
81
+ *
82
+ * `extraSystem` is `ProviderRequest.system` — the hook-injection side
83
+ * channel (e.g. the memory consolidation nudge), delivered IN ADDITION to
84
+ * the message-derived system prompt. It rides as a separate block AFTER
85
+ * the cache-marked one so volatile per-request text never busts the
86
+ * stable system-prefix cache.
87
+ */
88
+ private buildSystemParam;
89
+ stream(req: ProviderRequest): AsyncIterable<ProviderEvent>;
90
+ /**
91
+ * One streaming attempt. THROWS on transport/HTTP errors so `stream()` can
92
+ * decide whether to refresh-and-replay (OAuth 401) or surface the error;
93
+ * yields content events plus a terminal `message_end` on the happy path.
94
+ * Abort is terminal here (yields the abort error and returns).
95
+ */
96
+ private streamOnce;
97
+ countTokens(req: Pick<ProviderRequest, 'model' | 'messages' | 'system' | 'tools'>): Promise<number>;
98
+ }
99
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAC1C,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EACf,aAAa,EACb,eAAe,EAGhB,MAAM,YAAY,CAAC;AAOpB,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IACjD;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,kFAAkF;IAClF,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC3C;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,4EAA4E;IAC5E,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChG;AAaD,eAAO,MAAM,eAAe,EAAE,aAAa,CAAC,eAAe,CAO1D,CAAC;AAEF,qBAAa,iBAAkB,YAAW,WAAW;IACnD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAGhD,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAS;IAElC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAIrB;IACF,OAAO,CAAC,UAAU,CAAC,CAAS;IAC5B,OAAO,CAAC,cAAc,CAAC,CAAS;gBAEpB,MAAM,GAAE,uBAA4B;IAyBhD;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IAUvB,6EAA6E;YAC/D,gBAAgB;IAO9B,wEAAwE;YAC1D,eAAe;IAQ7B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,gBAAgB;IAwBjB,MAAM,CAAC,GAAG,EAAE,eAAe,GAAG,aAAa,CAAC,aAAa,CAAC;IAsFjE;;;;;OAKG;YACY,UAAU;IAyJnB,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,OAAO,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;CAkC1G"}
@@ -0,0 +1,438 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import { estimateTextTokens, toFriendlyError } from '@moxxy/sdk';
3
+ import { toAnthropicMessages, toAnthropicTools } from './translate.js';
4
+ // Hardcoded model catalog (re-exported to @moxxy/plugin-provider-claude-code, which
5
+ // reuses this provider class for the subscription path). Deriving it from the Models
6
+ // API is a larger change (auth + caching) — deliberately deferred (TECH_DEBT P3 #8).
7
+ // Values verified against the current Anthropic model catalog (2026-06): fable-5,
8
+ // opus-4-8, opus-4-7 and opus-4-6 carry a 1M context window with a 128k streaming
9
+ // ceiling; sonnet-4-6 is 1M/64k; haiku-4-5 is 200k/64k. fable-5 is Anthropic's most
10
+ // capable model (always-on reasoning); the loop never sets `temperature`, which
11
+ // fable-5/opus-4-8/4.7 reject — so they stream cleanly here, same as opus-4-7 already did.
12
+ // `supportsReasoning` marks models that accept adaptive thinking (`thinking:
13
+ // {type:'adaptive', display:'summarized'}`) — fable-5/opus-4-8/4-7/4-6 and sonnet-4-6
14
+ // do; haiku-4-5 does not (effort/adaptive-thinking error there), so it stays off.
15
+ export const anthropicModels = [
16
+ { id: 'claude-fable-5', contextWindow: 1_000_000, maxOutputTokens: 128_000, supportsTools: true, supportsStreaming: true, supportsImages: true, supportsDocuments: true, supportsReasoning: true },
17
+ { id: 'claude-opus-4-8', contextWindow: 1_000_000, maxOutputTokens: 128_000, supportsTools: true, supportsStreaming: true, supportsImages: true, supportsDocuments: true, supportsReasoning: true },
18
+ { id: 'claude-opus-4-7', contextWindow: 1_000_000, maxOutputTokens: 128_000, supportsTools: true, supportsStreaming: true, supportsImages: true, supportsDocuments: true, supportsReasoning: true },
19
+ { id: 'claude-opus-4-6', contextWindow: 1_000_000, maxOutputTokens: 128_000, supportsTools: true, supportsStreaming: true, supportsImages: true, supportsDocuments: true, supportsReasoning: true },
20
+ { id: 'claude-sonnet-4-6', contextWindow: 1_000_000, maxOutputTokens: 64_000, supportsTools: true, supportsStreaming: true, supportsImages: true, supportsDocuments: true, supportsReasoning: true },
21
+ { id: 'claude-haiku-4-5-20251001', contextWindow: 200_000, maxOutputTokens: 64_000, supportsTools: true, supportsStreaming: true, supportsImages: true, supportsDocuments: true },
22
+ ];
23
+ export class AnthropicProvider {
24
+ name;
25
+ models;
26
+ // Mutable so OAuth-mode refresh can swap in a client carrying the new
27
+ // bearer token; the plain apiKey client never changes after construction.
28
+ client;
29
+ defaultModel;
30
+ baseURL;
31
+ // Present only in OAuth (Claude subscription) mode.
32
+ oauth;
33
+ oauthToken;
34
+ oauthExpiresAt;
35
+ constructor(config = {}) {
36
+ this.name = config.name ?? 'anthropic';
37
+ this.models = config.models ?? anthropicModels;
38
+ this.defaultModel = config.defaultModel ?? 'claude-sonnet-4-6';
39
+ if (config.baseURL)
40
+ this.baseURL = config.baseURL;
41
+ if (config.oauthToken) {
42
+ this.oauthToken = config.oauthToken;
43
+ this.oauthExpiresAt = config.oauthExpiresAt;
44
+ this.oauth = {
45
+ beta: config.oauthBeta ?? [],
46
+ ...(config.systemPreamble ? { systemPreamble: config.systemPreamble } : {}),
47
+ ...(config.oauthRefresh ? { refresh: config.oauthRefresh } : {}),
48
+ };
49
+ this.client = config.client ?? this.makeOauthClient(config.oauthToken);
50
+ }
51
+ else {
52
+ this.client =
53
+ config.client ??
54
+ new Anthropic({
55
+ apiKey: config.apiKey ?? process.env.ANTHROPIC_API_KEY,
56
+ ...(config.baseURL ? { baseURL: config.baseURL } : {}),
57
+ });
58
+ }
59
+ }
60
+ /**
61
+ * Build an Anthropic client in OAuth (bearer) mode. `apiKey: null` stops
62
+ * the SDK from falling back to `ANTHROPIC_API_KEY` and suppresses the
63
+ * `x-api-key` header (its `apiKeyAuth()` returns `{}` when apiKey is null),
64
+ * so only `Authorization: Bearer <token>` goes out, plus the beta header.
65
+ */
66
+ makeOauthClient(token) {
67
+ const beta = this.oauth?.beta ?? [];
68
+ return new Anthropic({
69
+ apiKey: null,
70
+ authToken: token,
71
+ ...(beta.length > 0 ? { defaultHeaders: { 'anthropic-beta': beta.join(',') } } : {}),
72
+ ...(this.baseURL ? { baseURL: this.baseURL } : {}),
73
+ });
74
+ }
75
+ /** Proactively refresh the bearer token when it's within the skew window. */
76
+ async ensureFreshOauth() {
77
+ if (!this.oauth?.refresh)
78
+ return;
79
+ if (this.oauthExpiresAt === undefined)
80
+ return;
81
+ if (Date.now() + 60_000 < this.oauthExpiresAt)
82
+ return;
83
+ await this.refreshOauthNow();
84
+ }
85
+ /** Force a token refresh and rebuild the client with the new bearer. */
86
+ async refreshOauthNow() {
87
+ if (!this.oauth?.refresh)
88
+ throw new Error('no refresh callback');
89
+ const next = await this.oauth.refresh();
90
+ this.oauthToken = next.token;
91
+ this.oauthExpiresAt = next.expiresAt;
92
+ this.client = this.makeOauthClient(next.token);
93
+ }
94
+ /**
95
+ * Build the `system` request field. In OAuth mode the Claude Code identity
96
+ * preamble MUST lead, so we always emit block form with the preamble first;
97
+ * the real system prompt follows (carrying the cache breakpoint if set).
98
+ * In apiKey mode the behaviour is unchanged: a bare string, upgraded to a
99
+ * single cache-marked block only when a system cache hint is present.
100
+ *
101
+ * `extraSystem` is `ProviderRequest.system` — the hook-injection side
102
+ * channel (e.g. the memory consolidation nudge), delivered IN ADDITION to
103
+ * the message-derived system prompt. It rides as a separate block AFTER
104
+ * the cache-marked one so volatile per-request text never busts the
105
+ * stable system-prefix cache.
106
+ */
107
+ buildSystemParam(system, cacheSystem, extraSystem) {
108
+ const preamble = this.oauth?.systemPreamble;
109
+ if (preamble || extraSystem) {
110
+ const blocks = [];
111
+ if (preamble)
112
+ blocks.push({ type: 'text', text: preamble });
113
+ if (system) {
114
+ blocks.push(cacheSystem
115
+ ? { type: 'text', text: system, cache_control: { type: 'ephemeral' } }
116
+ : { type: 'text', text: system });
117
+ }
118
+ if (extraSystem)
119
+ blocks.push({ type: 'text', text: extraSystem });
120
+ return blocks.length > 0 ? blocks : undefined;
121
+ }
122
+ return cacheSystem && system
123
+ ? [{ type: 'text', text: system, cache_control: { type: 'ephemeral' } }]
124
+ : system;
125
+ }
126
+ async *stream(req) {
127
+ // Translate provider-neutral cache hints into Anthropic cache_control
128
+ // markers. `tools`/`system` mark those session-stable regions; a
129
+ // `{ messageIndex }` hint marks the end of that message (the rolling
130
+ // prefix breakpoint). Anthropic honors at most 4 breakpoints.
131
+ const hints = req.cacheHints ?? [];
132
+ const cacheTools = hints.some((h) => h.target === 'tools');
133
+ const cacheSystem = hints.some((h) => h.target === 'system');
134
+ const cacheMessageIndices = new Set();
135
+ for (const h of hints) {
136
+ if (typeof h.target === 'object')
137
+ cacheMessageIndices.add(h.target.messageIndex);
138
+ }
139
+ const { system, messages } = toAnthropicMessages(req.messages, { cacheMessageIndices });
140
+ const tools = req.tools && req.tools.length > 0
141
+ ? toAnthropicTools(req.tools, { cacheLast: cacheTools })
142
+ : undefined;
143
+ // OAuth mode prepends the Claude Code identity preamble as the first
144
+ // system block; apiKey mode keeps the prior string/cache-block behaviour.
145
+ // req.system (hook-injected extra system text) is appended last.
146
+ const systemParam = this.buildSystemParam(system, cacheSystem, req.system);
147
+ const model = req.model || this.defaultModel;
148
+ yield { type: 'message_start', model };
149
+ // In OAuth mode refresh the bearer proactively when it's near expiry, so
150
+ // we don't fire a request on a token we already knew was about to die.
151
+ if (this.oauth) {
152
+ try {
153
+ await this.ensureFreshOauth();
154
+ }
155
+ catch (err) {
156
+ yield { type: 'error', ...toFriendlyError(err, { provider: this.name }) };
157
+ return;
158
+ }
159
+ }
160
+ // NARROW cast: `messages`/`tools`/`systemParam` are our hand-rolled
161
+ // Anthropic shapes (e.g. `media_type: string`) which the SDK narrows to
162
+ // literal unions it can't see we never violate. The body is otherwise
163
+ // typed as the SDK's real `MessageStreamParams` — `model`/`max_tokens`/
164
+ // `temperature` are checked at compile time.
165
+ const requestBody = {
166
+ model,
167
+ max_tokens: req.maxTokens ?? 4096,
168
+ system: systemParam,
169
+ messages: messages,
170
+ tools: tools,
171
+ ...(req.temperature !== undefined ? { temperature: req.temperature } : {}),
172
+ };
173
+ // Reasoning: on supported models (gated upstream by `supportsReasoning`),
174
+ // enable adaptive thinking with summarized display so the model streams a
175
+ // readable reasoning summary (`display: 'omitted'` — the default — would
176
+ // stream empty thinking blocks). Adaptive thinking auto-enables interleaved
177
+ // thinking on the 4.6+ catalog, so no beta header is needed. `effort` maps
178
+ // to `output_config.effort`. Fields are attached via a cast because the
179
+ // pinned SDK's `MessageStreamParams` predates these params (same hand-rolled
180
+ // approach used for system/messages/tools above).
181
+ const reasoningOn = req.reasoning !== undefined && req.reasoning !== false;
182
+ if (reasoningOn) {
183
+ const effort = typeof req.reasoning === 'object' ? req.reasoning.effort : undefined;
184
+ const body = requestBody;
185
+ body.thinking = { type: 'adaptive', display: 'summarized' };
186
+ if (effort)
187
+ body.output_config = { effort };
188
+ }
189
+ // A 401 always arrives before any SSE body, so in OAuth mode we can force
190
+ // a single refresh and replay the request with no risk of duplicate output.
191
+ try {
192
+ yield* this.streamOnce(requestBody, req.signal);
193
+ }
194
+ catch (err) {
195
+ if (this.oauth?.refresh && isUnauthorized(err)) {
196
+ try {
197
+ await this.refreshOauthNow();
198
+ yield* this.streamOnce(requestBody, req.signal);
199
+ return;
200
+ }
201
+ catch (retryErr) {
202
+ yield { type: 'error', ...toFriendlyError(retryErr, { provider: this.name }) };
203
+ return;
204
+ }
205
+ }
206
+ yield { type: 'error', ...toFriendlyError(err, { provider: this.name }) };
207
+ }
208
+ }
209
+ /**
210
+ * One streaming attempt. THROWS on transport/HTTP errors so `stream()` can
211
+ * decide whether to refresh-and-replay (OAuth 401) or surface the error;
212
+ * yields content events plus a terminal `message_end` on the happy path.
213
+ * Abort is terminal here (yields the abort error and returns).
214
+ */
215
+ async *streamOnce(requestBody, signal) {
216
+ const stream = this.client.messages.stream(requestBody,
217
+ // Pass the AbortSignal into the SDK request options so cancelling
218
+ // tears down the underlying HTTP request. Without this, Esc only
219
+ // stopped our loop while the model kept generating upstream.
220
+ signal ? { signal } : undefined);
221
+ const pendingToolUses = new Map();
222
+ // Anthropic's stream events carry a block `index` on every delta/stop;
223
+ // we map that index to the tool_use id at content_block_start time so
224
+ // parallel tool_use blocks route their deltas correctly. Without this,
225
+ // we used to return the first key in `pendingToolUses` for every event,
226
+ // causing two parallel blocks to overwrite each other's partial JSON.
227
+ const blockIndexToId = new Map();
228
+ // Track which block indices are `thinking` blocks so their signature_delta
229
+ // accumulates and flushes as a reasoning_signature at content_block_stop.
230
+ const thinkingBlockIndices = new Set();
231
+ let pendingThinkingSig = '';
232
+ let stopReason = 'end_turn';
233
+ // Type as the SDK's `TokenUsage` (which carries the optional cache fields)
234
+ // so cacheReadTokens/cacheCreationTokens are first-class on the accumulator
235
+ // and the message_delta merge below is type-checked to preserve them —
236
+ // rather than the cache fields sneaking in only via a spread (which bypasses
237
+ // the excess-property check) against a narrower declared type.
238
+ let usage;
239
+ try {
240
+ for await (const event of stream) {
241
+ if (signal?.aborted) {
242
+ yield { type: 'error', message: 'aborted', retryable: false };
243
+ return;
244
+ }
245
+ switch (event.type) {
246
+ case 'message_start': {
247
+ // Anthropic reports cache hits/writes only on the message_start
248
+ // usage block — `cache_read_input_tokens` (billed 0.1x) and
249
+ // `cache_creation_input_tokens` (billed 1.25x). Capture them here
250
+ // so the metering layer can prove cache savings; without this the
251
+ // fields are silently dropped and cache wins are invisible.
252
+ const u = event.message?.usage;
253
+ usage = {
254
+ inputTokens: u?.input_tokens ?? 0,
255
+ outputTokens: u?.output_tokens ?? 0,
256
+ ...(u?.cache_read_input_tokens !== undefined
257
+ ? { cacheReadTokens: u.cache_read_input_tokens }
258
+ : {}),
259
+ ...(u?.cache_creation_input_tokens !== undefined
260
+ ? { cacheCreationTokens: u.cache_creation_input_tokens }
261
+ : {}),
262
+ };
263
+ break;
264
+ }
265
+ case 'content_block_start': {
266
+ const block = event.content_block;
267
+ if (block && block.type === 'tool_use') {
268
+ pendingToolUses.set(block.id, { name: block.name, partial: '' });
269
+ // Real Anthropic events carry `index` here; fall back to the
270
+ // arrival ordinal when callers (e.g. test fakes) omit it.
271
+ const idx = typeof event.index === 'number' ? event.index : blockIndexToId.size;
272
+ blockIndexToId.set(idx, block.id);
273
+ yield { type: 'tool_use_start', id: block.id, name: block.name };
274
+ }
275
+ else if (block && block.type === 'thinking') {
276
+ if (typeof event.index === 'number')
277
+ thinkingBlockIndices.add(event.index);
278
+ pendingThinkingSig = '';
279
+ }
280
+ else if (block && block.type === 'redacted_thinking') {
281
+ // No readable text — replay the opaque blob verbatim on round-trip.
282
+ yield { type: 'reasoning_signature', redacted: true, ...(block.data ? { encrypted: block.data } : {}) };
283
+ }
284
+ break;
285
+ }
286
+ case 'content_block_delta': {
287
+ const delta = event.delta;
288
+ if (!delta)
289
+ break;
290
+ if (delta.type === 'text_delta' && typeof delta.text === 'string') {
291
+ yield { type: 'text_delta', delta: delta.text };
292
+ }
293
+ else if (delta.type === 'thinking_delta' && typeof delta.thinking === 'string') {
294
+ yield { type: 'reasoning_delta', delta: delta.thinking };
295
+ }
296
+ else if (delta.type === 'signature_delta' && typeof delta.signature === 'string') {
297
+ pendingThinkingSig += delta.signature;
298
+ }
299
+ else if (delta.type === 'input_json_delta' && typeof delta.partial_json === 'string') {
300
+ const id = idOfBlock(event, blockIndexToId);
301
+ if (id) {
302
+ const t = pendingToolUses.get(id);
303
+ if (t) {
304
+ t.partial += delta.partial_json;
305
+ yield { type: 'tool_use_delta', id, partialInput: delta.partial_json };
306
+ }
307
+ }
308
+ }
309
+ break;
310
+ }
311
+ case 'content_block_stop': {
312
+ if (typeof event.index === 'number' && thinkingBlockIndices.has(event.index)) {
313
+ thinkingBlockIndices.delete(event.index);
314
+ if (pendingThinkingSig)
315
+ yield { type: 'reasoning_signature', signature: pendingThinkingSig };
316
+ pendingThinkingSig = '';
317
+ break;
318
+ }
319
+ const id = idOfBlock(event, blockIndexToId);
320
+ if (id) {
321
+ const t = pendingToolUses.get(id);
322
+ if (t) {
323
+ let parsed = {};
324
+ try {
325
+ parsed = t.partial ? JSON.parse(t.partial) : {};
326
+ }
327
+ catch {
328
+ parsed = { _rawPartial: t.partial };
329
+ }
330
+ yield { type: 'tool_use_end', id, input: parsed };
331
+ pendingToolUses.delete(id);
332
+ if (typeof event.index === 'number')
333
+ blockIndexToId.delete(event.index);
334
+ }
335
+ }
336
+ break;
337
+ }
338
+ case 'message_delta': {
339
+ if (event.delta?.stop_reason) {
340
+ stopReason = mapStopReason(event.delta.stop_reason);
341
+ }
342
+ if (event.usage) {
343
+ // Preserve cache fields captured at message_start — the delta
344
+ // usage only carries the final output_tokens count.
345
+ usage = {
346
+ ...usage,
347
+ inputTokens: usage?.inputTokens ?? 0,
348
+ outputTokens: event.usage.output_tokens ?? usage?.outputTokens ?? 0,
349
+ };
350
+ }
351
+ break;
352
+ }
353
+ case 'message_stop':
354
+ break;
355
+ }
356
+ }
357
+ }
358
+ catch (err) {
359
+ // A cancel surfaces as a thrown AbortError mid-await — report it as the
360
+ // clean terminal 'aborted' event. Every other error propagates so
361
+ // `stream()` can classify it (OAuth 401 → refresh+replay; else error).
362
+ if (signal?.aborted) {
363
+ yield { type: 'error', message: 'aborted', retryable: false };
364
+ return;
365
+ }
366
+ throw err;
367
+ }
368
+ yield { type: 'message_end', stopReason, usage };
369
+ }
370
+ async countTokens(req) {
371
+ const { system, messages } = toAnthropicMessages(req.messages);
372
+ const tools = req.tools && req.tools.length > 0 ? toAnthropicTools(req.tools) : undefined;
373
+ // Mirror stream(): in OAuth mode the request carries the identity preamble
374
+ // as an extra system block, and req.system (hook-injected extra system
375
+ // text) is appended as another — count both for a faithful estimate.
376
+ const parts = [this.oauth?.systemPreamble, system, req.system].filter((s) => typeof s === 'string' && s.length > 0);
377
+ const systemForCount = parts.length > 0 ? parts.join('\n\n') : undefined;
378
+ try {
379
+ // Mirror stream(): in OAuth mode refresh a near-expiry bearer before the
380
+ // request so a token we already knew was about to die doesn't 401 us
381
+ // straight into the (less accurate) text-estimate fallback below.
382
+ if (this.oauth)
383
+ await this.ensureFreshOauth();
384
+ const result = await this.client.messages.countTokens({
385
+ model: req.model || this.defaultModel,
386
+ ...(systemForCount !== undefined ? { system: systemForCount } : {}),
387
+ // NARROW cast: our hand-rolled message/tool shapes carry `media_type:
388
+ // string`, which the SDK narrows to a literal union it can't see we
389
+ // never violate. The method itself is fully typed (no `as unknown` on
390
+ // the resource anymore); only these two args need the structural cast.
391
+ messages: messages,
392
+ ...(tools !== undefined ? { tools: tools } : {}),
393
+ });
394
+ return result.input_tokens;
395
+ }
396
+ catch {
397
+ const blob = (systemForCount ?? '') +
398
+ messages.map((m) => JSON.stringify(m.content)).join('') +
399
+ JSON.stringify(tools ?? []);
400
+ return estimateTextTokens(blob);
401
+ }
402
+ }
403
+ }
404
+ function idOfBlock(event, blockIndexToId) {
405
+ if (typeof event.index === 'number') {
406
+ return blockIndexToId.get(event.index) ?? null;
407
+ }
408
+ // Fallback when `index` is missing (older SDKs / hand-rolled fakes): only
409
+ // unambiguous when exactly one tool_use is pending; otherwise refuse to
410
+ // guess and let the delta drop rather than misroute it.
411
+ if (blockIndexToId.size === 1) {
412
+ for (const id of blockIndexToId.values())
413
+ return id;
414
+ }
415
+ return null;
416
+ }
417
+ function mapStopReason(s) {
418
+ if (s === 'tool_use')
419
+ return 'tool_use';
420
+ if (s === 'max_tokens')
421
+ return 'max_tokens';
422
+ if (s === 'stop_sequence')
423
+ return 'stop_sequence';
424
+ if (s === 'end_turn')
425
+ return 'end_turn';
426
+ return 'error';
427
+ }
428
+ /**
429
+ * True when the SDK error is an HTTP 401. The Anthropic SDK throws
430
+ * `APIError` instances carrying a numeric `status`; an expired/revoked OAuth
431
+ * bearer is the only 401 we want to refresh-and-retry on.
432
+ */
433
+ function isUnauthorized(err) {
434
+ return typeof err?.status === 'number'
435
+ ? err.status === 401
436
+ : false;
437
+ }
438
+ //# sourceMappingURL=provider.js.map