@lobehub/lobehub 2.0.0-next.116 → 2.0.0-next.118

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
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.118](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.117...v2.0.0-next.118)
6
+
7
+ <sup>Released on **2025-11-26**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Showing compatibility with both new and old versions of Plugins.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Showing compatibility with both new and old versions of Plugins, closes [#10418](https://github.com/lobehub/lobe-chat/issues/10418) ([64af7b1](https://github.com/lobehub/lobe-chat/commit/64af7b1))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ## [Version 2.0.0-next.117](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.116...v2.0.0-next.117)
31
+
32
+ <sup>Released on **2025-11-25**</sup>
33
+
34
+ #### ✨ Features
35
+
36
+ - **misc**: Bedrock claude model thinking support.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's improved
44
+
45
+ - **misc**: Bedrock claude model thinking support, closes [#10422](https://github.com/lobehub/lobe-chat/issues/10422) ([8b41638](https://github.com/lobehub/lobe-chat/commit/8b41638))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ## [Version 2.0.0-next.116](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.115...v2.0.0-next.116)
6
56
 
7
57
  <sup>Released on **2025-11-25**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Showing compatibility with both new and old versions of Plugins."
6
+ ]
7
+ },
8
+ "date": "2025-11-26",
9
+ "version": "2.0.0-next.118"
10
+ },
11
+ {
12
+ "children": {
13
+ "features": [
14
+ "Bedrock claude model thinking support."
15
+ ]
16
+ },
17
+ "date": "2025-11-25",
18
+ "version": "2.0.0-next.117"
19
+ },
2
20
  {
3
21
  "children": {
4
22
  "features": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.116",
3
+ "version": "2.0.0-next.118",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -0,0 +1,367 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { headersToRecord } from '../headers';
4
+
5
+ describe('headersToRecord', () => {
6
+ describe('undefined or null input', () => {
7
+ it('should return empty object when headersInit is undefined', () => {
8
+ // Arrange & Act
9
+ const result = headersToRecord(undefined);
10
+
11
+ // Assert
12
+ expect(result).toEqual({});
13
+ });
14
+
15
+ it('should return empty object when headersInit is not provided', () => {
16
+ // Arrange & Act
17
+ const result = headersToRecord();
18
+
19
+ // Assert
20
+ expect(result).toEqual({});
21
+ });
22
+ });
23
+
24
+ describe('Headers instance', () => {
25
+ it('should convert Headers instance to record', () => {
26
+ // Arrange
27
+ const headers = new Headers();
28
+ headers.append('content-type', 'application/json');
29
+ headers.append('authorization', 'Bearer token123');
30
+ headers.append('x-custom-header', 'custom-value');
31
+
32
+ // Act
33
+ const result = headersToRecord(headers);
34
+
35
+ // Assert
36
+ expect(result).toEqual({
37
+ 'content-type': 'application/json',
38
+ 'authorization': 'Bearer token123',
39
+ 'x-custom-header': 'custom-value',
40
+ });
41
+ });
42
+
43
+ it('should handle Headers instance with multiple values for same key', () => {
44
+ // Arrange
45
+ const headers = new Headers();
46
+ headers.append('accept', 'application/json');
47
+ headers.append('accept', 'text/html');
48
+
49
+ // Act
50
+ const result = headersToRecord(headers);
51
+
52
+ // Assert
53
+ expect(result).toHaveProperty('accept');
54
+ expect(typeof result.accept).toBe('string');
55
+ });
56
+
57
+ it('should handle empty Headers instance', () => {
58
+ // Arrange
59
+ const headers = new Headers();
60
+
61
+ // Act
62
+ const result = headersToRecord(headers);
63
+
64
+ // Assert
65
+ expect(result).toEqual({});
66
+ });
67
+ });
68
+
69
+ describe('Array format', () => {
70
+ it('should convert array of tuples to record', () => {
71
+ // Arrange
72
+ const headersArray: [string, string][] = [
73
+ ['content-type', 'application/json'],
74
+ ['authorization', 'Bearer token123'],
75
+ ['x-api-key', 'api-key-value'],
76
+ ];
77
+
78
+ // Act
79
+ const result = headersToRecord(headersArray);
80
+
81
+ // Assert
82
+ expect(result).toEqual({
83
+ 'content-type': 'application/json',
84
+ 'authorization': 'Bearer token123',
85
+ 'x-api-key': 'api-key-value',
86
+ });
87
+ });
88
+
89
+ it('should handle empty array', () => {
90
+ // Arrange
91
+ const headersArray: [string, string][] = [];
92
+
93
+ // Act
94
+ const result = headersToRecord(headersArray);
95
+
96
+ // Assert
97
+ expect(result).toEqual({});
98
+ });
99
+
100
+ it('should handle array with single header', () => {
101
+ // Arrange
102
+ const headersArray: [string, string][] = [['x-single-header', 'single-value']];
103
+
104
+ // Act
105
+ const result = headersToRecord(headersArray);
106
+
107
+ // Assert
108
+ expect(result).toEqual({
109
+ 'x-single-header': 'single-value',
110
+ });
111
+ });
112
+ });
113
+
114
+ describe('Plain object', () => {
115
+ it('should convert plain object to record', () => {
116
+ // Arrange
117
+ const headersObj = {
118
+ 'content-type': 'application/json',
119
+ 'authorization': 'Bearer token123',
120
+ 'x-custom': 'value',
121
+ };
122
+
123
+ // Act
124
+ const result = headersToRecord(headersObj);
125
+
126
+ // Assert
127
+ expect(result).toEqual({
128
+ 'content-type': 'application/json',
129
+ 'authorization': 'Bearer token123',
130
+ 'x-custom': 'value',
131
+ });
132
+ });
133
+
134
+ it('should handle empty object', () => {
135
+ // Arrange
136
+ const headersObj = {};
137
+
138
+ // Act
139
+ const result = headersToRecord(headersObj);
140
+
141
+ // Assert
142
+ expect(result).toEqual({});
143
+ });
144
+
145
+ it('should handle object with special characters in values', () => {
146
+ // Arrange
147
+ const headersObj = {
148
+ 'authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9',
149
+ 'x-special': 'value with spaces and symbols: !@#$%',
150
+ };
151
+
152
+ // Act
153
+ const result = headersToRecord(headersObj);
154
+
155
+ // Assert
156
+ expect(result).toEqual({
157
+ 'authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9',
158
+ 'x-special': 'value with spaces and symbols: !@#$%',
159
+ });
160
+ });
161
+ });
162
+
163
+ describe('Restricted headers filtering', () => {
164
+ it('should remove "host" header from Headers instance', () => {
165
+ // Arrange
166
+ const headers = new Headers();
167
+ headers.append('host', 'example.com');
168
+ headers.append('content-type', 'application/json');
169
+
170
+ // Act
171
+ const result = headersToRecord(headers);
172
+
173
+ // Assert
174
+ expect(result).not.toHaveProperty('host');
175
+ expect(result).toEqual({
176
+ 'content-type': 'application/json',
177
+ });
178
+ });
179
+
180
+ it('should remove "connection" header from array', () => {
181
+ // Arrange
182
+ const headersArray: [string, string][] = [
183
+ ['connection', 'keep-alive'],
184
+ ['authorization', 'Bearer token'],
185
+ ];
186
+
187
+ // Act
188
+ const result = headersToRecord(headersArray);
189
+
190
+ // Assert
191
+ expect(result).not.toHaveProperty('connection');
192
+ expect(result).toEqual({
193
+ authorization: 'Bearer token',
194
+ });
195
+ });
196
+
197
+ it('should remove "content-length" header from plain object', () => {
198
+ // Arrange
199
+ const headersObj = {
200
+ 'content-length': '1234',
201
+ 'content-type': 'application/json',
202
+ };
203
+
204
+ // Act
205
+ const result = headersToRecord(headersObj);
206
+
207
+ // Assert
208
+ expect(result).not.toHaveProperty('content-length');
209
+ expect(result).toEqual({
210
+ 'content-type': 'application/json',
211
+ });
212
+ });
213
+
214
+ it('should remove all restricted headers (host, connection, content-length)', () => {
215
+ // Arrange
216
+ const headersObj = {
217
+ 'host': 'example.com',
218
+ 'connection': 'keep-alive',
219
+ 'content-length': '1234',
220
+ 'authorization': 'Bearer token',
221
+ 'content-type': 'application/json',
222
+ };
223
+
224
+ // Act
225
+ const result = headersToRecord(headersObj);
226
+
227
+ // Assert
228
+ expect(result).not.toHaveProperty('host');
229
+ expect(result).not.toHaveProperty('connection');
230
+ expect(result).not.toHaveProperty('content-length');
231
+ expect(result).toEqual({
232
+ 'authorization': 'Bearer token',
233
+ 'content-type': 'application/json',
234
+ });
235
+ });
236
+
237
+ it('should handle case when only restricted headers are present', () => {
238
+ // Arrange
239
+ const headersObj = {
240
+ 'host': 'example.com',
241
+ 'connection': 'keep-alive',
242
+ 'content-length': '1234',
243
+ };
244
+
245
+ // Act
246
+ const result = headersToRecord(headersObj);
247
+
248
+ // Assert
249
+ expect(result).toEqual({});
250
+ });
251
+ });
252
+
253
+ describe('Edge cases', () => {
254
+ it('should handle headers with empty string values', () => {
255
+ // Arrange
256
+ const headersObj = {
257
+ 'x-empty': '',
258
+ 'x-normal': 'value',
259
+ };
260
+
261
+ // Act
262
+ const result = headersToRecord(headersObj);
263
+
264
+ // Assert
265
+ expect(result).toEqual({
266
+ 'x-empty': '',
267
+ 'x-normal': 'value',
268
+ });
269
+ });
270
+
271
+ it('should handle headers with numeric-like values as strings', () => {
272
+ // Arrange
273
+ const headersArray: [string, string][] = [
274
+ ['x-request-id', '12345'],
275
+ ['x-retry-count', '3'],
276
+ ];
277
+
278
+ // Act
279
+ const result = headersToRecord(headersArray);
280
+
281
+ // Assert
282
+ expect(result).toEqual({
283
+ 'x-request-id': '12345',
284
+ 'x-retry-count': '3',
285
+ });
286
+ });
287
+
288
+ it('should handle case-sensitive header names', () => {
289
+ // Arrange
290
+ const headersObj = {
291
+ 'Content-Type': 'application/json',
292
+ 'Authorization': 'Bearer token',
293
+ };
294
+
295
+ // Act
296
+ const result = headersToRecord(headersObj);
297
+
298
+ // Assert
299
+ expect(result).toEqual({
300
+ 'Content-Type': 'application/json',
301
+ 'Authorization': 'Bearer token',
302
+ });
303
+ });
304
+
305
+ it('should preserve header order from array input', () => {
306
+ // Arrange
307
+ const headersArray: [string, string][] = [
308
+ ['z-last', 'last'],
309
+ ['a-first', 'first'],
310
+ ['m-middle', 'middle'],
311
+ ];
312
+
313
+ // Act
314
+ const result = headersToRecord(headersArray);
315
+
316
+ // Assert
317
+ expect(Object.keys(result)).toEqual(['z-last', 'a-first', 'm-middle']);
318
+ });
319
+ });
320
+
321
+ describe('Real-world scenarios', () => {
322
+ it('should handle typical SSE request headers', () => {
323
+ // Arrange
324
+ const headers = new Headers();
325
+ headers.append('accept', 'text/event-stream');
326
+ headers.append('content-type', 'application/json');
327
+ headers.append('authorization', 'Bearer abc123');
328
+ headers.append('cache-control', 'no-cache');
329
+ headers.append('connection', 'keep-alive'); // Should be filtered
330
+
331
+ // Act
332
+ const result = headersToRecord(headers);
333
+
334
+ // Assert
335
+ expect(result).toEqual({
336
+ 'accept': 'text/event-stream',
337
+ 'content-type': 'application/json',
338
+ 'authorization': 'Bearer abc123',
339
+ 'cache-control': 'no-cache',
340
+ });
341
+ expect(result).not.toHaveProperty('connection');
342
+ });
343
+
344
+ it('should handle API request headers with custom fields', () => {
345
+ // Arrange
346
+ const headersObj = {
347
+ 'content-type': 'application/json',
348
+ 'x-api-key': 'secret-key',
349
+ 'x-request-id': 'req-123',
350
+ 'user-agent': 'MyApp/1.0',
351
+ 'host': 'api.example.com', // Should be filtered
352
+ 'content-length': '256', // Should be filtered
353
+ };
354
+
355
+ // Act
356
+ const result = headersToRecord(headersObj);
357
+
358
+ // Assert
359
+ expect(result).toEqual({
360
+ 'content-type': 'application/json',
361
+ 'x-api-key': 'secret-key',
362
+ 'x-request-id': 'req-123',
363
+ 'user-agent': 'MyApp/1.0',
364
+ });
365
+ });
366
+ });
367
+ });
@@ -13,7 +13,7 @@ const bedrockChatModels: AIChatModelCard[] = [
13
13
  'Claude Opus 4.5 是 Anthropic 的旗舰模型,结合了卓越的智能与可扩展性能,适合需要最高质量回应和推理能力的复杂任务。',
14
14
  displayName: 'Claude Opus 4.5',
15
15
  enabled: true,
16
- id: 'us.anthropic.claude-opus-4-5-20251101-v1:0',
16
+ id: 'global.anthropic.claude-opus-4-5-20251101-v1:0',
17
17
  maxOutput: 64_000,
18
18
  pricing: {
19
19
  units: [
@@ -23,6 +23,9 @@ const bedrockChatModels: AIChatModelCard[] = [
23
23
  ],
24
24
  },
25
25
  releasedAt: '2025-11-24',
26
+ settings: {
27
+ extendParams: ['disableContextCaching', 'enableReasoning', 'reasoningBudgetToken'],
28
+ },
26
29
  type: 'chat',
27
30
  },
28
31
  {
@@ -45,6 +48,9 @@ const bedrockChatModels: AIChatModelCard[] = [
45
48
  ],
46
49
  },
47
50
  releasedAt: '2025-09-29',
51
+ settings: {
52
+ extendParams: ['disableContextCaching', 'enableReasoning', 'reasoningBudgetToken'],
53
+ },
48
54
  type: 'chat',
49
55
  },
50
56
  {
@@ -68,6 +74,9 @@ const bedrockChatModels: AIChatModelCard[] = [
68
74
  ],
69
75
  },
70
76
  releasedAt: '2025-10-15',
77
+ settings: {
78
+ extendParams: ['disableContextCaching', 'enableReasoning', 'reasoningBudgetToken'],
79
+ },
71
80
  type: 'chat',
72
81
  },
73
82
  /*
@@ -103,7 +112,7 @@ const bedrockChatModels: AIChatModelCard[] = [
103
112
  'Claude 3.7 sonnet 是 Anthropic 最快的下一代模型。与 Claude 3 Haiku 相比,Claude 3.7 Sonnet 在各项技能上都有所提升,并在许多智力基准测试中超越了上一代最大的模型 Claude 3 Opus。',
104
113
  displayName: 'Claude 3.7 Sonnet',
105
114
  id: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0',
106
- maxOutput: 8192,
115
+ maxOutput: 64_000,
107
116
  pricing: {
108
117
  units: [
109
118
  { name: 'textInput', rate: 3, strategy: 'fixed', unit: 'millionTokens' },
@@ -111,6 +120,9 @@ const bedrockChatModels: AIChatModelCard[] = [
111
120
  ],
112
121
  },
113
122
  releasedAt: '2025-02-24',
123
+ settings: {
124
+ extendParams: ['disableContextCaching', 'enableReasoning', 'reasoningBudgetToken'],
125
+ },
114
126
  type: 'chat',
115
127
  },
116
128
  {
@@ -131,6 +143,9 @@ const bedrockChatModels: AIChatModelCard[] = [
131
143
  ],
132
144
  },
133
145
  releasedAt: '2024-10-22',
146
+ settings: {
147
+ extendParams: ['disableContextCaching'],
148
+ },
134
149
  type: 'chat',
135
150
  },
136
151
  {
@@ -151,6 +166,9 @@ const bedrockChatModels: AIChatModelCard[] = [
151
166
  ],
152
167
  },
153
168
  releasedAt: '2024-10-22',
169
+ settings: {
170
+ extendParams: ['disableContextCaching'],
171
+ },
154
172
  type: 'chat',
155
173
  },
156
174
  {
@@ -171,6 +189,9 @@ const bedrockChatModels: AIChatModelCard[] = [
171
189
  ],
172
190
  },
173
191
  releasedAt: '2024-06-20',
192
+ settings: {
193
+ extendParams: ['disableContextCaching'],
194
+ },
174
195
  type: 'chat',
175
196
  },
176
197
  {
@@ -191,6 +212,9 @@ const bedrockChatModels: AIChatModelCard[] = [
191
212
  ],
192
213
  },
193
214
  releasedAt: '2024-03-07',
215
+ settings: {
216
+ extendParams: ['disableContextCaching'],
217
+ },
194
218
  type: 'chat',
195
219
  },
196
220
  {
@@ -228,6 +252,9 @@ const bedrockChatModels: AIChatModelCard[] = [
228
252
  ],
229
253
  },
230
254
  releasedAt: '2024-02-29',
255
+ settings: {
256
+ extendParams: ['disableContextCaching'],
257
+ },
231
258
  type: 'chat',
232
259
  },
233
260
  {
@@ -264,6 +291,7 @@ const bedrockChatModels: AIChatModelCard[] = [
264
291
  '一款快速、经济且仍然非常有能力的模型,可以处理包括日常对话、文本分析、总结和文档问答在内的一系列任务。',
265
292
  displayName: 'Claude Instant',
266
293
  id: 'anthropic.claude-instant-v1',
294
+ maxOutput: 4096,
267
295
  pricing: {
268
296
  units: [
269
297
  { name: 'textInput', rate: 0.8, strategy: 'fixed', unit: 'millionTokens' },
@@ -5,14 +5,14 @@ export const systemToUserModels = new Set([
5
5
  'o1-mini-2024-09-12',
6
6
  ]);
7
7
 
8
- // TODO: 临时写法,后续要重构成 model card 展示配置
8
+ // TODO: temporary implementation, needs to be refactored into model card display configuration
9
9
  export const disableStreamModels = new Set([
10
10
  'o1',
11
11
  'o1-2024-12-17',
12
12
  'o1-pro',
13
13
  'o1-pro-2025-03-19',
14
14
  /*
15
- 官网显示不支持,但是实际试下来支持 Streaming,暂时注释掉
15
+ Official documentation shows no support, but actual testing shows Streaming is supported, temporarily commented out
16
16
  'o3-pro',
17
17
  'o3-pro-2025-06-10',
18
18
  */
@@ -38,30 +38,68 @@ export const responsesAPIModels = new Set([
38
38
  'gpt-5-codex',
39
39
  'gpt-5-pro',
40
40
  'gpt-5-pro-2025-10-06',
41
+ 'gpt-5.1-codex',
42
+ 'gpt-5.1-codex-mini',
41
43
  ]);
42
44
 
43
45
  /**
44
- * models support context caching
46
+ * Regex patterns for models that support context caching (3.5+)
45
47
  */
46
- export const contextCachingModels = new Set([
47
- 'claude-opus-4-latest',
48
- 'claude-opus-4-20250514',
49
- 'claude-sonnet-4-latest',
50
- 'claude-sonnet-4-20250514',
51
- 'claude-3-7-sonnet-latest',
52
- 'claude-3-7-sonnet-20250219',
53
- 'claude-3-5-sonnet-latest',
54
- 'claude-3-5-sonnet-20241022',
55
- 'claude-3-5-sonnet-20240620',
56
- 'claude-3-5-haiku-latest',
57
- 'claude-3-5-haiku-20241022',
58
- ]);
48
+ export const contextCachingModelPatterns: RegExp[] = [
49
+ // Claude 4.5 series - Anthropic API
50
+ /^claude-(opus|sonnet|haiku)-4-5-/,
51
+ // Claude 4 series - Anthropic API
52
+ /^claude-(opus|sonnet)-4-/,
53
+ // Claude 3.7 - Anthropic API
54
+ /^claude-3-7-sonnet-/,
55
+ // Claude 3.5 series - Anthropic API
56
+ /^claude-3-5-(sonnet|haiku)-/,
57
+ // OpenRouter format (3.5+)
58
+ /^anthropic\/claude-(opus|sonnet|haiku)-(4\.5|4|3\.7|3\.5)/,
59
+ /^anthropic\/claude-(4\.5|4|3\.7|3\.5)-(opus|sonnet|haiku)/,
60
+ // AWS Bedrock format: [region.]anthropic.claude-xxx
61
+ /anthropic\.claude-(opus|sonnet|haiku)-(4-5|4|3-7|3-5)-/,
62
+ ];
59
63
 
60
- export const thinkingWithToolClaudeModels = new Set([
61
- 'claude-opus-4-latest',
62
- 'claude-opus-4-20250514',
63
- 'claude-sonnet-4-latest',
64
- 'claude-sonnet-4-20250514',
65
- 'claude-3-7-sonnet-latest',
66
- 'claude-3-7-sonnet-20250219',
67
- ]);
64
+ export const isContextCachingModel = (model: string): boolean => {
65
+ return contextCachingModelPatterns.some((pattern) => pattern.test(model));
66
+ };
67
+
68
+ /**
69
+ * Regex patterns for Claude models that support thinking with tools (3.7+)
70
+ */
71
+ export const thinkingWithToolClaudeModelPatterns: RegExp[] = [
72
+ // Claude 4.5 series - Anthropic API
73
+ /^claude-(opus|sonnet|haiku)-4-5-/,
74
+ // Claude 4 series - Anthropic API
75
+ /^claude-(opus|sonnet)-4-/,
76
+ // Claude 3.7 - Anthropic API
77
+ /^claude-3-7-sonnet-/,
78
+ // OpenRouter format (3.7+)
79
+ /^anthropic\/claude-(opus|sonnet|haiku)-(4\.5|4|3\.7)/,
80
+ /^anthropic\/claude-(4\.5|4|3\.7)-(opus|sonnet|haiku)/,
81
+ // AWS Bedrock format: [region.]anthropic.claude-xxx
82
+ /anthropic\.claude-(opus|sonnet|haiku)-(4-5|4|3-7)-/,
83
+ ];
84
+
85
+ export const isThinkingWithToolClaudeModel = (model: string): boolean => {
86
+ return thinkingWithToolClaudeModelPatterns.some((pattern) => pattern.test(model));
87
+ };
88
+
89
+ /**
90
+ * Regex patterns for Claude 4+ models that have temperature/top_p parameter conflict
91
+ * (cannot set both temperature and top_p at the same time)
92
+ */
93
+ export const temperatureTopPConflictModelPatterns: RegExp[] = [
94
+ // Claude 4+ series - Anthropic API (4, 4.1, 4.5)
95
+ /^claude-(opus|sonnet|haiku)-4/,
96
+ // OpenRouter format
97
+ /^anthropic\/claude-(opus|sonnet|haiku)-(4\.5|4\.1|4)/,
98
+ /^anthropic\/claude-(4\.5|4\.1|4)-(opus|sonnet|haiku)/,
99
+ // AWS Bedrock format: [region.]anthropic.claude-xxx
100
+ /anthropic\.claude-(opus|sonnet|haiku)-4/,
101
+ ];
102
+
103
+ export const hasTemperatureTopPConflict = (model: string): boolean => {
104
+ return temperatureTopPConflictModelPatterns.some((pattern) => pattern.test(model));
105
+ };
@@ -223,3 +223,17 @@ export const buildAnthropicTools = (
223
223
  }),
224
224
  );
225
225
  };
226
+
227
+ export const buildSearchTool = (): Anthropic.WebSearchTool20250305 => {
228
+ const maxUses = process.env.ANTHROPIC_MAX_USES;
229
+
230
+ return {
231
+ name: 'web_search',
232
+ type: 'web_search_20250305',
233
+ ...(maxUses &&
234
+ Number.isInteger(Number(maxUses)) &&
235
+ Number(maxUses) > 0 && {
236
+ max_uses: Number(maxUses),
237
+ }),
238
+ };
239
+ };