@aigne/anthropic 0.7.2 → 0.8.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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.0](https://github.com/AIGNE-io/aigne-framework/compare/anthropic-v0.7.2...anthropic-v0.8.0) (2025-07-10)
4
+
5
+
6
+ ### Features
7
+
8
+ * **model:** reduce unnecessary LLM requests for structured output ([#241](https://github.com/AIGNE-io/aigne-framework/issues/241)) ([e28813c](https://github.com/AIGNE-io/aigne-framework/commit/e28813c021ed35c0251e198e2e007e2d746ab3d8))
9
+
10
+
11
+ ### Dependencies
12
+
13
+ * The following workspace dependencies were updated
14
+ * dependencies
15
+ * @aigne/core bumped to 1.33.0
16
+ * devDependencies
17
+ * @aigne/test-utils bumped to 0.5.5
18
+
3
19
  ## [0.7.2](https://github.com/AIGNE-io/aigne-framework/compare/anthropic-v0.7.1...anthropic-v0.7.2) (2025-07-09)
4
20
 
5
21
 
package/README.md CHANGED
@@ -6,8 +6,6 @@
6
6
  [![NPM Version](https://img.shields.io/npm/v/@aigne/anthropic)](https://www.npmjs.com/package/@aigne/anthropic)
7
7
  [![Elastic-2.0 licensed](https://img.shields.io/npm/l/@aigne/anthropic)](https://github.com/AIGNE-io/aigne-framework/blob/main/LICENSE.md)
8
8
 
9
- **English** | [中文](README.zh.md)
10
-
11
9
  AIGNE Anthropic SDK for integrating with Claude AI models within the [AIGNE Framework](https://github.com/AIGNE-io/aigne-framework).
12
10
 
13
11
  ## Introduction
@@ -105,12 +105,14 @@ export declare class AnthropicChatModel extends ChatModel {
105
105
  protected _client?: Anthropic;
106
106
  get client(): Anthropic;
107
107
  get modelOptions(): ChatModelOptions | undefined;
108
+ private getMaxTokens;
108
109
  /**
109
110
  * Process the input using Claude's chat model
110
111
  * @param input - The input to process
111
112
  * @returns The processed output from the model
112
113
  */
113
114
  process(input: ChatModelInput): PromiseOrValue<AgentProcessResult<ChatModelOutput>>;
115
+ private ajv;
114
116
  private _process;
115
117
  private extractResultFromAnthropicStream;
116
118
  private requestStructuredOutput;
@@ -6,10 +6,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.AnthropicChatModel = exports.claudeChatModelOptionsSchema = void 0;
7
7
  const core_1 = require("@aigne/core");
8
8
  const json_schema_js_1 = require("@aigne/core/utils/json-schema.js");
9
+ const logger_js_1 = require("@aigne/core/utils/logger.js");
9
10
  const model_utils_js_1 = require("@aigne/core/utils/model-utils.js");
10
11
  const stream_utils_js_1 = require("@aigne/core/utils/stream-utils.js");
11
12
  const type_utils_js_1 = require("@aigne/core/utils/type-utils.js");
12
13
  const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
14
+ const ajv_1 = require("ajv");
15
+ const jaison_1 = __importDefault(require("jaison"));
13
16
  const zod_1 = require("zod");
14
17
  const CHAT_MODEL_CLAUDE_DEFAULT_MODEL = "claude-3-7-sonnet-latest";
15
18
  /**
@@ -66,12 +69,28 @@ class AnthropicChatModel extends core_1.ChatModel {
66
69
  this._client ??= new sdk_1.default({
67
70
  apiKey,
68
71
  ...this.options?.clientOptions,
72
+ timeout: this.options?.clientOptions?.timeout ?? 600e3,
69
73
  });
70
74
  return this._client;
71
75
  }
72
76
  get modelOptions() {
73
77
  return this.options?.modelOptions;
74
78
  }
79
+ getMaxTokens(model) {
80
+ const matchers = [
81
+ [/claude-opus-4-/, 32000],
82
+ [/claude-sonnet-4-/, 64000],
83
+ [/claude-3-7-sonnet-/, 64000],
84
+ [/claude-3-5-sonnet-/, 8192],
85
+ [/claude-3-5-haiku-/, 8192],
86
+ ];
87
+ for (const [regex, maxTokens] of matchers) {
88
+ if (regex.test(model)) {
89
+ return maxTokens;
90
+ }
91
+ }
92
+ return 4096;
93
+ }
75
94
  /**
76
95
  * Process the input using Claude's chat model
77
96
  * @param input - The input to process
@@ -80,6 +99,7 @@ class AnthropicChatModel extends core_1.ChatModel {
80
99
  process(input) {
81
100
  return this._process(input);
82
101
  }
102
+ ajv = new ajv_1.Ajv();
83
103
  async _process(input) {
84
104
  const model = this.options?.model || CHAT_MODEL_CLAUDE_DEFAULT_MODEL;
85
105
  const disableParallelToolUse = input.modelOptions?.parallelToolCalls === false ||
@@ -89,10 +109,15 @@ class AnthropicChatModel extends core_1.ChatModel {
89
109
  temperature: input.modelOptions?.temperature ?? this.modelOptions?.temperature,
90
110
  top_p: input.modelOptions?.topP ?? this.modelOptions?.topP,
91
111
  // TODO: make dynamic based on model https://docs.anthropic.com/en/docs/about-claude/models/all-models
92
- max_tokens: /claude-3-[5|7]/.test(model) ? 8192 : 4096,
112
+ max_tokens: this.getMaxTokens(model),
93
113
  ...convertMessages(input),
94
114
  ...convertTools({ ...input, disableParallelToolUse }),
95
115
  };
116
+ // Claude does not support json_schema response and tool calls in the same request,
117
+ // so we need to handle the case where tools are not used and responseFormat is json
118
+ if (!input.tools?.length && input.responseFormat?.type === "json_schema") {
119
+ return this.requestStructuredOutput(body, input.responseFormat);
120
+ }
96
121
  const stream = this.client.messages.stream({
97
122
  ...body,
98
123
  stream: true,
@@ -101,20 +126,26 @@ class AnthropicChatModel extends core_1.ChatModel {
101
126
  return this.extractResultFromAnthropicStream(stream, true);
102
127
  }
103
128
  const result = await this.extractResultFromAnthropicStream(stream);
129
+ // Just return the result if it has tool calls
130
+ if (result.toolCalls?.length)
131
+ return result;
132
+ // Try to parse the text response as JSON
133
+ // If it matches the json_schema, return it as json
134
+ const json = safeParseJSON(result.text || "");
135
+ if (this.ajv.validate(input.responseFormat.jsonSchema.schema, json)) {
136
+ return { ...result, json, text: undefined };
137
+ }
138
+ logger_js_1.logger.warn(`AnthropicChatModel: Text response does not match JSON schema, trying to use tool to extract json `, { text: result.text });
104
139
  // Claude doesn't support json_schema response and tool calls in the same request,
105
140
  // so we need to make a separate request for json_schema response when the tool calls is empty
106
- if (!result.toolCalls?.length && input.responseFormat?.type === "json_schema") {
107
- const output = await this.requestStructuredOutput(body, input.responseFormat);
108
- return {
109
- ...output,
110
- // merge usage from both requests
111
- usage: (0, model_utils_js_1.mergeUsage)(result.usage, output.usage),
112
- };
113
- }
114
- return result;
141
+ const output = await this.requestStructuredOutput(body, input.responseFormat);
142
+ return {
143
+ ...output,
144
+ // merge usage from both requests
145
+ usage: (0, model_utils_js_1.mergeUsage)(result.usage, output.usage),
146
+ };
115
147
  }
116
148
  async extractResultFromAnthropicStream(stream, streaming) {
117
- const logs = [];
118
149
  const result = new ReadableStream({
119
150
  async start(controller) {
120
151
  try {
@@ -136,7 +167,6 @@ class AnthropicChatModel extends core_1.ChatModel {
136
167
  if (chunk.type === "message_delta" && usage) {
137
168
  usage.outputTokens = chunk.usage.output_tokens;
138
169
  }
139
- logs.push(JSON.stringify(chunk));
140
170
  // handle streaming text
141
171
  if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
142
172
  controller.enqueue({
@@ -223,7 +253,7 @@ class AnthropicChatModel extends core_1.ChatModel {
223
253
  }
224
254
  }
225
255
  exports.AnthropicChatModel = AnthropicChatModel;
226
- function convertMessages({ messages, responseFormat }) {
256
+ function convertMessages({ messages, responseFormat, tools }) {
227
257
  const systemMessages = [];
228
258
  const msgs = [];
229
259
  for (const msg of messages) {
@@ -273,7 +303,9 @@ function convertMessages({ messages, responseFormat }) {
273
303
  }
274
304
  }
275
305
  }
276
- if (responseFormat?.type === "json_schema") {
306
+ // If there are tools and responseFormat is json_schema, we need to add a system message
307
+ // to inform the model about the expected json schema, then trying to parse the response as json
308
+ if (tools?.length && responseFormat?.type === "json_schema") {
277
309
  systemMessages.push(`You should provide a json response with schema: ${JSON.stringify(responseFormat.jsonSchema.schema)}`);
278
310
  }
279
311
  const system = systemMessages.join("\n").trim() || undefined;
@@ -329,3 +361,13 @@ function convertTools({ tools, toolChoice, disableParallelToolUse, }) {
329
361
  tool_choice: choice,
330
362
  };
331
363
  }
364
+ function safeParseJSON(text) {
365
+ if (!text)
366
+ return null;
367
+ try {
368
+ return (0, jaison_1.default)(text);
369
+ }
370
+ catch {
371
+ return null;
372
+ }
373
+ }
@@ -105,12 +105,14 @@ export declare class AnthropicChatModel extends ChatModel {
105
105
  protected _client?: Anthropic;
106
106
  get client(): Anthropic;
107
107
  get modelOptions(): ChatModelOptions | undefined;
108
+ private getMaxTokens;
108
109
  /**
109
110
  * Process the input using Claude's chat model
110
111
  * @param input - The input to process
111
112
  * @returns The processed output from the model
112
113
  */
113
114
  process(input: ChatModelInput): PromiseOrValue<AgentProcessResult<ChatModelOutput>>;
115
+ private ajv;
114
116
  private _process;
115
117
  private extractResultFromAnthropicStream;
116
118
  private requestStructuredOutput;
@@ -105,12 +105,14 @@ export declare class AnthropicChatModel extends ChatModel {
105
105
  protected _client?: Anthropic;
106
106
  get client(): Anthropic;
107
107
  get modelOptions(): ChatModelOptions | undefined;
108
+ private getMaxTokens;
108
109
  /**
109
110
  * Process the input using Claude's chat model
110
111
  * @param input - The input to process
111
112
  * @returns The processed output from the model
112
113
  */
113
114
  process(input: ChatModelInput): PromiseOrValue<AgentProcessResult<ChatModelOutput>>;
115
+ private ajv;
114
116
  private _process;
115
117
  private extractResultFromAnthropicStream;
116
118
  private requestStructuredOutput;
@@ -1,9 +1,12 @@
1
1
  import { ChatModel, } from "@aigne/core";
2
2
  import { parseJSON } from "@aigne/core/utils/json-schema.js";
3
+ import { logger } from "@aigne/core/utils/logger.js";
3
4
  import { mergeUsage } from "@aigne/core/utils/model-utils.js";
4
5
  import { agentResponseStreamToObject } from "@aigne/core/utils/stream-utils.js";
5
6
  import { checkArguments, isEmpty, isNonNullable, } from "@aigne/core/utils/type-utils.js";
6
7
  import Anthropic from "@anthropic-ai/sdk";
8
+ import { Ajv } from "ajv";
9
+ import jaison from "jaison";
7
10
  import { z } from "zod";
8
11
  const CHAT_MODEL_CLAUDE_DEFAULT_MODEL = "claude-3-7-sonnet-latest";
9
12
  /**
@@ -60,12 +63,28 @@ export class AnthropicChatModel extends ChatModel {
60
63
  this._client ??= new Anthropic({
61
64
  apiKey,
62
65
  ...this.options?.clientOptions,
66
+ timeout: this.options?.clientOptions?.timeout ?? 600e3,
63
67
  });
64
68
  return this._client;
65
69
  }
66
70
  get modelOptions() {
67
71
  return this.options?.modelOptions;
68
72
  }
73
+ getMaxTokens(model) {
74
+ const matchers = [
75
+ [/claude-opus-4-/, 32000],
76
+ [/claude-sonnet-4-/, 64000],
77
+ [/claude-3-7-sonnet-/, 64000],
78
+ [/claude-3-5-sonnet-/, 8192],
79
+ [/claude-3-5-haiku-/, 8192],
80
+ ];
81
+ for (const [regex, maxTokens] of matchers) {
82
+ if (regex.test(model)) {
83
+ return maxTokens;
84
+ }
85
+ }
86
+ return 4096;
87
+ }
69
88
  /**
70
89
  * Process the input using Claude's chat model
71
90
  * @param input - The input to process
@@ -74,6 +93,7 @@ export class AnthropicChatModel extends ChatModel {
74
93
  process(input) {
75
94
  return this._process(input);
76
95
  }
96
+ ajv = new Ajv();
77
97
  async _process(input) {
78
98
  const model = this.options?.model || CHAT_MODEL_CLAUDE_DEFAULT_MODEL;
79
99
  const disableParallelToolUse = input.modelOptions?.parallelToolCalls === false ||
@@ -83,10 +103,15 @@ export class AnthropicChatModel extends ChatModel {
83
103
  temperature: input.modelOptions?.temperature ?? this.modelOptions?.temperature,
84
104
  top_p: input.modelOptions?.topP ?? this.modelOptions?.topP,
85
105
  // TODO: make dynamic based on model https://docs.anthropic.com/en/docs/about-claude/models/all-models
86
- max_tokens: /claude-3-[5|7]/.test(model) ? 8192 : 4096,
106
+ max_tokens: this.getMaxTokens(model),
87
107
  ...convertMessages(input),
88
108
  ...convertTools({ ...input, disableParallelToolUse }),
89
109
  };
110
+ // Claude does not support json_schema response and tool calls in the same request,
111
+ // so we need to handle the case where tools are not used and responseFormat is json
112
+ if (!input.tools?.length && input.responseFormat?.type === "json_schema") {
113
+ return this.requestStructuredOutput(body, input.responseFormat);
114
+ }
90
115
  const stream = this.client.messages.stream({
91
116
  ...body,
92
117
  stream: true,
@@ -95,20 +120,26 @@ export class AnthropicChatModel extends ChatModel {
95
120
  return this.extractResultFromAnthropicStream(stream, true);
96
121
  }
97
122
  const result = await this.extractResultFromAnthropicStream(stream);
123
+ // Just return the result if it has tool calls
124
+ if (result.toolCalls?.length)
125
+ return result;
126
+ // Try to parse the text response as JSON
127
+ // If it matches the json_schema, return it as json
128
+ const json = safeParseJSON(result.text || "");
129
+ if (this.ajv.validate(input.responseFormat.jsonSchema.schema, json)) {
130
+ return { ...result, json, text: undefined };
131
+ }
132
+ logger.warn(`AnthropicChatModel: Text response does not match JSON schema, trying to use tool to extract json `, { text: result.text });
98
133
  // Claude doesn't support json_schema response and tool calls in the same request,
99
134
  // so we need to make a separate request for json_schema response when the tool calls is empty
100
- if (!result.toolCalls?.length && input.responseFormat?.type === "json_schema") {
101
- const output = await this.requestStructuredOutput(body, input.responseFormat);
102
- return {
103
- ...output,
104
- // merge usage from both requests
105
- usage: mergeUsage(result.usage, output.usage),
106
- };
107
- }
108
- return result;
135
+ const output = await this.requestStructuredOutput(body, input.responseFormat);
136
+ return {
137
+ ...output,
138
+ // merge usage from both requests
139
+ usage: mergeUsage(result.usage, output.usage),
140
+ };
109
141
  }
110
142
  async extractResultFromAnthropicStream(stream, streaming) {
111
- const logs = [];
112
143
  const result = new ReadableStream({
113
144
  async start(controller) {
114
145
  try {
@@ -130,7 +161,6 @@ export class AnthropicChatModel extends ChatModel {
130
161
  if (chunk.type === "message_delta" && usage) {
131
162
  usage.outputTokens = chunk.usage.output_tokens;
132
163
  }
133
- logs.push(JSON.stringify(chunk));
134
164
  // handle streaming text
135
165
  if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
136
166
  controller.enqueue({
@@ -216,7 +246,7 @@ export class AnthropicChatModel extends ChatModel {
216
246
  };
217
247
  }
218
248
  }
219
- function convertMessages({ messages, responseFormat }) {
249
+ function convertMessages({ messages, responseFormat, tools }) {
220
250
  const systemMessages = [];
221
251
  const msgs = [];
222
252
  for (const msg of messages) {
@@ -266,7 +296,9 @@ function convertMessages({ messages, responseFormat }) {
266
296
  }
267
297
  }
268
298
  }
269
- if (responseFormat?.type === "json_schema") {
299
+ // If there are tools and responseFormat is json_schema, we need to add a system message
300
+ // to inform the model about the expected json schema, then trying to parse the response as json
301
+ if (tools?.length && responseFormat?.type === "json_schema") {
270
302
  systemMessages.push(`You should provide a json response with schema: ${JSON.stringify(responseFormat.jsonSchema.schema)}`);
271
303
  }
272
304
  const system = systemMessages.join("\n").trim() || undefined;
@@ -322,3 +354,13 @@ function convertTools({ tools, toolChoice, disableParallelToolUse, }) {
322
354
  tool_choice: choice,
323
355
  };
324
356
  }
357
+ function safeParseJSON(text) {
358
+ if (!text)
359
+ return null;
360
+ try {
361
+ return jaison(text);
362
+ }
363
+ catch {
364
+ return null;
365
+ }
366
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/anthropic",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "AIGNE Anthropic SDK for integrating with Claude AI models",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -33,8 +33,10 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@anthropic-ai/sdk": "^0.55.1",
36
+ "ajv": "^8.17.1",
37
+ "jaison": "^2.0.2",
36
38
  "zod": "^3.25.67",
37
- "@aigne/core": "^1.32.2"
39
+ "@aigne/core": "^1.33.0"
38
40
  },
39
41
  "devDependencies": {
40
42
  "@types/bun": "^1.2.17",
@@ -42,7 +44,7 @@
42
44
  "npm-run-all": "^4.1.5",
43
45
  "rimraf": "^6.0.1",
44
46
  "typescript": "^5.8.3",
45
- "@aigne/test-utils": "^0.5.4"
47
+ "@aigne/test-utils": "^0.5.5"
46
48
  },
47
49
  "scripts": {
48
50
  "lint": "tsc --noEmit",
package/README.zh.md DELETED
@@ -1,116 +0,0 @@
1
- # @aigne/anthropic
2
-
3
- [![GitHub star chart](https://img.shields.io/github/stars/AIGNE-io/aigne-framework?style=flat-square)](https://star-history.com/#AIGNE-io/aigne-framework)
4
- [![Open Issues](https://img.shields.io/github/issues-raw/AIGNE-io/aigne-framework?style=flat-square)](https://github.com/AIGNE-io/aigne-framework/issues)
5
- [![codecov](https://codecov.io/gh/AIGNE-io/aigne-framework/graph/badge.svg?token=DO07834RQL)](https://codecov.io/gh/AIGNE-io/aigne-framework)
6
- [![NPM Version](https://img.shields.io/npm/v/@aigne/anthropic)](https://www.npmjs.com/package/@aigne/anthropic)
7
- [![Elastic-2.0 licensed](https://img.shields.io/npm/l/@aigne/anthropic)](https://github.com/AIGNE-io/aigne-framework/blob/main/LICENSE.md)
8
-
9
- [English](README.md) | **中文**
10
-
11
- AIGNE Anthropic SDK,用于在 [AIGNE 框架](https://github.com/AIGNE-io/aigne-framework) 中集成 Anthropic 的 Claude AI 模型。
12
-
13
- ## 简介
14
-
15
- `@aigne/anthropic` 提供了 AIGNE 框架与 Anthropic 的 Claude 语言模型和 API 之间的无缝集成。该包使开发者能够在 AIGNE 应用程序中轻松利用 Anthropic 的 Claude 模型,同时提供框架内一致的接口,充分发挥 Claude 先进的 AI 能力。
16
-
17
- ## 特性
18
-
19
- * **Anthropic API 集成**:使用官方 SDK 直接连接到 Anthropic 的 API 服务
20
- * **聊天完成**:支持 Claude 的聊天完成 API 和所有可用模型
21
- * **工具调用**:内置支持 Claude 的工具调用功能
22
- * **流式响应**:支持流式响应,提供更高响应性的应用程序体验
23
- * **类型安全**:为所有 API 和模型提供全面的 TypeScript 类型定义
24
- * **一致接口**:兼容 AIGNE 框架的模型接口
25
- * **错误处理**:健壮的错误处理和重试机制
26
- * **完整配置**:丰富的配置选项用于微调行为
27
-
28
- ## 安装
29
-
30
- ### 使用 npm
31
-
32
- ```bash
33
- npm install @aigne/anthropic @aigne/core
34
- ```
35
-
36
- ### 使用 yarn
37
-
38
- ```bash
39
- yarn add @aigne/anthropic @aigne/core
40
- ```
41
-
42
- ### 使用 pnpm
43
-
44
- ```bash
45
- pnpm add @aigne/anthropic @aigne/core
46
- ```
47
-
48
- ## 基本用法
49
-
50
- ```typescript file="test/anthropic-chat-model.test.ts" region="example-anthropic-chat-model"
51
- import { AnthropicChatModel } from "@aigne/anthropic";
52
-
53
- const model = new AnthropicChatModel({
54
- // Provide API key directly or use environment variable ANTHROPIC_API_KEY or CLAUDE_API_KEY
55
- apiKey: "your-api-key", // Optional if set in env variables
56
- // Specify Claude model version (defaults to 'claude-3-7-sonnet-latest')
57
- model: "claude-3-haiku-20240307",
58
- // Configure model behavior
59
- modelOptions: {
60
- temperature: 0.7,
61
- },
62
- });
63
-
64
- const result = await model.invoke({
65
- messages: [{ role: "user", content: "Tell me about yourself" }],
66
- });
67
-
68
- console.log(result);
69
- /* Output:
70
- {
71
- text: "I'm Claude, an AI assistant created by Anthropic. How can I help you today?",
72
- model: "claude-3-haiku-20240307",
73
- usage: {
74
- inputTokens: 8,
75
- outputTokens: 15
76
- }
77
- }
78
- */
79
- ```
80
-
81
- ## 流式响应
82
-
83
- ```typescript file="test/anthropic-chat-model.test.ts" region="example-anthropic-chat-model-streaming-async-generator"
84
- import { AnthropicChatModel } from "@aigne/anthropic";
85
- import { isAgentResponseDelta } from "@aigne/core";
86
-
87
- const model = new AnthropicChatModel({
88
- apiKey: "your-api-key",
89
- model: "claude-3-haiku-20240307",
90
- });
91
-
92
- const stream = await model.invoke(
93
- {
94
- messages: [{ role: "user", content: "Tell me about yourself" }],
95
- },
96
- { streaming: true },
97
- );
98
-
99
- let fullText = "";
100
- const json = {};
101
-
102
- for await (const chunk of stream) {
103
- if (isAgentResponseDelta(chunk)) {
104
- const text = chunk.delta.text?.text;
105
- if (text) fullText += text;
106
- if (chunk.delta.json) Object.assign(json, chunk.delta.json);
107
- }
108
- }
109
-
110
- console.log(fullText); // Output: "I'm Claude, an AI assistant created by Anthropic. How can I help you today?"
111
- console.log(json); // { model: "claude-3-haiku-20240307", usage: { inputTokens: 8, outputTokens: 15 } }
112
- ```
113
-
114
- ## 许可证
115
-
116
- Elastic-2.0