@elevasis/core 0.46.0 → 0.48.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.
@@ -71,20 +71,47 @@ describe('validateModelConfig', () => {
71
71
  }).not.toThrow()
72
72
  })
73
73
 
74
- it('passes for config without topP', () => {
75
- expect(() => {
76
- validateModelConfig({
77
- model: 'gpt-5',
78
- provider: 'openai',
74
+ it('passes for config without topP', () => {
75
+ expect(() => {
76
+ validateModelConfig({
77
+ model: 'gpt-5',
78
+ provider: 'openai',
79
79
  apiKey: 'test-key',
80
80
  temperature: 1,
81
81
  maxOutputTokens: 8000
82
- })
83
- }).not.toThrow()
84
- })
85
- })
82
+ })
83
+ }).not.toThrow()
84
+ })
85
+
86
+ it.each(['claude-opus-4-8', 'claude-sonnet-4-6', 'claude-haiku-4-5-20251001', 'claude-haiku-4-5'] as const)(
87
+ 'passes for Anthropic %s',
88
+ (model) => {
89
+ expect(() => {
90
+ validateModelConfig({
91
+ model,
92
+ provider: 'anthropic',
93
+ apiKey: 'test-key',
94
+ maxOutputTokens: 4000
95
+ })
96
+ }).not.toThrow()
97
+ }
98
+ )
99
+
100
+ it('passes for Sonnet 4.6 with sampling parameters', () => {
101
+ expect(() => {
102
+ validateModelConfig({
103
+ model: 'claude-sonnet-4-6',
104
+ provider: 'anthropic',
105
+ apiKey: 'test-key',
106
+ temperature: 0.7,
107
+ topP: 0.9,
108
+ maxOutputTokens: 4000
109
+ })
110
+ }).not.toThrow()
111
+ })
112
+ })
86
113
 
87
- describe('Invalid Temperature', () => {
114
+ describe('Invalid Temperature', () => {
88
115
  it('throws for gpt-5 with temperature=0.7', () => {
89
116
  expect(() => {
90
117
  validateModelConfig({
@@ -109,9 +136,9 @@ describe('validateModelConfig', () => {
109
136
  }).toThrow(ModelConfigError)
110
137
  })
111
138
 
112
- it('provides correct error details for invalid temperature', () => {
113
- try {
114
- validateModelConfig({
139
+ it('provides correct error details for invalid temperature', () => {
140
+ try {
141
+ validateModelConfig({
115
142
  model: 'gpt-5',
116
143
  provider: 'openai',
117
144
  apiKey: 'test-key',
@@ -123,10 +150,48 @@ describe('validateModelConfig', () => {
123
150
  expect(error).toBeInstanceOf(ModelConfigError)
124
151
  const modelError = error as ModelConfigError
125
152
  expect(modelError.field).toBe('temperature')
126
- expect(modelError.model).toBe('gpt-5')
127
- }
128
- })
129
- })
153
+ expect(modelError.model).toBe('gpt-5')
154
+ }
155
+ })
156
+
157
+ it('throws for Opus 4.8 with non-default temperature', () => {
158
+ expect(() => {
159
+ validateModelConfig({
160
+ model: 'claude-opus-4-8',
161
+ provider: 'anthropic',
162
+ apiKey: 'test-key',
163
+ temperature: 0.7,
164
+ maxOutputTokens: 4000
165
+ })
166
+ }).toThrow(ModelConfigError)
167
+ })
168
+ })
169
+
170
+ describe('Invalid Anthropic Sampling Parameters', () => {
171
+ it('throws for Opus 4.8 with non-default topP', () => {
172
+ expect(() => {
173
+ validateModelConfig({
174
+ model: 'claude-opus-4-8',
175
+ provider: 'anthropic',
176
+ apiKey: 'test-key',
177
+ topP: 0.9,
178
+ maxOutputTokens: 4000
179
+ })
180
+ }).toThrow(ModelConfigError)
181
+ })
182
+
183
+ it('throws for Opus 4.8 with top_k', () => {
184
+ expect(() => {
185
+ validateModelConfig({
186
+ model: 'claude-opus-4-8',
187
+ provider: 'anthropic',
188
+ apiKey: 'test-key',
189
+ maxOutputTokens: 4000,
190
+ top_k: 40
191
+ } as unknown as Parameters<typeof validateModelConfig>[0])
192
+ }).toThrow(ModelConfigError)
193
+ })
194
+ })
130
195
 
131
196
  describe('Invalid maxOutputTokens', () => {
132
197
  it('throws for maxOutputTokens below minimum', () => {
@@ -81,17 +81,18 @@ describe('AnthropicAdapter', () => {
81
81
  }
82
82
  }
83
83
 
84
- describe('isAnthropicModel', () => {
85
- it('returns true for claude models with anthropic provider', () => {
86
- expect(isAnthropicModel('claude-opus-4-5', 'anthropic')).toBe(true)
87
- expect(isAnthropicModel('claude-sonnet-4-5', 'anthropic')).toBe(true)
88
- expect(isAnthropicModel('claude-haiku-4-5', 'anthropic')).toBe(true)
89
- })
90
-
91
- it('returns false for claude models with other providers', () => {
92
- expect(isAnthropicModel('claude-opus-4-5', 'openrouter')).toBe(false)
93
- expect(isAnthropicModel('claude-sonnet-4-5', 'google')).toBe(false)
94
- })
84
+ describe('isAnthropicModel', () => {
85
+ it('returns true for claude models with anthropic provider', () => {
86
+ expect(isAnthropicModel('claude-opus-4-8', 'anthropic')).toBe(true)
87
+ expect(isAnthropicModel('claude-sonnet-4-6', 'anthropic')).toBe(true)
88
+ expect(isAnthropicModel('claude-haiku-4-5-20251001', 'anthropic')).toBe(true)
89
+ expect(isAnthropicModel('claude-haiku-4-5', 'anthropic')).toBe(true)
90
+ })
91
+
92
+ it('returns false for claude models with other providers', () => {
93
+ expect(isAnthropicModel('claude-opus-4-8', 'openrouter')).toBe(false)
94
+ expect(isAnthropicModel('claude-sonnet-4-6', 'google')).toBe(false)
95
+ })
95
96
 
96
97
  it('returns false for non-claude models', () => {
97
98
  expect(isAnthropicModel('gpt-5', 'anthropic')).toBe(false)
@@ -101,10 +102,10 @@ describe('AnthropicAdapter', () => {
101
102
 
102
103
  describe('Basic Configuration', () => {
103
104
  it('creates adapter without model options', async () => {
104
- const adapter = new AnthropicAdapter({
105
- apiKey: 'test-key',
106
- model: 'claude-sonnet-4-5'
107
- })
105
+ const adapter = new AnthropicAdapter({
106
+ apiKey: 'test-key',
107
+ model: 'claude-sonnet-4-6'
108
+ })
108
109
 
109
110
  mockStreamResponse(mockAnthropicResponse)
110
111
 
@@ -114,11 +115,11 @@ describe('AnthropicAdapter', () => {
114
115
  })
115
116
 
116
117
  it('creates adapter with model options', async () => {
117
- const adapter = new AnthropicAdapter({
118
- apiKey: 'test-key',
119
- model: 'claude-opus-4-5',
120
- modelOptions: {}
121
- })
118
+ const adapter = new AnthropicAdapter({
119
+ apiKey: 'test-key',
120
+ model: 'claude-opus-4-8',
121
+ modelOptions: {}
122
+ })
122
123
 
123
124
  mockStreamResponse(mockAnthropicResponse)
124
125
 
@@ -130,10 +131,10 @@ describe('AnthropicAdapter', () => {
130
131
 
131
132
  describe('Message Translation', () => {
132
133
  it('converts system messages to separate system field', async () => {
133
- const adapter = new AnthropicAdapter({
134
- apiKey: 'test-key',
135
- model: 'claude-sonnet-4-5'
136
- })
134
+ const adapter = new AnthropicAdapter({
135
+ apiKey: 'test-key',
136
+ model: 'claude-sonnet-4-6'
137
+ })
137
138
 
138
139
  mockStreamResponse(mockAnthropicResponse)
139
140
 
@@ -157,10 +158,10 @@ describe('AnthropicAdapter', () => {
157
158
  })
158
159
 
159
160
  it('combines multiple system messages', async () => {
160
- const adapter = new AnthropicAdapter({
161
- apiKey: 'test-key',
162
- model: 'claude-sonnet-4-5'
163
- })
161
+ const adapter = new AnthropicAdapter({
162
+ apiKey: 'test-key',
163
+ model: 'claude-sonnet-4-6'
164
+ })
164
165
 
165
166
  mockStreamResponse(mockAnthropicResponse)
166
167
 
@@ -179,10 +180,10 @@ describe('AnthropicAdapter', () => {
179
180
  })
180
181
 
181
182
  it('handles requests without system messages', async () => {
182
- const adapter = new AnthropicAdapter({
183
- apiKey: 'test-key',
184
- model: 'claude-sonnet-4-5'
185
- })
183
+ const adapter = new AnthropicAdapter({
184
+ apiKey: 'test-key',
185
+ model: 'claude-sonnet-4-6'
186
+ })
186
187
 
187
188
  mockStreamResponse(mockAnthropicResponse)
188
189
 
@@ -197,10 +198,10 @@ describe('AnthropicAdapter', () => {
197
198
  })
198
199
 
199
200
  it('preserves user and assistant roles', async () => {
200
- const adapter = new AnthropicAdapter({
201
- apiKey: 'test-key',
202
- model: 'claude-sonnet-4-5'
203
- })
201
+ const adapter = new AnthropicAdapter({
202
+ apiKey: 'test-key',
203
+ model: 'claude-sonnet-4-6'
204
+ })
204
205
 
205
206
  mockStreamResponse(mockAnthropicResponse)
206
207
 
@@ -223,10 +224,10 @@ describe('AnthropicAdapter', () => {
223
224
 
224
225
  describe('Tool-based Structured Output', () => {
225
226
  it('configures tool_use for structured output', async () => {
226
- const adapter = new AnthropicAdapter({
227
- apiKey: 'test-key',
228
- model: 'claude-sonnet-4-5'
229
- })
227
+ const adapter = new AnthropicAdapter({
228
+ apiKey: 'test-key',
229
+ model: 'claude-sonnet-4-6'
230
+ })
230
231
 
231
232
  mockStreamResponse(mockAnthropicResponse)
232
233
 
@@ -244,10 +245,10 @@ describe('AnthropicAdapter', () => {
244
245
  })
245
246
 
246
247
  it('extracts response from tool_use block', async () => {
247
- const adapter = new AnthropicAdapter({
248
- apiKey: 'test-key',
249
- model: 'claude-sonnet-4-5'
250
- })
248
+ const adapter = new AnthropicAdapter({
249
+ apiKey: 'test-key',
250
+ model: 'claude-sonnet-4-6'
251
+ })
251
252
 
252
253
  mockStreamResponse({
253
254
  content: [
@@ -278,10 +279,10 @@ describe('AnthropicAdapter', () => {
278
279
  })
279
280
 
280
281
  it('throws error when no tool_use block in response', async () => {
281
- const adapter = new AnthropicAdapter({
282
- apiKey: 'test-key',
283
- model: 'claude-sonnet-4-5'
284
- })
282
+ const adapter = new AnthropicAdapter({
283
+ apiKey: 'test-key',
284
+ model: 'claude-sonnet-4-6'
285
+ })
285
286
 
286
287
  mockStreamResponse({
287
288
  content: [{ type: 'text', text: 'Hello!' }],
@@ -296,10 +297,10 @@ describe('AnthropicAdapter', () => {
296
297
 
297
298
  describe('Usage Metadata', () => {
298
299
  it('calculates total tokens from input + output', async () => {
299
- const adapter = new AnthropicAdapter({
300
- apiKey: 'test-key',
301
- model: 'claude-sonnet-4-5'
302
- })
300
+ const adapter = new AnthropicAdapter({
301
+ apiKey: 'test-key',
302
+ model: 'claude-sonnet-4-6'
303
+ })
303
304
 
304
305
  mockStreamResponse({
305
306
  content: [mockAnthropicResponse.content[0]],
@@ -319,10 +320,10 @@ describe('AnthropicAdapter', () => {
319
320
  })
320
321
 
321
322
  it('extracts token usage correctly', async () => {
322
- const adapter = new AnthropicAdapter({
323
- apiKey: 'test-key',
324
- model: 'claude-sonnet-4-5'
325
- })
323
+ const adapter = new AnthropicAdapter({
324
+ apiKey: 'test-key',
325
+ model: 'claude-sonnet-4-6'
326
+ })
326
327
 
327
328
  mockStreamResponse(mockAnthropicResponse)
328
329
 
@@ -338,10 +339,10 @@ describe('AnthropicAdapter', () => {
338
339
 
339
340
  describe('MaxTokens Configuration', () => {
340
341
  it('passes maxOutputTokens to API', async () => {
341
- const adapter = new AnthropicAdapter({
342
- apiKey: 'test-key',
343
- model: 'claude-sonnet-4-5'
344
- })
342
+ const adapter = new AnthropicAdapter({
343
+ apiKey: 'test-key',
344
+ model: 'claude-sonnet-4-6'
345
+ })
345
346
 
346
347
  mockStreamResponse(mockAnthropicResponse)
347
348
 
@@ -355,10 +356,10 @@ describe('AnthropicAdapter', () => {
355
356
  })
356
357
 
357
358
  it('uses default 4000 when maxOutputTokens not specified', async () => {
358
- const adapter = new AnthropicAdapter({
359
- apiKey: 'test-key',
360
- model: 'claude-sonnet-4-5'
361
- })
359
+ const adapter = new AnthropicAdapter({
360
+ apiKey: 'test-key',
361
+ model: 'claude-sonnet-4-6'
362
+ })
362
363
 
363
364
  mockStreamResponse(mockAnthropicResponse)
364
365
 
@@ -373,12 +374,12 @@ describe('AnthropicAdapter', () => {
373
374
  })
374
375
  })
375
376
 
376
- describe('Temperature and TopP', () => {
377
- it('passes temperature to API', async () => {
378
- const adapter = new AnthropicAdapter({
379
- apiKey: 'test-key',
380
- model: 'claude-sonnet-4-5'
381
- })
377
+ describe('Temperature and TopP', () => {
378
+ it('passes temperature to API', async () => {
379
+ const adapter = new AnthropicAdapter({
380
+ apiKey: 'test-key',
381
+ model: 'claude-sonnet-4-6'
382
+ })
382
383
 
383
384
  mockStreamResponse(mockAnthropicResponse)
384
385
 
@@ -392,10 +393,10 @@ describe('AnthropicAdapter', () => {
392
393
  })
393
394
 
394
395
  it('passes topP to API', async () => {
395
- const adapter = new AnthropicAdapter({
396
- apiKey: 'test-key',
397
- model: 'claude-sonnet-4-5'
398
- })
396
+ const adapter = new AnthropicAdapter({
397
+ apiKey: 'test-key',
398
+ model: 'claude-sonnet-4-6'
399
+ })
399
400
 
400
401
  mockStreamResponse(mockAnthropicResponse)
401
402
 
@@ -404,17 +405,37 @@ describe('AnthropicAdapter', () => {
404
405
  topP: 0.9
405
406
  })
406
407
 
407
- const callParams = mockMessagesStream.mock.calls[0][0]
408
- expect(callParams.top_p).toBe(0.9)
409
- })
410
- })
408
+ const callParams = mockMessagesStream.mock.calls[0][0]
409
+ expect(callParams.top_p).toBe(0.9)
410
+ })
411
+
412
+ it('omits sampling parameters for Opus 4.8', async () => {
413
+ const adapter = new AnthropicAdapter({
414
+ apiKey: 'test-key',
415
+ model: 'claude-opus-4-8'
416
+ })
417
+
418
+ mockStreamResponse(mockAnthropicResponse)
419
+
420
+ await adapter.generate({
421
+ ...baseRequest,
422
+ temperature: 0.7,
423
+ topP: 0.9
424
+ })
425
+
426
+ const callParams = mockMessagesStream.mock.calls[0][0]
427
+ expect(callParams).not.toHaveProperty('temperature')
428
+ expect(callParams).not.toHaveProperty('top_p')
429
+ expect(callParams).not.toHaveProperty('top_k')
430
+ })
431
+ })
411
432
 
412
433
  describe('Error Handling', () => {
413
434
  it('propagates API errors', async () => {
414
- const adapter = new AnthropicAdapter({
415
- apiKey: 'test-key',
416
- model: 'claude-sonnet-4-5'
417
- })
435
+ const adapter = new AnthropicAdapter({
436
+ apiKey: 'test-key',
437
+ model: 'claude-sonnet-4-6'
438
+ })
418
439
 
419
440
  const apiError = new Error('API rate limit exceeded')
420
441
  mockStreamError(apiError)
@@ -425,10 +446,10 @@ describe('AnthropicAdapter', () => {
425
446
 
426
447
  describe('Multiple Calls', () => {
427
448
  it('reuses client across multiple generate calls', async () => {
428
- const adapter = new AnthropicAdapter({
429
- apiKey: 'test-key',
430
- model: 'claude-opus-4-5'
431
- })
449
+ const adapter = new AnthropicAdapter({
450
+ apiKey: 'test-key',
451
+ model: 'claude-sonnet-4-6'
452
+ })
432
453
 
433
454
  mockStreamResponsePersistent(mockAnthropicResponse)
434
455
 
@@ -443,7 +464,7 @@ describe('AnthropicAdapter', () => {
443
464
  })
444
465
 
445
466
  describe('Model Selection', () => {
446
- it.each(['claude-opus-4-5', 'claude-sonnet-4-5', 'claude-haiku-4-5'] as const)(
467
+ it.each(['claude-opus-4-8', 'claude-sonnet-4-6', 'claude-haiku-4-5-20251001', 'claude-haiku-4-5'] as const)(
447
468
  'supports %s model',
448
469
  async (model) => {
449
470
  const adapter = new AnthropicAdapter({
@@ -25,19 +25,34 @@ export interface AnthropicAdapterConfig {
25
25
  }
26
26
 
27
27
  /**
28
- * Check if the model and provider combination is for Anthropic
29
- *
30
- * @param model - Model identifier (e.g., 'claude-opus-4-5-20251101')
31
- * @param provider - Provider name
32
- * @returns True if this is an Anthropic model configuration
33
- */
34
- export function isAnthropicModel(model: LLMModel, provider: string): boolean {
35
- return model.startsWith('claude-') && provider === 'anthropic'
36
- }
37
-
38
- /**
39
- * Anthropic Claude Adapter - Implements universal protocol
40
- * Uses tool_use for structured output (Anthropic's recommended approach)
28
+ * Check if the model and provider combination is for Anthropic
29
+ *
30
+ * @param model - Model identifier (e.g., 'claude-opus-4-8')
31
+ * @param provider - Provider name
32
+ * @returns True if this is an Anthropic model configuration
33
+ */
34
+ export function isAnthropicModel(model: LLMModel, provider: string): boolean {
35
+ return model.startsWith('claude-') && provider === 'anthropic'
36
+ }
37
+
38
+ function shouldOmitSamplingParameters(model: LLMModel): boolean {
39
+ return model === 'claude-opus-4-8'
40
+ }
41
+
42
+ function getSamplingParameters(model: LLMModel, request: LLMGenerateRequest): { temperature?: number; top_p?: number } {
43
+ if (shouldOmitSamplingParameters(model)) {
44
+ return {}
45
+ }
46
+
47
+ return {
48
+ ...(request.temperature !== undefined ? { temperature: request.temperature } : {}),
49
+ ...(request.topP !== undefined ? { top_p: request.topP } : {})
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Anthropic Claude Adapter - Implements universal protocol
55
+ * Uses tool_use for structured output (Anthropic's recommended approach)
41
56
  *
42
57
  * Key differences from other adapters:
43
58
  * - System messages go in separate `system` field (not in messages array)
@@ -87,10 +102,10 @@ export class AnthropicAdapter implements LLMAdapter {
87
102
  // This guarantees schema compliance without requiring JSON mode
88
103
  // Stream + finalMessage() to avoid SDK non-streaming timeout rejection
89
104
  // at high max_tokens values (SDK calculates estimated time > 10 min → error)
90
- const stream = client.messages.stream(
91
- {
92
- model: this.model,
93
- max_tokens: request.maxOutputTokens || 4000, // Required for Anthropic
105
+ const stream = client.messages.stream(
106
+ {
107
+ model: this.model,
108
+ max_tokens: request.maxOutputTokens || 4000, // Required for Anthropic
94
109
  system: systemContent,
95
110
  messages: anthropicMessages,
96
111
  tools: [
@@ -99,13 +114,12 @@ export class AnthropicAdapter implements LLMAdapter {
99
114
  description: 'Return structured output matching the required schema',
100
115
  input_schema: request.responseSchema as import('@anthropic-ai/sdk').Anthropic.Tool['input_schema']
101
116
  }
102
- ],
103
- tool_choice: { type: 'tool', name: 'structured_output' },
104
- temperature: request.temperature,
105
- top_p: request.topP
106
- },
107
- { signal: composeSignal(DEFAULT_LLM_TIMEOUT, request.signal) }
108
- )
117
+ ],
118
+ tool_choice: { type: 'tool', name: 'structured_output' },
119
+ ...getSamplingParameters(this.model, request)
120
+ },
121
+ { signal: composeSignal(DEFAULT_LLM_TIMEOUT, request.signal) }
122
+ )
109
123
  const response = await stream.finalMessage()
110
124
 
111
125
  // Extract structured output from tool_use block