@aigne/anthropic 0.14.16-beta.2 → 0.14.16-beta.20

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,232 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.14.16-beta.20](https://github.com/AIGNE-io/aigne-framework/compare/anthropic-v0.14.16-beta.19...anthropic-v0.14.16-beta.20) (2026-01-13)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **anthropic:** handle null content blocks in streaming responses ([9fefd6f](https://github.com/AIGNE-io/aigne-framework/commit/9fefd6fcca58bb8a59616560f86a04a0015f6aca))
9
+
10
+ ## [0.14.16-beta.19](https://github.com/AIGNE-io/aigne-framework/compare/anthropic-v0.14.16-beta.18...anthropic-v0.14.16-beta.19) (2026-01-13)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **anthropic:** simplify structured output with output tool pattern ([#899](https://github.com/AIGNE-io/aigne-framework/issues/899)) ([a6033c8](https://github.com/AIGNE-io/aigne-framework/commit/a6033c8a9ccf5e8ff6bcb14bc68c43a990ce2fa2))
16
+ * **anthropic:** update structured output tool name to generate_json ([897e94d](https://github.com/AIGNE-io/aigne-framework/commit/897e94d82a1bbfa46ae13038a58a65cba6a3b259))
17
+
18
+
19
+ ### Dependencies
20
+
21
+ * The following workspace dependencies were updated
22
+ * dependencies
23
+ * @aigne/core bumped to 1.72.0-beta.18
24
+ * devDependencies
25
+ * @aigne/test-utils bumped to 0.5.69-beta.18
26
+
27
+ ## [0.14.16-beta.18](https://github.com/AIGNE-io/aigne-framework/compare/anthropic-v0.14.16-beta.17...anthropic-v0.14.16-beta.18) (2026-01-12)
28
+
29
+
30
+ ### Dependencies
31
+
32
+ * The following workspace dependencies were updated
33
+ * dependencies
34
+ * @aigne/core bumped to 1.72.0-beta.17
35
+ * devDependencies
36
+ * @aigne/test-utils bumped to 0.5.69-beta.17
37
+
38
+ ## [0.14.16-beta.17](https://github.com/AIGNE-io/aigne-framework/compare/anthropic-v0.14.16-beta.16...anthropic-v0.14.16-beta.17) (2026-01-12)
39
+
40
+
41
+ ### Dependencies
42
+
43
+ * The following workspace dependencies were updated
44
+ * dependencies
45
+ * @aigne/core bumped to 1.72.0-beta.16
46
+ * devDependencies
47
+ * @aigne/test-utils bumped to 0.5.69-beta.16
48
+
49
+ ## [0.14.16-beta.16](https://github.com/AIGNE-io/aigne-framework/compare/anthropic-v0.14.16-beta.15...anthropic-v0.14.16-beta.16) (2026-01-10)
50
+
51
+
52
+ ### Bug Fixes
53
+
54
+ * **core:** simplify token-estimator logic for remaining characters ([45d43cc](https://github.com/AIGNE-io/aigne-framework/commit/45d43ccd3afd636cfb459eea2e6551e8f9c53765))
55
+
56
+
57
+ ### Dependencies
58
+
59
+ * The following workspace dependencies were updated
60
+ * dependencies
61
+ * @aigne/core bumped to 1.72.0-beta.15
62
+ * devDependencies
63
+ * @aigne/test-utils bumped to 0.5.69-beta.15
64
+
65
+ ## [0.14.16-beta.15](https://github.com/AIGNE-io/aigne-framework/compare/anthropic-v0.14.16-beta.14...anthropic-v0.14.16-beta.15) (2026-01-09)
66
+
67
+
68
+ ### Bug Fixes
69
+
70
+ * **core:** default enable auto breakpoints for chat model ([d4a6b83](https://github.com/AIGNE-io/aigne-framework/commit/d4a6b8323d6c83be45669885b32febb545bdf797))
71
+
72
+
73
+ ### Dependencies
74
+
75
+ * The following workspace dependencies were updated
76
+ * dependencies
77
+ * @aigne/core bumped to 1.72.0-beta.14
78
+ * devDependencies
79
+ * @aigne/test-utils bumped to 0.5.69-beta.14
80
+
81
+ ## [0.14.16-beta.14](https://github.com/AIGNE-io/aigne-framework/compare/anthropic-v0.14.16-beta.13...anthropic-v0.14.16-beta.14) (2026-01-08)
82
+
83
+
84
+ ### Bug Fixes
85
+
86
+ * bump version ([696560f](https://github.com/AIGNE-io/aigne-framework/commit/696560fa2673eddcb4d00ac0523fbbbde7273cb3))
87
+
88
+
89
+ ### Dependencies
90
+
91
+ * The following workspace dependencies were updated
92
+ * dependencies
93
+ * @aigne/core bumped to 1.72.0-beta.13
94
+ * @aigne/platform-helpers bumped to 0.6.7-beta.1
95
+ * devDependencies
96
+ * @aigne/test-utils bumped to 0.5.69-beta.13
97
+
98
+ ## [0.14.16-beta.13](https://github.com/AIGNE-io/aigne-framework/compare/anthropic-v0.14.16-beta.12...anthropic-v0.14.16-beta.13) (2026-01-07)
99
+
100
+
101
+ ### Dependencies
102
+
103
+ * The following workspace dependencies were updated
104
+ * dependencies
105
+ * @aigne/core bumped to 1.72.0-beta.12
106
+ * devDependencies
107
+ * @aigne/test-utils bumped to 0.5.69-beta.12
108
+
109
+ ## [0.14.16-beta.12](https://github.com/AIGNE-io/aigne-framework/compare/anthropic-v0.14.16-beta.11...anthropic-v0.14.16-beta.12) (2026-01-06)
110
+
111
+
112
+ ### Bug Fixes
113
+
114
+ * **core:** preserve Agent Skill in session compact and support complex tool result content ([#876](https://github.com/AIGNE-io/aigne-framework/issues/876)) ([edb86ae](https://github.com/AIGNE-io/aigne-framework/commit/edb86ae2b9cfe56a8f08b276f843606e310566cf))
115
+
116
+
117
+ ### Dependencies
118
+
119
+ * The following workspace dependencies were updated
120
+ * dependencies
121
+ * @aigne/core bumped to 1.72.0-beta.11
122
+ * devDependencies
123
+ * @aigne/test-utils bumped to 0.5.69-beta.11
124
+
125
+ ## [0.14.16-beta.11](https://github.com/AIGNE-io/aigne-framework/compare/anthropic-v0.14.16-beta.10...anthropic-v0.14.16-beta.11) (2026-01-06)
126
+
127
+
128
+ ### Dependencies
129
+
130
+ * The following workspace dependencies were updated
131
+ * dependencies
132
+ * @aigne/core bumped to 1.72.0-beta.10
133
+ * devDependencies
134
+ * @aigne/test-utils bumped to 0.5.69-beta.10
135
+
136
+ ## [0.14.16-beta.10](https://github.com/AIGNE-io/aigne-framework/compare/anthropic-v0.14.16-beta.9...anthropic-v0.14.16-beta.10) (2026-01-02)
137
+
138
+
139
+ ### Dependencies
140
+
141
+ * The following workspace dependencies were updated
142
+ * dependencies
143
+ * @aigne/core bumped to 1.72.0-beta.9
144
+ * devDependencies
145
+ * @aigne/test-utils bumped to 0.5.69-beta.9
146
+
147
+ ## [0.14.16-beta.9](https://github.com/AIGNE-io/aigne-framework/compare/anthropic-v0.14.16-beta.8...anthropic-v0.14.16-beta.9) (2025-12-31)
148
+
149
+
150
+ ### Features
151
+
152
+ * add session compact support for AIAgent ([#863](https://github.com/AIGNE-io/aigne-framework/issues/863)) ([9010918](https://github.com/AIGNE-io/aigne-framework/commit/9010918cd3f18b02b5c60ddc9ed5c34b568d0b28))
153
+
154
+
155
+ ### Dependencies
156
+
157
+ * The following workspace dependencies were updated
158
+ * dependencies
159
+ * @aigne/core bumped to 1.72.0-beta.8
160
+ * devDependencies
161
+ * @aigne/test-utils bumped to 0.5.69-beta.8
162
+
163
+ ## [0.14.16-beta.8](https://github.com/AIGNE-io/aigne-framework/compare/anthropic-v0.14.16-beta.7...anthropic-v0.14.16-beta.8) (2025-12-26)
164
+
165
+
166
+ ### Dependencies
167
+
168
+ * The following workspace dependencies were updated
169
+ * dependencies
170
+ * @aigne/core bumped to 1.72.0-beta.7
171
+ * devDependencies
172
+ * @aigne/test-utils bumped to 0.5.69-beta.7
173
+
174
+ ## [0.14.16-beta.7](https://github.com/AIGNE-io/aigne-framework/compare/anthropic-v0.14.16-beta.6...anthropic-v0.14.16-beta.7) (2025-12-25)
175
+
176
+
177
+ ### Dependencies
178
+
179
+ * The following workspace dependencies were updated
180
+ * dependencies
181
+ * @aigne/core bumped to 1.72.0-beta.6
182
+ * devDependencies
183
+ * @aigne/test-utils bumped to 0.5.69-beta.6
184
+
185
+ ## [0.14.16-beta.6](https://github.com/AIGNE-io/aigne-framework/compare/anthropic-v0.14.16-beta.5...anthropic-v0.14.16-beta.6) (2025-12-25)
186
+
187
+
188
+ ### Bug Fixes
189
+
190
+ * **models:** support cache the last message for anthropic chat model ([#853](https://github.com/AIGNE-io/aigne-framework/issues/853)) ([bd08e44](https://github.com/AIGNE-io/aigne-framework/commit/bd08e44b28c46ac9a85234f0100d6dd144703c9d))
191
+
192
+ ## [0.14.16-beta.5](https://github.com/AIGNE-io/aigne-framework/compare/anthropic-v0.14.16-beta.4...anthropic-v0.14.16-beta.5) (2025-12-25)
193
+
194
+
195
+ ### Dependencies
196
+
197
+ * The following workspace dependencies were updated
198
+ * dependencies
199
+ * @aigne/core bumped to 1.72.0-beta.5
200
+ * devDependencies
201
+ * @aigne/test-utils bumped to 0.5.69-beta.5
202
+
203
+ ## [0.14.16-beta.4](https://github.com/AIGNE-io/aigne-framework/compare/anthropic-v0.14.16-beta.3...anthropic-v0.14.16-beta.4) (2025-12-24)
204
+
205
+
206
+ ### Dependencies
207
+
208
+ * The following workspace dependencies were updated
209
+ * dependencies
210
+ * @aigne/core bumped to 1.72.0-beta.4
211
+ * devDependencies
212
+ * @aigne/test-utils bumped to 0.5.69-beta.4
213
+
214
+ ## [0.14.16-beta.3](https://github.com/AIGNE-io/aigne-framework/compare/anthropic-v0.14.16-beta.2...anthropic-v0.14.16-beta.3) (2025-12-19)
215
+
216
+
217
+ ### Features
218
+
219
+ * add prompt caching for OpenAI/Gemini/Anthropic and cache token display ([#838](https://github.com/AIGNE-io/aigne-framework/issues/838)) ([46c628f](https://github.com/AIGNE-io/aigne-framework/commit/46c628f180572ea1b955d1a9888aad6145204842))
220
+
221
+
222
+ ### Dependencies
223
+
224
+ * The following workspace dependencies were updated
225
+ * dependencies
226
+ * @aigne/core bumped to 1.72.0-beta.3
227
+ * devDependencies
228
+ * @aigne/test-utils bumped to 0.5.69-beta.3
229
+
3
230
  ## [0.14.16-beta.2](https://github.com/AIGNE-io/aigne-framework/compare/anthropic-v0.14.16-beta.1...anthropic-v0.14.16-beta.2) (2025-12-19)
4
231
 
5
232
 
@@ -124,19 +124,22 @@ export declare class AnthropicChatModel extends ChatModel {
124
124
  reasoningEffort?: number | "minimal" | "low" | "medium" | "high" | {
125
125
  $get: string;
126
126
  } | undefined;
127
+ cacheConfig?: import("@aigne/core").CacheConfig | {
128
+ $get: string;
129
+ } | undefined;
127
130
  }> | undefined;
128
131
  get credential(): {
129
132
  apiKey: string | undefined;
130
133
  model: string;
131
134
  };
135
+ countTokens(input: ChatModelInput): Promise<number>;
136
+ private getMessageCreateParams;
132
137
  private getMaxTokens;
133
138
  /**
134
139
  * Process the input using Claude's chat model
135
140
  * @param input - The input to process
136
141
  * @returns The processed output from the model
137
142
  */
138
- process(input: ChatModelInput, options: AgentInvokeOptions): PromiseOrValue<AgentProcessResult<ChatModelOutput>>;
139
- private _process;
140
- private extractResultFromAnthropicStream;
141
- private requestStructuredOutput;
143
+ process(input: ChatModelInput, _options: AgentInvokeOptions): PromiseOrValue<AgentProcessResult<ChatModelOutput>>;
144
+ private processInput;
142
145
  }
@@ -6,13 +6,11 @@ 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");
10
- const model_utils_js_1 = require("@aigne/core/utils/model-utils.js");
11
- const stream_utils_js_1 = require("@aigne/core/utils/stream-utils.js");
12
9
  const type_utils_js_1 = require("@aigne/core/utils/type-utils.js");
13
10
  const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
14
11
  const zod_1 = require("zod");
15
12
  const CHAT_MODEL_CLAUDE_DEFAULT_MODEL = "claude-3-7-sonnet-latest";
13
+ const OUTPUT_FUNCTION_NAME = "generate_json";
16
14
  /**
17
15
  * @hidden
18
16
  */
@@ -82,6 +80,23 @@ class AnthropicChatModel extends core_1.ChatModel {
82
80
  model: this.options?.model || CHAT_MODEL_CLAUDE_DEFAULT_MODEL,
83
81
  };
84
82
  }
83
+ async countTokens(input) {
84
+ const request = await this.getMessageCreateParams(input);
85
+ return (await this.client.messages.countTokens((0, type_utils_js_1.omit)(request, "max_tokens"))).input_tokens;
86
+ }
87
+ async getMessageCreateParams(input) {
88
+ const { modelOptions = {} } = input;
89
+ const model = modelOptions.model || this.credential.model;
90
+ const disableParallelToolUse = modelOptions.parallelToolCalls === false;
91
+ return {
92
+ model,
93
+ temperature: modelOptions.temperature,
94
+ top_p: modelOptions.topP,
95
+ max_tokens: this.getMaxTokens(model),
96
+ ...(await convertMessages(input)),
97
+ ...convertTools({ ...input, disableParallelToolUse }),
98
+ };
99
+ }
85
100
  getMaxTokens(model) {
86
101
  const matchers = [
87
102
  [/claude-opus-4-/, 32000],
@@ -102,186 +117,131 @@ class AnthropicChatModel extends core_1.ChatModel {
102
117
  * @param input - The input to process
103
118
  * @returns The processed output from the model
104
119
  */
105
- process(input, options) {
106
- return this._process(input, options);
120
+ process(input, _options) {
121
+ return this.processInput(input);
107
122
  }
108
- async _process(input, _options) {
109
- const { modelOptions = {} } = input;
110
- const model = modelOptions.model || this.credential.model;
111
- const disableParallelToolUse = modelOptions.parallelToolCalls === false;
112
- const body = {
113
- model,
114
- temperature: modelOptions.temperature,
115
- top_p: modelOptions.topP,
116
- // TODO: make dynamic based on model https://docs.anthropic.com/en/docs/about-claude/models/all-models
117
- max_tokens: this.getMaxTokens(model),
118
- ...(await convertMessages(input)),
119
- ...convertTools({ ...input, disableParallelToolUse }),
120
- };
121
- // Claude does not support json_schema response and tool calls in the same request,
122
- // so we need to handle the case where tools are not used and responseFormat is json
123
- if (!input.tools?.length && input.responseFormat?.type === "json_schema") {
124
- return this.requestStructuredOutput(body, input.responseFormat);
123
+ async *processInput(input) {
124
+ const body = await this.getMessageCreateParams(input);
125
+ const stream = this.client.messages.stream({ ...body, stream: true });
126
+ const blocks = [];
127
+ let usage;
128
+ let json;
129
+ for await (const chunk of stream) {
130
+ if (chunk.type === "message_start") {
131
+ yield { delta: { json: { model: chunk.message.model } } };
132
+ const { input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens, } = chunk.message.usage;
133
+ usage = {
134
+ inputTokens: input_tokens,
135
+ outputTokens: output_tokens,
136
+ cacheCreationInputTokens: cache_creation_input_tokens ?? undefined,
137
+ cacheReadInputTokens: cache_read_input_tokens ?? undefined,
138
+ };
139
+ }
140
+ if (chunk.type === "message_delta" && usage) {
141
+ usage.outputTokens = chunk.usage.output_tokens;
142
+ }
143
+ if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
144
+ yield { delta: { text: { text: chunk.delta.text } } };
145
+ }
146
+ if (chunk.type === "content_block_start" && chunk.content_block.type === "tool_use") {
147
+ blocks[chunk.index] = {
148
+ type: "function",
149
+ id: chunk.content_block.id,
150
+ function: { name: chunk.content_block.name, arguments: {} },
151
+ args: "",
152
+ };
153
+ }
154
+ if (chunk.type === "content_block_delta" && chunk.delta.type === "input_json_delta") {
155
+ const call = blocks[chunk.index];
156
+ if (!call)
157
+ throw new Error("Tool call not found");
158
+ call.args += chunk.delta.partial_json;
159
+ }
125
160
  }
126
- const stream = this.client.messages.stream({
127
- ...body,
128
- stream: true,
129
- });
130
- if (input.responseFormat?.type !== "json_schema") {
131
- return this.extractResultFromAnthropicStream(stream, true);
161
+ const toolCalls = blocks.filter(type_utils_js_1.isNonNullable);
162
+ // Separate output tool from business tool calls
163
+ const outputToolCall = toolCalls.find((c) => c.function.name === OUTPUT_FUNCTION_NAME);
164
+ const businessToolCalls = toolCalls
165
+ .filter((c) => c.function.name !== OUTPUT_FUNCTION_NAME)
166
+ .map(({ args, ...c }) => ({
167
+ ...c,
168
+ function: {
169
+ ...c.function,
170
+ arguments: args.trim() ? (0, json_schema_js_1.parseJSON)(args) : {},
171
+ },
172
+ }))
173
+ .filter(type_utils_js_1.isNonNullable);
174
+ if (outputToolCall) {
175
+ json = outputToolCall.args.trim() ? (0, json_schema_js_1.parseJSON)(outputToolCall.args) : {};
132
176
  }
133
- const result = await this.extractResultFromAnthropicStream(stream);
134
- // Just return the result if it has tool calls
135
- if (result.toolCalls?.length)
136
- return result;
137
- // Try to parse the text response as JSON
138
- // If it matches the json_schema, return it as json
139
- const json = (0, core_1.safeParseJSON)(result.text || "");
140
- const validated = this.validateJsonSchema(input.responseFormat.jsonSchema.schema, json, {
141
- safe: true,
142
- });
143
- if (validated.success) {
144
- return { ...result, json: validated.data, text: undefined };
177
+ if (businessToolCalls.length) {
178
+ yield { delta: { json: { toolCalls: businessToolCalls } } };
145
179
  }
146
- logger_js_1.logger.warn(`AnthropicChatModel: Text response does not match JSON schema, trying to use tool to extract json `, { text: result.text });
147
- // Claude doesn't support json_schema response and tool calls in the same request,
148
- // so we need to make a separate request for json_schema response when the tool calls is empty
149
- const output = await this.requestStructuredOutput(body, input.responseFormat);
150
- return {
151
- ...output,
152
- // merge usage from both requests
153
- usage: (0, model_utils_js_1.mergeUsage)(result.usage, output.usage),
154
- };
155
- }
156
- async extractResultFromAnthropicStream(stream, streaming) {
157
- const result = new ReadableStream({
158
- async start(controller) {
159
- try {
160
- const toolCalls = [];
161
- let usage;
162
- let model;
163
- for await (const chunk of stream) {
164
- if (chunk.type === "message_start") {
165
- if (!model) {
166
- model = chunk.message.model;
167
- controller.enqueue({ delta: { json: { model } } });
168
- }
169
- const { input_tokens, output_tokens } = chunk.message.usage;
170
- usage = {
171
- inputTokens: input_tokens,
172
- outputTokens: output_tokens,
173
- };
174
- }
175
- if (chunk.type === "message_delta" && usage) {
176
- usage.outputTokens = chunk.usage.output_tokens;
177
- }
178
- // handle streaming text
179
- if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
180
- controller.enqueue({
181
- delta: { text: { text: chunk.delta.text } },
182
- });
183
- }
184
- if (chunk.type === "content_block_start" && chunk.content_block.type === "tool_use") {
185
- toolCalls[chunk.index] = {
186
- type: "function",
187
- id: chunk.content_block.id,
188
- function: {
189
- name: chunk.content_block.name,
190
- arguments: {},
191
- },
192
- args: "",
193
- };
194
- }
195
- if (chunk.type === "content_block_delta" && chunk.delta.type === "input_json_delta") {
196
- const call = toolCalls[chunk.index];
197
- if (!call)
198
- throw new Error("Tool call not found");
199
- call.args += chunk.delta.partial_json;
200
- }
201
- }
202
- controller.enqueue({ delta: { json: { usage } } });
203
- if (toolCalls.length) {
204
- controller.enqueue({
205
- delta: {
206
- json: {
207
- toolCalls: toolCalls
208
- .map(({ args, ...c }) => ({
209
- ...c,
210
- function: {
211
- ...c.function,
212
- // NOTE: claude may return a blank string for empty object (the tool's input schema is a empty object)
213
- arguments: args.trim() ? (0, json_schema_js_1.parseJSON)(args) : {},
214
- },
215
- }))
216
- .filter(type_utils_js_1.isNonNullable),
217
- },
218
- },
219
- });
220
- }
221
- controller.close();
222
- }
223
- catch (error) {
224
- controller.error(error);
225
- }
226
- },
227
- });
228
- return streaming ? result : await (0, stream_utils_js_1.agentResponseStreamToObject)(result);
229
- }
230
- async requestStructuredOutput(body, responseFormat) {
231
- if (responseFormat?.type !== "json_schema") {
232
- throw new Error("Expected json_schema response format");
180
+ if (json !== undefined) {
181
+ yield { delta: { json: { json: json } } };
233
182
  }
234
- const result = await this.client.messages.create({
235
- ...body,
236
- tools: [
237
- {
238
- name: "generate_json",
239
- description: "Generate a json result by given context",
240
- input_schema: responseFormat.jsonSchema.schema,
241
- },
242
- ],
243
- tool_choice: {
244
- type: "tool",
245
- name: "generate_json",
246
- disable_parallel_tool_use: true,
247
- },
248
- stream: false,
249
- });
250
- const jsonTool = result.content.find((i) => i.type === "tool_use" && i.name === "generate_json");
251
- if (!jsonTool)
252
- throw new Error("Json tool not found");
253
- return {
254
- json: jsonTool.input,
255
- model: result.model,
256
- usage: {
257
- inputTokens: result.usage.input_tokens,
258
- outputTokens: result.usage.output_tokens,
259
- },
260
- };
183
+ yield { delta: { json: { usage } } };
261
184
  }
262
185
  }
263
186
  exports.AnthropicChatModel = AnthropicChatModel;
264
- async function convertMessages({ messages, responseFormat, tools }) {
265
- const systemMessages = [];
187
+ /**
188
+ * Parse cache configuration from model options
189
+ */
190
+ function parseCacheConfig(modelOptions) {
191
+ const cacheConfig = modelOptions?.cacheConfig || {};
192
+ const shouldCache = cacheConfig.enabled !== false; // Default: enabled
193
+ const ttl = cacheConfig.ttl === "1h" ? "1h" : "5m"; // Default: 5m
194
+ const strategy = cacheConfig.strategy || "auto"; // Default: auto
195
+ const autoBreakpoints = {
196
+ tools: cacheConfig.autoBreakpoints?.tools !== false, // Default: true
197
+ system: cacheConfig.autoBreakpoints?.system !== false, // Default: true
198
+ lastMessage: cacheConfig.autoBreakpoints?.lastMessage === true, // Default: false
199
+ };
200
+ return {
201
+ shouldCache,
202
+ ttl,
203
+ strategy,
204
+ autoBreakpoints,
205
+ };
206
+ }
207
+ async function convertMessages({ messages, modelOptions }) {
208
+ const systemBlocks = [];
266
209
  const msgs = [];
210
+ // Extract cache configuration with defaults
211
+ const { shouldCache, strategy, autoBreakpoints, ...cacheConfig } = parseCacheConfig(modelOptions);
212
+ const ttl = cacheConfig.ttl === "1h" ? "1h" : undefined;
267
213
  for (const msg of messages) {
268
214
  if (msg.role === "system") {
269
- if (typeof msg.content !== "string")
270
- throw new Error("System message must have content");
271
- systemMessages.push(msg.content);
215
+ if (typeof msg.content === "string") {
216
+ const block = {
217
+ type: "text",
218
+ text: msg.content,
219
+ };
220
+ systemBlocks.push(block);
221
+ }
222
+ else if (Array.isArray(msg.content)) {
223
+ systemBlocks.push(...msg.content.map((item) => {
224
+ if (item.type !== "text")
225
+ throw new Error("System message only supports text content blocks");
226
+ return { type: "text", text: item.text };
227
+ }));
228
+ }
229
+ else {
230
+ throw new Error("System message must have string or array content");
231
+ }
272
232
  }
273
233
  else if (msg.role === "tool") {
274
234
  if (!msg.toolCallId)
275
235
  throw new Error("Tool message must have toolCallId");
276
- if (typeof msg.content !== "string")
277
- throw new Error("Tool message must have string content");
236
+ if (!msg.content)
237
+ throw new Error("Tool message must have content");
278
238
  msgs.push({
279
239
  role: "user",
280
240
  content: [
281
241
  {
282
242
  type: "tool_result",
283
243
  tool_use_id: msg.toolCallId,
284
- content: msg.content,
244
+ content: await convertContent(msg.content),
285
245
  },
286
246
  ],
287
247
  });
@@ -311,19 +271,60 @@ async function convertMessages({ messages, responseFormat, tools }) {
311
271
  }
312
272
  }
313
273
  }
314
- // If there are tools and responseFormat is json_schema, we need to add a system message
315
- // to inform the model about the expected json schema, then trying to parse the response as json
316
- if (tools?.length && responseFormat?.type === "json_schema") {
317
- systemMessages.push(`You should provide a json response with schema: ${JSON.stringify(responseFormat.jsonSchema.schema)}`);
274
+ // Apply cache_control to the last system block if auto strategy is enabled
275
+ if (shouldCache && strategy === "auto") {
276
+ if (autoBreakpoints.system && systemBlocks.length > 0) {
277
+ const lastBlock = systemBlocks[systemBlocks.length - 1];
278
+ if (lastBlock) {
279
+ lastBlock.cache_control = { type: "ephemeral", ttl };
280
+ }
281
+ }
282
+ if (autoBreakpoints.lastMessage) {
283
+ const lastMsg = msgs[msgs.length - 1];
284
+ if (lastMsg) {
285
+ if (typeof lastMsg.content === "string") {
286
+ lastMsg.content = [
287
+ { type: "text", text: lastMsg.content, cache_control: { type: "ephemeral", ttl } },
288
+ ];
289
+ }
290
+ else if (Array.isArray(lastMsg.content)) {
291
+ const lastBlock = lastMsg.content[lastMsg.content.length - 1];
292
+ if (lastBlock &&
293
+ lastBlock.type !== "thinking" &&
294
+ lastBlock.type !== "redacted_thinking") {
295
+ lastBlock.cache_control = { type: "ephemeral", ttl };
296
+ }
297
+ }
298
+ }
299
+ }
300
+ }
301
+ // Manual cache control: apply user-specified cacheControl from system messages
302
+ if (shouldCache && strategy === "manual") {
303
+ for (const [index, msg] of messages.entries()) {
304
+ const msgWithCache = msg;
305
+ if (msg.role === "system" && msgWithCache.cacheControl) {
306
+ const block = systemBlocks[index];
307
+ if (block) {
308
+ block.cache_control = {
309
+ type: msgWithCache.cacheControl.type,
310
+ ...(msgWithCache.cacheControl.ttl && { ttl: msgWithCache.cacheControl.ttl }),
311
+ };
312
+ }
313
+ }
314
+ }
318
315
  }
319
- const system = systemMessages.join("\n").trim() || undefined;
320
316
  // Claude requires at least one message, so we add a system message if there are no messages
321
317
  if (msgs.length === 0) {
322
- if (!system)
318
+ if (systemBlocks.length === 0)
323
319
  throw new Error("No messages provided");
324
- return { messages: [{ role: "user", content: system }] };
320
+ // Convert system blocks to a single user message
321
+ const systemText = systemBlocks.map((b) => b.text).join("\n");
322
+ return { messages: [{ role: "user", content: systemText }] };
325
323
  }
326
- return { messages: msgs, system };
324
+ return {
325
+ messages: msgs,
326
+ system: systemBlocks.length > 0 ? systemBlocks : undefined,
327
+ };
327
328
  }
328
329
  async function convertContent(content) {
329
330
  if (typeof content === "string")
@@ -348,38 +349,64 @@ async function convertContent(content) {
348
349
  }
349
350
  throw new Error("Invalid chat message content");
350
351
  }
351
- function convertTools({ tools, toolChoice, disableParallelToolUse, }) {
352
- let choice;
353
- if (typeof toolChoice === "object" && "type" in toolChoice && toolChoice.type === "function") {
354
- choice = {
355
- type: "tool",
356
- name: toolChoice.function.name,
357
- disable_parallel_tool_use: disableParallelToolUse,
358
- };
359
- }
360
- else if (toolChoice === "required") {
361
- choice = { type: "any", disable_parallel_tool_use: disableParallelToolUse };
362
- }
363
- else if (toolChoice === "auto") {
364
- choice = {
365
- type: "auto",
366
- disable_parallel_tool_use: disableParallelToolUse,
352
+ function convertTools({ tools, toolChoice, disableParallelToolUse, modelOptions, responseFormat, }) {
353
+ // Extract cache configuration with defaults
354
+ const { shouldCache, ttl, strategy, autoBreakpoints } = parseCacheConfig(modelOptions);
355
+ const shouldCacheTools = shouldCache && strategy === "auto" && autoBreakpoints.tools;
356
+ // Convert business tools
357
+ const convertedTools = (tools ?? []).map((i) => {
358
+ const tool = {
359
+ name: i.function.name,
360
+ description: i.function.description,
361
+ input_schema: (0, type_utils_js_1.isEmpty)(i.function.parameters)
362
+ ? { type: "object" }
363
+ : i.function.parameters,
367
364
  };
365
+ // Manual cache mode: apply tool-specific cacheControl
366
+ if (shouldCache && strategy === "manual" && i.cacheControl) {
367
+ tool.cache_control = {
368
+ type: i.cacheControl.type,
369
+ ...(i.cacheControl.ttl && { ttl: i.cacheControl.ttl }),
370
+ };
371
+ }
372
+ return tool;
373
+ });
374
+ // Add output tool for structured output
375
+ if (responseFormat?.type === "json_schema") {
376
+ convertedTools.push({
377
+ name: OUTPUT_FUNCTION_NAME,
378
+ description: "Generate a json result by given context",
379
+ input_schema: responseFormat.jsonSchema.schema,
380
+ });
368
381
  }
369
- else if (toolChoice === "none") {
370
- choice = { type: "none" };
382
+ // Auto cache mode: add cache_control to the last tool
383
+ if (shouldCacheTools && convertedTools.length) {
384
+ const lastTool = convertedTools[convertedTools.length - 1];
385
+ if (lastTool) {
386
+ lastTool.cache_control = { type: "ephemeral", ...(ttl === "1h" && { ttl: "1h" }) };
387
+ }
371
388
  }
389
+ // Determine tool choice
390
+ const choice = responseFormat?.type === "json_schema"
391
+ ? // For structured output: force output tool if no business tools, otherwise let model choose
392
+ tools?.length
393
+ ? { type: "any", disable_parallel_tool_use: disableParallelToolUse }
394
+ : { type: "tool", name: OUTPUT_FUNCTION_NAME, disable_parallel_tool_use: true }
395
+ : typeof toolChoice === "object" && "type" in toolChoice && toolChoice.type === "function"
396
+ ? {
397
+ type: "tool",
398
+ name: toolChoice.function.name,
399
+ disable_parallel_tool_use: disableParallelToolUse,
400
+ }
401
+ : toolChoice === "required"
402
+ ? { type: "any", disable_parallel_tool_use: disableParallelToolUse }
403
+ : toolChoice === "auto"
404
+ ? { type: "auto", disable_parallel_tool_use: disableParallelToolUse }
405
+ : toolChoice === "none"
406
+ ? { type: "none" }
407
+ : undefined;
372
408
  return {
373
- tools: tools?.length
374
- ? tools.map((i) => ({
375
- name: i.function.name,
376
- description: i.function.description,
377
- input_schema: (0, type_utils_js_1.isEmpty)(i.function.parameters)
378
- ? { type: "object" }
379
- : i.function.parameters,
380
- }))
381
- : undefined,
409
+ tools: convertedTools.length ? convertedTools : undefined,
382
410
  tool_choice: choice,
383
411
  };
384
412
  }
385
- // safeParseJSON is now imported from @aigne/core
@@ -124,19 +124,22 @@ export declare class AnthropicChatModel extends ChatModel {
124
124
  reasoningEffort?: number | "minimal" | "low" | "medium" | "high" | {
125
125
  $get: string;
126
126
  } | undefined;
127
+ cacheConfig?: import("@aigne/core").CacheConfig | {
128
+ $get: string;
129
+ } | undefined;
127
130
  }> | undefined;
128
131
  get credential(): {
129
132
  apiKey: string | undefined;
130
133
  model: string;
131
134
  };
135
+ countTokens(input: ChatModelInput): Promise<number>;
136
+ private getMessageCreateParams;
132
137
  private getMaxTokens;
133
138
  /**
134
139
  * Process the input using Claude's chat model
135
140
  * @param input - The input to process
136
141
  * @returns The processed output from the model
137
142
  */
138
- process(input: ChatModelInput, options: AgentInvokeOptions): PromiseOrValue<AgentProcessResult<ChatModelOutput>>;
139
- private _process;
140
- private extractResultFromAnthropicStream;
141
- private requestStructuredOutput;
143
+ process(input: ChatModelInput, _options: AgentInvokeOptions): PromiseOrValue<AgentProcessResult<ChatModelOutput>>;
144
+ private processInput;
142
145
  }
@@ -124,19 +124,22 @@ export declare class AnthropicChatModel extends ChatModel {
124
124
  reasoningEffort?: number | "minimal" | "low" | "medium" | "high" | {
125
125
  $get: string;
126
126
  } | undefined;
127
+ cacheConfig?: import("@aigne/core").CacheConfig | {
128
+ $get: string;
129
+ } | undefined;
127
130
  }> | undefined;
128
131
  get credential(): {
129
132
  apiKey: string | undefined;
130
133
  model: string;
131
134
  };
135
+ countTokens(input: ChatModelInput): Promise<number>;
136
+ private getMessageCreateParams;
132
137
  private getMaxTokens;
133
138
  /**
134
139
  * Process the input using Claude's chat model
135
140
  * @param input - The input to process
136
141
  * @returns The processed output from the model
137
142
  */
138
- process(input: ChatModelInput, options: AgentInvokeOptions): PromiseOrValue<AgentProcessResult<ChatModelOutput>>;
139
- private _process;
140
- private extractResultFromAnthropicStream;
141
- private requestStructuredOutput;
143
+ process(input: ChatModelInput, _options: AgentInvokeOptions): PromiseOrValue<AgentProcessResult<ChatModelOutput>>;
144
+ private processInput;
142
145
  }
@@ -1,12 +1,10 @@
1
- import { ChatModel, safeParseJSON, } from "@aigne/core";
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";
4
- import { mergeUsage } from "@aigne/core/utils/model-utils.js";
5
- import { agentResponseStreamToObject } from "@aigne/core/utils/stream-utils.js";
6
- import { checkArguments, isEmpty, isNonNullable, } from "@aigne/core/utils/type-utils.js";
3
+ import { checkArguments, isEmpty, isNonNullable, omit, } from "@aigne/core/utils/type-utils.js";
7
4
  import Anthropic from "@anthropic-ai/sdk";
8
5
  import { z } from "zod";
9
6
  const CHAT_MODEL_CLAUDE_DEFAULT_MODEL = "claude-3-7-sonnet-latest";
7
+ const OUTPUT_FUNCTION_NAME = "generate_json";
10
8
  /**
11
9
  * @hidden
12
10
  */
@@ -76,6 +74,23 @@ export class AnthropicChatModel extends ChatModel {
76
74
  model: this.options?.model || CHAT_MODEL_CLAUDE_DEFAULT_MODEL,
77
75
  };
78
76
  }
77
+ async countTokens(input) {
78
+ const request = await this.getMessageCreateParams(input);
79
+ return (await this.client.messages.countTokens(omit(request, "max_tokens"))).input_tokens;
80
+ }
81
+ async getMessageCreateParams(input) {
82
+ const { modelOptions = {} } = input;
83
+ const model = modelOptions.model || this.credential.model;
84
+ const disableParallelToolUse = modelOptions.parallelToolCalls === false;
85
+ return {
86
+ model,
87
+ temperature: modelOptions.temperature,
88
+ top_p: modelOptions.topP,
89
+ max_tokens: this.getMaxTokens(model),
90
+ ...(await convertMessages(input)),
91
+ ...convertTools({ ...input, disableParallelToolUse }),
92
+ };
93
+ }
79
94
  getMaxTokens(model) {
80
95
  const matchers = [
81
96
  [/claude-opus-4-/, 32000],
@@ -96,185 +111,130 @@ export class AnthropicChatModel extends ChatModel {
96
111
  * @param input - The input to process
97
112
  * @returns The processed output from the model
98
113
  */
99
- process(input, options) {
100
- return this._process(input, options);
114
+ process(input, _options) {
115
+ return this.processInput(input);
101
116
  }
102
- async _process(input, _options) {
103
- const { modelOptions = {} } = input;
104
- const model = modelOptions.model || this.credential.model;
105
- const disableParallelToolUse = modelOptions.parallelToolCalls === false;
106
- const body = {
107
- model,
108
- temperature: modelOptions.temperature,
109
- top_p: modelOptions.topP,
110
- // TODO: make dynamic based on model https://docs.anthropic.com/en/docs/about-claude/models/all-models
111
- max_tokens: this.getMaxTokens(model),
112
- ...(await convertMessages(input)),
113
- ...convertTools({ ...input, disableParallelToolUse }),
114
- };
115
- // Claude does not support json_schema response and tool calls in the same request,
116
- // so we need to handle the case where tools are not used and responseFormat is json
117
- if (!input.tools?.length && input.responseFormat?.type === "json_schema") {
118
- return this.requestStructuredOutput(body, input.responseFormat);
117
+ async *processInput(input) {
118
+ const body = await this.getMessageCreateParams(input);
119
+ const stream = this.client.messages.stream({ ...body, stream: true });
120
+ const blocks = [];
121
+ let usage;
122
+ let json;
123
+ for await (const chunk of stream) {
124
+ if (chunk.type === "message_start") {
125
+ yield { delta: { json: { model: chunk.message.model } } };
126
+ const { input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens, } = chunk.message.usage;
127
+ usage = {
128
+ inputTokens: input_tokens,
129
+ outputTokens: output_tokens,
130
+ cacheCreationInputTokens: cache_creation_input_tokens ?? undefined,
131
+ cacheReadInputTokens: cache_read_input_tokens ?? undefined,
132
+ };
133
+ }
134
+ if (chunk.type === "message_delta" && usage) {
135
+ usage.outputTokens = chunk.usage.output_tokens;
136
+ }
137
+ if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
138
+ yield { delta: { text: { text: chunk.delta.text } } };
139
+ }
140
+ if (chunk.type === "content_block_start" && chunk.content_block.type === "tool_use") {
141
+ blocks[chunk.index] = {
142
+ type: "function",
143
+ id: chunk.content_block.id,
144
+ function: { name: chunk.content_block.name, arguments: {} },
145
+ args: "",
146
+ };
147
+ }
148
+ if (chunk.type === "content_block_delta" && chunk.delta.type === "input_json_delta") {
149
+ const call = blocks[chunk.index];
150
+ if (!call)
151
+ throw new Error("Tool call not found");
152
+ call.args += chunk.delta.partial_json;
153
+ }
119
154
  }
120
- const stream = this.client.messages.stream({
121
- ...body,
122
- stream: true,
123
- });
124
- if (input.responseFormat?.type !== "json_schema") {
125
- return this.extractResultFromAnthropicStream(stream, true);
155
+ const toolCalls = blocks.filter(isNonNullable);
156
+ // Separate output tool from business tool calls
157
+ const outputToolCall = toolCalls.find((c) => c.function.name === OUTPUT_FUNCTION_NAME);
158
+ const businessToolCalls = toolCalls
159
+ .filter((c) => c.function.name !== OUTPUT_FUNCTION_NAME)
160
+ .map(({ args, ...c }) => ({
161
+ ...c,
162
+ function: {
163
+ ...c.function,
164
+ arguments: args.trim() ? parseJSON(args) : {},
165
+ },
166
+ }))
167
+ .filter(isNonNullable);
168
+ if (outputToolCall) {
169
+ json = outputToolCall.args.trim() ? parseJSON(outputToolCall.args) : {};
126
170
  }
127
- const result = await this.extractResultFromAnthropicStream(stream);
128
- // Just return the result if it has tool calls
129
- if (result.toolCalls?.length)
130
- return result;
131
- // Try to parse the text response as JSON
132
- // If it matches the json_schema, return it as json
133
- const json = safeParseJSON(result.text || "");
134
- const validated = this.validateJsonSchema(input.responseFormat.jsonSchema.schema, json, {
135
- safe: true,
136
- });
137
- if (validated.success) {
138
- return { ...result, json: validated.data, text: undefined };
171
+ if (businessToolCalls.length) {
172
+ yield { delta: { json: { toolCalls: businessToolCalls } } };
139
173
  }
140
- logger.warn(`AnthropicChatModel: Text response does not match JSON schema, trying to use tool to extract json `, { text: result.text });
141
- // Claude doesn't support json_schema response and tool calls in the same request,
142
- // so we need to make a separate request for json_schema response when the tool calls is empty
143
- const output = await this.requestStructuredOutput(body, input.responseFormat);
144
- return {
145
- ...output,
146
- // merge usage from both requests
147
- usage: mergeUsage(result.usage, output.usage),
148
- };
149
- }
150
- async extractResultFromAnthropicStream(stream, streaming) {
151
- const result = new ReadableStream({
152
- async start(controller) {
153
- try {
154
- const toolCalls = [];
155
- let usage;
156
- let model;
157
- for await (const chunk of stream) {
158
- if (chunk.type === "message_start") {
159
- if (!model) {
160
- model = chunk.message.model;
161
- controller.enqueue({ delta: { json: { model } } });
162
- }
163
- const { input_tokens, output_tokens } = chunk.message.usage;
164
- usage = {
165
- inputTokens: input_tokens,
166
- outputTokens: output_tokens,
167
- };
168
- }
169
- if (chunk.type === "message_delta" && usage) {
170
- usage.outputTokens = chunk.usage.output_tokens;
171
- }
172
- // handle streaming text
173
- if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
174
- controller.enqueue({
175
- delta: { text: { text: chunk.delta.text } },
176
- });
177
- }
178
- if (chunk.type === "content_block_start" && chunk.content_block.type === "tool_use") {
179
- toolCalls[chunk.index] = {
180
- type: "function",
181
- id: chunk.content_block.id,
182
- function: {
183
- name: chunk.content_block.name,
184
- arguments: {},
185
- },
186
- args: "",
187
- };
188
- }
189
- if (chunk.type === "content_block_delta" && chunk.delta.type === "input_json_delta") {
190
- const call = toolCalls[chunk.index];
191
- if (!call)
192
- throw new Error("Tool call not found");
193
- call.args += chunk.delta.partial_json;
194
- }
195
- }
196
- controller.enqueue({ delta: { json: { usage } } });
197
- if (toolCalls.length) {
198
- controller.enqueue({
199
- delta: {
200
- json: {
201
- toolCalls: toolCalls
202
- .map(({ args, ...c }) => ({
203
- ...c,
204
- function: {
205
- ...c.function,
206
- // NOTE: claude may return a blank string for empty object (the tool's input schema is a empty object)
207
- arguments: args.trim() ? parseJSON(args) : {},
208
- },
209
- }))
210
- .filter(isNonNullable),
211
- },
212
- },
213
- });
214
- }
215
- controller.close();
216
- }
217
- catch (error) {
218
- controller.error(error);
219
- }
220
- },
221
- });
222
- return streaming ? result : await agentResponseStreamToObject(result);
223
- }
224
- async requestStructuredOutput(body, responseFormat) {
225
- if (responseFormat?.type !== "json_schema") {
226
- throw new Error("Expected json_schema response format");
174
+ if (json !== undefined) {
175
+ yield { delta: { json: { json: json } } };
227
176
  }
228
- const result = await this.client.messages.create({
229
- ...body,
230
- tools: [
231
- {
232
- name: "generate_json",
233
- description: "Generate a json result by given context",
234
- input_schema: responseFormat.jsonSchema.schema,
235
- },
236
- ],
237
- tool_choice: {
238
- type: "tool",
239
- name: "generate_json",
240
- disable_parallel_tool_use: true,
241
- },
242
- stream: false,
243
- });
244
- const jsonTool = result.content.find((i) => i.type === "tool_use" && i.name === "generate_json");
245
- if (!jsonTool)
246
- throw new Error("Json tool not found");
247
- return {
248
- json: jsonTool.input,
249
- model: result.model,
250
- usage: {
251
- inputTokens: result.usage.input_tokens,
252
- outputTokens: result.usage.output_tokens,
253
- },
254
- };
177
+ yield { delta: { json: { usage } } };
255
178
  }
256
179
  }
257
- async function convertMessages({ messages, responseFormat, tools }) {
258
- const systemMessages = [];
180
+ /**
181
+ * Parse cache configuration from model options
182
+ */
183
+ function parseCacheConfig(modelOptions) {
184
+ const cacheConfig = modelOptions?.cacheConfig || {};
185
+ const shouldCache = cacheConfig.enabled !== false; // Default: enabled
186
+ const ttl = cacheConfig.ttl === "1h" ? "1h" : "5m"; // Default: 5m
187
+ const strategy = cacheConfig.strategy || "auto"; // Default: auto
188
+ const autoBreakpoints = {
189
+ tools: cacheConfig.autoBreakpoints?.tools !== false, // Default: true
190
+ system: cacheConfig.autoBreakpoints?.system !== false, // Default: true
191
+ lastMessage: cacheConfig.autoBreakpoints?.lastMessage === true, // Default: false
192
+ };
193
+ return {
194
+ shouldCache,
195
+ ttl,
196
+ strategy,
197
+ autoBreakpoints,
198
+ };
199
+ }
200
+ async function convertMessages({ messages, modelOptions }) {
201
+ const systemBlocks = [];
259
202
  const msgs = [];
203
+ // Extract cache configuration with defaults
204
+ const { shouldCache, strategy, autoBreakpoints, ...cacheConfig } = parseCacheConfig(modelOptions);
205
+ const ttl = cacheConfig.ttl === "1h" ? "1h" : undefined;
260
206
  for (const msg of messages) {
261
207
  if (msg.role === "system") {
262
- if (typeof msg.content !== "string")
263
- throw new Error("System message must have content");
264
- systemMessages.push(msg.content);
208
+ if (typeof msg.content === "string") {
209
+ const block = {
210
+ type: "text",
211
+ text: msg.content,
212
+ };
213
+ systemBlocks.push(block);
214
+ }
215
+ else if (Array.isArray(msg.content)) {
216
+ systemBlocks.push(...msg.content.map((item) => {
217
+ if (item.type !== "text")
218
+ throw new Error("System message only supports text content blocks");
219
+ return { type: "text", text: item.text };
220
+ }));
221
+ }
222
+ else {
223
+ throw new Error("System message must have string or array content");
224
+ }
265
225
  }
266
226
  else if (msg.role === "tool") {
267
227
  if (!msg.toolCallId)
268
228
  throw new Error("Tool message must have toolCallId");
269
- if (typeof msg.content !== "string")
270
- throw new Error("Tool message must have string content");
229
+ if (!msg.content)
230
+ throw new Error("Tool message must have content");
271
231
  msgs.push({
272
232
  role: "user",
273
233
  content: [
274
234
  {
275
235
  type: "tool_result",
276
236
  tool_use_id: msg.toolCallId,
277
- content: msg.content,
237
+ content: await convertContent(msg.content),
278
238
  },
279
239
  ],
280
240
  });
@@ -304,19 +264,60 @@ async function convertMessages({ messages, responseFormat, tools }) {
304
264
  }
305
265
  }
306
266
  }
307
- // If there are tools and responseFormat is json_schema, we need to add a system message
308
- // to inform the model about the expected json schema, then trying to parse the response as json
309
- if (tools?.length && responseFormat?.type === "json_schema") {
310
- systemMessages.push(`You should provide a json response with schema: ${JSON.stringify(responseFormat.jsonSchema.schema)}`);
267
+ // Apply cache_control to the last system block if auto strategy is enabled
268
+ if (shouldCache && strategy === "auto") {
269
+ if (autoBreakpoints.system && systemBlocks.length > 0) {
270
+ const lastBlock = systemBlocks[systemBlocks.length - 1];
271
+ if (lastBlock) {
272
+ lastBlock.cache_control = { type: "ephemeral", ttl };
273
+ }
274
+ }
275
+ if (autoBreakpoints.lastMessage) {
276
+ const lastMsg = msgs[msgs.length - 1];
277
+ if (lastMsg) {
278
+ if (typeof lastMsg.content === "string") {
279
+ lastMsg.content = [
280
+ { type: "text", text: lastMsg.content, cache_control: { type: "ephemeral", ttl } },
281
+ ];
282
+ }
283
+ else if (Array.isArray(lastMsg.content)) {
284
+ const lastBlock = lastMsg.content[lastMsg.content.length - 1];
285
+ if (lastBlock &&
286
+ lastBlock.type !== "thinking" &&
287
+ lastBlock.type !== "redacted_thinking") {
288
+ lastBlock.cache_control = { type: "ephemeral", ttl };
289
+ }
290
+ }
291
+ }
292
+ }
293
+ }
294
+ // Manual cache control: apply user-specified cacheControl from system messages
295
+ if (shouldCache && strategy === "manual") {
296
+ for (const [index, msg] of messages.entries()) {
297
+ const msgWithCache = msg;
298
+ if (msg.role === "system" && msgWithCache.cacheControl) {
299
+ const block = systemBlocks[index];
300
+ if (block) {
301
+ block.cache_control = {
302
+ type: msgWithCache.cacheControl.type,
303
+ ...(msgWithCache.cacheControl.ttl && { ttl: msgWithCache.cacheControl.ttl }),
304
+ };
305
+ }
306
+ }
307
+ }
311
308
  }
312
- const system = systemMessages.join("\n").trim() || undefined;
313
309
  // Claude requires at least one message, so we add a system message if there are no messages
314
310
  if (msgs.length === 0) {
315
- if (!system)
311
+ if (systemBlocks.length === 0)
316
312
  throw new Error("No messages provided");
317
- return { messages: [{ role: "user", content: system }] };
313
+ // Convert system blocks to a single user message
314
+ const systemText = systemBlocks.map((b) => b.text).join("\n");
315
+ return { messages: [{ role: "user", content: systemText }] };
318
316
  }
319
- return { messages: msgs, system };
317
+ return {
318
+ messages: msgs,
319
+ system: systemBlocks.length > 0 ? systemBlocks : undefined,
320
+ };
320
321
  }
321
322
  async function convertContent(content) {
322
323
  if (typeof content === "string")
@@ -341,38 +342,64 @@ async function convertContent(content) {
341
342
  }
342
343
  throw new Error("Invalid chat message content");
343
344
  }
344
- function convertTools({ tools, toolChoice, disableParallelToolUse, }) {
345
- let choice;
346
- if (typeof toolChoice === "object" && "type" in toolChoice && toolChoice.type === "function") {
347
- choice = {
348
- type: "tool",
349
- name: toolChoice.function.name,
350
- disable_parallel_tool_use: disableParallelToolUse,
351
- };
352
- }
353
- else if (toolChoice === "required") {
354
- choice = { type: "any", disable_parallel_tool_use: disableParallelToolUse };
355
- }
356
- else if (toolChoice === "auto") {
357
- choice = {
358
- type: "auto",
359
- disable_parallel_tool_use: disableParallelToolUse,
345
+ function convertTools({ tools, toolChoice, disableParallelToolUse, modelOptions, responseFormat, }) {
346
+ // Extract cache configuration with defaults
347
+ const { shouldCache, ttl, strategy, autoBreakpoints } = parseCacheConfig(modelOptions);
348
+ const shouldCacheTools = shouldCache && strategy === "auto" && autoBreakpoints.tools;
349
+ // Convert business tools
350
+ const convertedTools = (tools ?? []).map((i) => {
351
+ const tool = {
352
+ name: i.function.name,
353
+ description: i.function.description,
354
+ input_schema: isEmpty(i.function.parameters)
355
+ ? { type: "object" }
356
+ : i.function.parameters,
360
357
  };
358
+ // Manual cache mode: apply tool-specific cacheControl
359
+ if (shouldCache && strategy === "manual" && i.cacheControl) {
360
+ tool.cache_control = {
361
+ type: i.cacheControl.type,
362
+ ...(i.cacheControl.ttl && { ttl: i.cacheControl.ttl }),
363
+ };
364
+ }
365
+ return tool;
366
+ });
367
+ // Add output tool for structured output
368
+ if (responseFormat?.type === "json_schema") {
369
+ convertedTools.push({
370
+ name: OUTPUT_FUNCTION_NAME,
371
+ description: "Generate a json result by given context",
372
+ input_schema: responseFormat.jsonSchema.schema,
373
+ });
361
374
  }
362
- else if (toolChoice === "none") {
363
- choice = { type: "none" };
375
+ // Auto cache mode: add cache_control to the last tool
376
+ if (shouldCacheTools && convertedTools.length) {
377
+ const lastTool = convertedTools[convertedTools.length - 1];
378
+ if (lastTool) {
379
+ lastTool.cache_control = { type: "ephemeral", ...(ttl === "1h" && { ttl: "1h" }) };
380
+ }
364
381
  }
382
+ // Determine tool choice
383
+ const choice = responseFormat?.type === "json_schema"
384
+ ? // For structured output: force output tool if no business tools, otherwise let model choose
385
+ tools?.length
386
+ ? { type: "any", disable_parallel_tool_use: disableParallelToolUse }
387
+ : { type: "tool", name: OUTPUT_FUNCTION_NAME, disable_parallel_tool_use: true }
388
+ : typeof toolChoice === "object" && "type" in toolChoice && toolChoice.type === "function"
389
+ ? {
390
+ type: "tool",
391
+ name: toolChoice.function.name,
392
+ disable_parallel_tool_use: disableParallelToolUse,
393
+ }
394
+ : toolChoice === "required"
395
+ ? { type: "any", disable_parallel_tool_use: disableParallelToolUse }
396
+ : toolChoice === "auto"
397
+ ? { type: "auto", disable_parallel_tool_use: disableParallelToolUse }
398
+ : toolChoice === "none"
399
+ ? { type: "none" }
400
+ : undefined;
365
401
  return {
366
- tools: tools?.length
367
- ? tools.map((i) => ({
368
- name: i.function.name,
369
- description: i.function.description,
370
- input_schema: isEmpty(i.function.parameters)
371
- ? { type: "object" }
372
- : i.function.parameters,
373
- }))
374
- : undefined,
402
+ tools: convertedTools.length ? convertedTools : undefined,
375
403
  tool_choice: choice,
376
404
  };
377
405
  }
378
- // safeParseJSON is now imported from @aigne/core
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/anthropic",
3
- "version": "0.14.16-beta.2",
3
+ "version": "0.14.16-beta.20",
4
4
  "description": "AIGNE Anthropic SDK for integrating with Claude AI models",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -37,8 +37,8 @@
37
37
  "dependencies": {
38
38
  "@anthropic-ai/sdk": "^0.63.0",
39
39
  "zod": "^3.25.67",
40
- "@aigne/core": "^1.72.0-beta.2",
41
- "@aigne/platform-helpers": "^0.6.7-beta"
40
+ "@aigne/core": "^1.72.0-beta.18",
41
+ "@aigne/platform-helpers": "^0.6.7-beta.1"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/bun": "^1.2.22",
@@ -46,7 +46,7 @@
46
46
  "npm-run-all": "^4.1.5",
47
47
  "rimraf": "^6.0.1",
48
48
  "typescript": "^5.9.2",
49
- "@aigne/test-utils": "^0.5.69-beta.2"
49
+ "@aigne/test-utils": "^0.5.69-beta.18"
50
50
  },
51
51
  "scripts": {
52
52
  "lint": "tsc --noEmit",