purra-anthropic 0.5.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 Lybrands
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,53 @@
1
+ # purra-anthropic · TypeScript
2
+
3
+ English | [简体中文](README.zh-CN.md)
4
+
5
+ Anthropic Messages gateway for PurrA using the official SDK. Supports text,
6
+ function tools, streaming, cancellation, and token usage. Requires Node.js 22+ (ESM).
7
+
8
+ ## Install
9
+
10
+ Follow [source installation](../../README.md#source-installation), selecting
11
+ `integrations/anthropic/typescript`.
12
+
13
+ ## Configure
14
+
15
+ Supply the selected model ID and its verified capability snapshot:
16
+
17
+ ```ts
18
+ import { Agent, type ModelCapabilitySnapshot } from "purra";
19
+ import { AnthropicMessagesGateway } from "purra-anthropic";
20
+
21
+ function createAgent(model: string, capabilities: ModelCapabilitySnapshot) {
22
+ const gateway = new AnthropicMessagesGateway({
23
+ model,
24
+ capabilities,
25
+ timeoutMs: 60_000,
26
+ });
27
+ return new Agent({ model: gateway });
28
+ }
29
+ ```
30
+
31
+ Pass an SDK client through `client` to configure credentials or transport.
32
+ Without one, the gateway creates a client using the SDK's environment configuration.
33
+
34
+ ## Model options
35
+
36
+ The constructor accepts `thinking`, `outputConfig`, `temperature`, `topP`, and
37
+ `topK`. Choose options supported by the selected model. For example,
38
+ `thinking: { type: "adaptive" }` requires a model that supports adaptive thinking.
39
+ Manual thinking budgets must be integers, at least 1024, and below Core's resolved
40
+ total-generation limit.
41
+
42
+ ## Behavior
43
+
44
+ SDK retries are disabled; Core controls retry and budget accounting. The gateway
45
+ applies Core's total-generation limit and combines system/developer instructions into the
46
+ system field. Tool choice is automatic when tools are available.
47
+
48
+ Signed thinking and redacted blocks are retained as private continuation data.
49
+ Keep them with the unchanged assistant message when continuing on the same model;
50
+ they are excluded from public output. Input usage includes cache writes and reads.
51
+
52
+ Multimodal content, hosted tools, and beta APIs are not supported.
53
+ Other vendors require application adapters.
@@ -0,0 +1,48 @@
1
+ # purra-anthropic · TypeScript
2
+
3
+ [English](README.md) | 简体中文
4
+
5
+ 使用官方 SDK 将 Anthropic Messages 接入 PurrA,支持文本、函数工具、流式输出、取消和 Token 用量。
6
+ 要求 Node.js 22+(ESM)。
7
+
8
+ ## 安装
9
+
10
+ 按照[源码安装说明](../../README.zh-CN.md#从源码安装)操作,选择
11
+ `integrations/anthropic/typescript`。
12
+
13
+ ## 配置
14
+
15
+ 传入所选模型 ID 和已核实的模型能力快照:
16
+
17
+ ```ts
18
+ import { Agent, type ModelCapabilitySnapshot } from "purra";
19
+ import { AnthropicMessagesGateway } from "purra-anthropic";
20
+
21
+ function createAgent(model: string, capabilities: ModelCapabilitySnapshot) {
22
+ const gateway = new AnthropicMessagesGateway({
23
+ model,
24
+ capabilities,
25
+ timeoutMs: 60_000,
26
+ });
27
+ return new Agent({ model: gateway });
28
+ }
29
+ ```
30
+
31
+ 通过 `client` 传入 SDK 客户端,可自定义凭据与传输配置。
32
+ 省略时,网关使用 SDK 的环境配置创建客户端。
33
+
34
+ ## 模型参数
35
+
36
+ 构造函数接受 `thinking`、`outputConfig`、`temperature`、`topP` 和 `topK`。
37
+ 根据所选模型支持的能力设置参数。例如,`thinking: { type: "adaptive" }`
38
+ 要求模型支持自适应 thinking。手动推理预算必须是至少 1024 的整数,且小于 Core 确定的输出上限。
39
+
40
+ ## 行为
41
+
42
+ SDK 自动重试已关闭,重试与预算核算由 Core 控制。网关使用 Core 确定的输出上限,
43
+ 将 system/developer 指令合并到 system 字段,有工具可用时采用自动工具选择。
44
+
45
+ 带签名的 thinking 和 redacted 块保留为私有续接数据。使用同一模型继续对话时,
46
+ 应与未改动的 assistant 消息一同保留;它们不会进入公开输出。输入用量包括缓存写入和读取。
47
+
48
+ 不支持多模态内容、服务端托管工具和 beta API。其他厂商由应用提供适配。
@@ -0,0 +1,22 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ import type { MessageCreateParamsNonStreaming } from "@anthropic-ai/sdk/resources/messages";
3
+ import type { ModelCapabilitySnapshot, ModelGateway, ModelRequest, ModelStream, ModelTurn } from "purra";
4
+ export interface AnthropicMessagesOptions {
5
+ readonly client?: Anthropic;
6
+ readonly model: string;
7
+ readonly capabilities: ModelCapabilitySnapshot;
8
+ readonly timeoutMs?: number;
9
+ readonly thinking?: MessageCreateParamsNonStreaming["thinking"];
10
+ readonly outputConfig?: MessageCreateParamsNonStreaming["output_config"];
11
+ readonly temperature?: number;
12
+ readonly topP?: number;
13
+ readonly topK?: number;
14
+ }
15
+ /** Official SDK transport; Core owns execution, retries and cumulative budgets. */
16
+ export declare class AnthropicMessagesGateway implements ModelGateway {
17
+ #private;
18
+ readonly capabilities: ModelCapabilitySnapshot;
19
+ constructor(options: AnthropicMessagesOptions);
20
+ invoke(request: ModelRequest, signal?: AbortSignal): Promise<ModelTurn>;
21
+ stream(request: ModelRequest, signal?: AbortSignal): Promise<ModelStream>;
22
+ }
package/dist/index.js ADDED
@@ -0,0 +1,251 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ import { AgentCanceledError, AgentError } from "purra";
3
+ const replayKey = "anthropic_message";
4
+ const maxContinuationChars = 1_000_000;
5
+ function copy(value) { return JSON.parse(JSON.stringify(value)); }
6
+ function text(value) {
7
+ if (value === null)
8
+ return "";
9
+ if (typeof value !== "string")
10
+ throw new TypeError("Anthropic adapter accepts text messages only");
11
+ return value;
12
+ }
13
+ function projection(blocks) {
14
+ let content = "";
15
+ const toolCalls = [];
16
+ for (const block of blocks) {
17
+ if (block.type === "text")
18
+ content += text(block.text);
19
+ else if (block.type === "tool_use") {
20
+ if (!block.input || typeof block.input !== "object" || Array.isArray(block.input))
21
+ throw new TypeError("Tool input must be an object");
22
+ toolCalls.push({ id: block.id, name: block.name, arguments: copy(block.input) });
23
+ }
24
+ else if (block.type === "thinking") {
25
+ text(block.thinking);
26
+ if (typeof block.signature !== "string" || !block.signature)
27
+ throw new TypeError("Thinking continuation requires a signature");
28
+ }
29
+ else if (block.type === "redacted_thinking") {
30
+ if (typeof block.data !== "string" || !block.data)
31
+ throw new TypeError("Redacted thinking requires opaque data");
32
+ }
33
+ else
34
+ throw new TypeError("Unsupported Anthropic content block");
35
+ }
36
+ return { content, toolCalls };
37
+ }
38
+ function input(messages, model) {
39
+ const system = [];
40
+ const rows = [];
41
+ for (const m of messages) {
42
+ const content = text(m.content);
43
+ if (m.toolCalls?.length && m.role !== "assistant")
44
+ throw new TypeError("Only assistant messages may carry tool calls");
45
+ if (m.role === "system" || m.role === "developer") {
46
+ if (content)
47
+ system.push({ type: "text", text: content });
48
+ continue;
49
+ }
50
+ let blocks;
51
+ const role = m.role === "assistant" ? "assistant" : "user";
52
+ if (m.role === "tool") {
53
+ if (!m.toolCallId)
54
+ throw new TypeError("Tool output requires a call id");
55
+ blocks = [{ type: "tool_result", tool_use_id: m.toolCallId, content }];
56
+ }
57
+ else if (m.role === "assistant") {
58
+ const replay = m.providerData?.[replayKey];
59
+ if (replay != null) {
60
+ if (typeof replay !== "object" || Array.isArray(replay) || !("model" in replay) || !("content" in replay) || replay.model !== model || !Array.isArray(replay.content))
61
+ throw new TypeError("Anthropic continuation belongs to a different model");
62
+ if (JSON.stringify(replay.content).length > maxContinuationChars)
63
+ throw new TypeError("Anthropic continuation exceeds limit");
64
+ const original = copy(replay.content);
65
+ const visible = projection(original);
66
+ if (visible.content !== content || JSON.stringify(visible.toolCalls) !== JSON.stringify(m.toolCalls ?? []))
67
+ throw new TypeError("Anthropic continuation does not match assistant message");
68
+ blocks = original;
69
+ }
70
+ else {
71
+ blocks = content ? [{ type: "text", text: content }] : [];
72
+ for (const call of m.toolCalls ?? []) {
73
+ if (!call.arguments || typeof call.arguments !== "object" || Array.isArray(call.arguments))
74
+ throw new TypeError("Tool input must be an object");
75
+ blocks.push({ type: "tool_use", id: call.id, name: call.name, input: copy(call.arguments) });
76
+ }
77
+ }
78
+ }
79
+ else
80
+ blocks = [{ type: "text", text: content }];
81
+ if (!blocks.length)
82
+ throw new TypeError("Anthropic message cannot be empty");
83
+ const previous = rows.at(-1);
84
+ if (previous?.role === role)
85
+ previous.content.push(...blocks);
86
+ else
87
+ rows.push({ role, content: blocks });
88
+ }
89
+ return { messages: rows, system };
90
+ }
91
+ function attributes(content, model) {
92
+ if (!content.some(b => b.type === "thinking" || b.type === "redacted_thinking"))
93
+ return {};
94
+ if (JSON.stringify(content).length > maxContinuationChars)
95
+ throw new TypeError("Anthropic continuation exceeds limit");
96
+ return { [replayKey]: { model, content: copy(content) } };
97
+ }
98
+ function usage(u) {
99
+ if (u?.input_tokens == null || u.output_tokens == null)
100
+ return undefined;
101
+ const cached = u.cache_read_input_tokens ?? 0;
102
+ const inputs = u.input_tokens + cached + (u.cache_creation_input_tokens ?? 0);
103
+ return { inputTokens: inputs, generationTokens: u.output_tokens, totalTokens: inputs + u.output_tokens, cachedInputTokens: cached };
104
+ }
105
+ function finish(reason) {
106
+ if (reason === "end_turn" || reason === "stop_sequence")
107
+ return "stop";
108
+ if (reason === "tool_use")
109
+ return "tool_calls";
110
+ if (reason === "max_tokens" || reason === "model_context_window_exceeded")
111
+ return "length";
112
+ if (reason === "refusal")
113
+ return "filtered";
114
+ throw new AgentError("invalid_model_response", "Unsupported Anthropic stop reason");
115
+ }
116
+ function failed(error, signal) {
117
+ if (signal?.aborted)
118
+ throw new AgentCanceledError();
119
+ if (error instanceof Anthropic.APIError)
120
+ throw new AgentError(error.status === undefined ? "anthropic_transport_error" : `anthropic_http_${error.status}`, "Anthropic request failed");
121
+ if (error instanceof AgentError)
122
+ throw error;
123
+ throw new AgentError("invalid_model_response", "Invalid Anthropic response");
124
+ }
125
+ /** Official SDK transport; Core owns execution, retries and cumulative budgets. */
126
+ export class AnthropicMessagesGateway {
127
+ capabilities;
128
+ #client;
129
+ #options;
130
+ constructor(options) {
131
+ if (!options.model.trim())
132
+ throw new TypeError("Model name is required");
133
+ if (!Number.isFinite(options.timeoutMs ?? 60000) || (options.timeoutMs ?? 60000) <= 0)
134
+ throw new TypeError("Timeout must be positive and finite");
135
+ this.capabilities = options.capabilities;
136
+ this.#options = { ...options, ...(options.thinking === undefined ? {} : { thinking: copy(options.thinking) }),
137
+ ...(options.outputConfig === undefined ? {} : { outputConfig: copy(options.outputConfig) }) };
138
+ this.#client = (options.client ?? new Anthropic()).withOptions({ maxRetries: 0, timeout: options.timeoutMs ?? 60000 });
139
+ }
140
+ #request(request) {
141
+ if (!request.outputBudget)
142
+ throw new TypeError("Anthropic gateway requires a resolved generation budget");
143
+ const o = this.#options;
144
+ if (o.thinking?.type === "enabled" && (!Number.isInteger(o.thinking.budget_tokens) || o.thinking.budget_tokens < 1024 || o.thinking.budget_tokens >= request.outputBudget.maxGenerationTokens))
145
+ throw new TypeError("Thinking budget must be at least 1024 and below the generation budget");
146
+ const rows = input(request.messages, o.model);
147
+ return { model: o.model, messages: rows.messages, max_tokens: request.outputBudget.maxGenerationTokens,
148
+ ...(rows.system.length ? { system: rows.system } : {}),
149
+ ...(o.thinking === undefined ? {} : { thinking: o.thinking }),
150
+ ...(o.outputConfig === undefined ? {} : { output_config: o.outputConfig }),
151
+ ...(o.temperature === undefined ? {} : { temperature: o.temperature }),
152
+ ...(o.topP === undefined ? {} : { top_p: o.topP }),
153
+ ...(o.topK === undefined ? {} : { top_k: o.topK }),
154
+ ...(request.tools.length ? { tools: request.tools.map(t => {
155
+ if (t.inputSchema.type !== "object")
156
+ throw new TypeError("Tool schema must describe an object");
157
+ return { name: t.name, description: t.description, input_schema: copy(t.inputSchema) };
158
+ }), tool_choice: { type: "auto" } } : {}),
159
+ };
160
+ }
161
+ async invoke(request, signal) {
162
+ const params = this.#request(request);
163
+ if (signal?.aborted)
164
+ throw new AgentCanceledError();
165
+ try {
166
+ const response = await this.#client.messages.create(params, { signal });
167
+ const tokenUsage = usage(response.usage);
168
+ return { message: { role: "assistant", ...projection(response.content), providerData: attributes(response.content, params.model) },
169
+ finishReason: finish(response.stop_reason), appliedGenerationLimit: request.outputBudget.maxGenerationTokens,
170
+ ...(tokenUsage === undefined ? {} : { usage: tokenUsage }) };
171
+ }
172
+ catch (error) {
173
+ return failed(error, signal);
174
+ }
175
+ }
176
+ async stream(request, signal) {
177
+ const params = this.#request(request);
178
+ const client = this.#client;
179
+ let consumed = false;
180
+ async function* chunks() {
181
+ if (consumed)
182
+ throw new AgentError("invalid_model_response", "Anthropic stream has already been consumed");
183
+ consumed = true;
184
+ if (signal?.aborted)
185
+ throw new AgentCanceledError();
186
+ let stream;
187
+ let terminal = false;
188
+ let privateChars = 0;
189
+ const emptyTools = new Set();
190
+ try {
191
+ stream = client.messages.stream(params, { signal });
192
+ for await (const event of stream) {
193
+ if (event.type === "content_block_start") {
194
+ const b = event.content_block;
195
+ if (b.type === "tool_use") {
196
+ emptyTools.add(event.index);
197
+ yield { toolCallDeltas: [{ index: event.index, id: b.id, name: b.name, type: "function" }] };
198
+ }
199
+ else if (b.type === "text" && b.text)
200
+ yield { contentDelta: b.text };
201
+ else if (b.type === "thinking" || b.type === "redacted_thinking") {
202
+ privateChars += JSON.stringify(b).length;
203
+ yield { type: "activity", kind: "working" };
204
+ }
205
+ else if (b.type !== "text")
206
+ throw new TypeError("Unsupported Anthropic content block");
207
+ }
208
+ else if (event.type === "content_block_delta") {
209
+ const d = event.delta;
210
+ if (d.type === "text_delta")
211
+ yield { contentDelta: d.text };
212
+ else if (d.type === "input_json_delta") {
213
+ emptyTools.delete(event.index);
214
+ yield { toolCallDeltas: [{ index: event.index, argumentsFragment: d.partial_json }] };
215
+ }
216
+ else if (d.type === "thinking_delta" || d.type === "signature_delta") {
217
+ privateChars += d.type === "thinking_delta" ? d.thinking.length : d.signature.length;
218
+ yield { type: "activity", kind: "working" };
219
+ }
220
+ else
221
+ throw new TypeError("Unsupported Anthropic content delta");
222
+ }
223
+ else if (event.type === "content_block_stop" && emptyTools.has(event.index)) {
224
+ emptyTools.delete(event.index);
225
+ yield { toolCallDeltas: [{ index: event.index, argumentsFragment: "{}" }] };
226
+ }
227
+ else if (event.type === "message_stop")
228
+ terminal = true;
229
+ else
230
+ yield { type: "activity", kind: "transport" };
231
+ if (privateChars > maxContinuationChars)
232
+ throw new TypeError("Anthropic continuation exceeds limit");
233
+ }
234
+ if (!terminal)
235
+ throw new AgentError("upstream_stream_interrupted", "Anthropic stream ended without message_stop");
236
+ const response = await stream.finalMessage();
237
+ projection(response.content);
238
+ const tokenUsage = usage(response.usage);
239
+ yield { finishReason: finish(response.stop_reason), providerData: attributes(response.content, params.model),
240
+ ...(tokenUsage === undefined ? {} : { usage: tokenUsage }) };
241
+ }
242
+ catch (error) {
243
+ failed(error, signal);
244
+ }
245
+ finally {
246
+ stream?.abort();
247
+ }
248
+ }
249
+ return { appliedGenerationLimit: request.outputBudget.maxGenerationTokens, activitySupport: "working", [Symbol.asyncIterator]: chunks };
250
+ }
251
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "purra-anthropic",
3
+ "version": "0.5.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "description": "Official Anthropic Messages SDK adapter for PurrA",
7
+ "files": [
8
+ "dist",
9
+ "README.md",
10
+ "README.zh-CN.md",
11
+ "LICENSE"
12
+ ],
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "dependencies": {
20
+ "@anthropic-ai/sdk": "0.123.0"
21
+ },
22
+ "peerDependencies": {
23
+ "purra": "0.5.0"
24
+ },
25
+ "devDependencies": {
26
+ "purra": "file:../../../typescript",
27
+ "typescript": "7.0.2"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc -p tsconfig.json",
31
+ "test": "node --test test/*.test.mjs",
32
+ "check": "npm run build && npm test",
33
+ "prepack": "npm run build"
34
+ }
35
+ }