@startsimpli/llm 0.4.14 → 0.4.16
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/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +171 -0
- package/src/index.ts +5 -1
- package/src/providers/anthropic.ts +146 -42
- package/src/providers/openai.ts +4 -0
- package/src/types.ts +8 -2
package/package.json
CHANGED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the Anthropic provider actually PUTS ON THE WIRE (bd startsim-768w.38).
|
|
3
|
+
*
|
|
4
|
+
* These assert the request body, not the response, because the bug they exist
|
|
5
|
+
* for was a 400 from a parameter we sent — `temperature` is rejected outright by
|
|
6
|
+
* the Claude 5 family, so a provider that always sends it can never call the
|
|
7
|
+
* models we want to be on.
|
|
8
|
+
*
|
|
9
|
+
* The acceptance boundary below is not invented. It was measured against
|
|
10
|
+
* api.anthropic.com with a real key on 2026-08-10:
|
|
11
|
+
*
|
|
12
|
+
* claude-sonnet-5 temperature -> 400 `temperature` is deprecated for this model.
|
|
13
|
+
* claude-opus-5 temperature -> 400 (same; also top_p, top_k)
|
|
14
|
+
* claude-fable-5 temperature -> 400
|
|
15
|
+
* claude-opus-4-8 temperature -> 400
|
|
16
|
+
* claude-opus-4-7 temperature -> 400
|
|
17
|
+
* claude-opus-4-6 temperature -> 200
|
|
18
|
+
* claude-sonnet-4-6 temperature -> 200
|
|
19
|
+
* claude-sonnet-4-5-* temperature -> 200
|
|
20
|
+
*/
|
|
21
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
22
|
+
|
|
23
|
+
import { AnthropicProvider } from '../providers/anthropic';
|
|
24
|
+
import { LLMProviderError } from '../errors';
|
|
25
|
+
|
|
26
|
+
interface Sent {
|
|
27
|
+
model: string;
|
|
28
|
+
temperature?: number;
|
|
29
|
+
top_p?: number;
|
|
30
|
+
max_tokens: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const OK_BODY = {
|
|
34
|
+
id: 'msg_1',
|
|
35
|
+
type: 'message',
|
|
36
|
+
role: 'assistant',
|
|
37
|
+
content: [{ type: 'text', text: '{"ok":true}' }],
|
|
38
|
+
model: 'server-reported-model',
|
|
39
|
+
stop_reason: 'end_turn',
|
|
40
|
+
usage: { input_tokens: 10, output_tokens: 4 },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function ok() {
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
status: 200,
|
|
47
|
+
json: async () => OK_BODY,
|
|
48
|
+
} as unknown as Response;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function deprecated(param: string) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
status: 400,
|
|
55
|
+
json: async () => ({
|
|
56
|
+
type: 'error',
|
|
57
|
+
error: {
|
|
58
|
+
type: 'invalid_request_error',
|
|
59
|
+
message: `\`${param}\` is deprecated for this model.`,
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
} as unknown as Response;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let sent: Sent[];
|
|
66
|
+
let fetchMock: ReturnType<typeof vi.fn>;
|
|
67
|
+
|
|
68
|
+
function provider(defaultModel: string) {
|
|
69
|
+
return new AnthropicProvider({ apiKey: 'sk-ant-test', defaultModel, defaultTemperature: 0.2 });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
sent = [];
|
|
74
|
+
fetchMock = vi.fn(async (_url: unknown, init: RequestInit) => {
|
|
75
|
+
sent.push(JSON.parse(String(init.body)) as Sent);
|
|
76
|
+
return ok();
|
|
77
|
+
});
|
|
78
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
vi.unstubAllGlobals();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('AnthropicProvider request body — sampling parameters', () => {
|
|
86
|
+
it('omits temperature for a Claude 5 model that rejects it', async () => {
|
|
87
|
+
await provider('claude-sonnet-5').generate('hi', 'sys', { temperature: 0.2 });
|
|
88
|
+
|
|
89
|
+
expect(sent).toHaveLength(1);
|
|
90
|
+
expect(sent[0].model).toBe('claude-sonnet-5');
|
|
91
|
+
expect(sent[0]).not.toHaveProperty('temperature');
|
|
92
|
+
// The parameters that ARE required still go, or the call is malformed.
|
|
93
|
+
expect(sent[0].max_tokens).toBeGreaterThan(0);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it.each(['claude-opus-5', 'claude-fable-5', 'claude-opus-4-8', 'claude-opus-4-7'])(
|
|
97
|
+
'omits temperature for %s',
|
|
98
|
+
async (model) => {
|
|
99
|
+
await provider(model).generate('hi', 'sys', { temperature: 0.2 });
|
|
100
|
+
expect(sent[0]).not.toHaveProperty('temperature');
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
it('still sends temperature for a 4.6-era model, which accepts it', async () => {
|
|
105
|
+
await provider('claude-sonnet-4-6').generate('hi', 'sys', { temperature: 0.2 });
|
|
106
|
+
|
|
107
|
+
expect(sent[0].model).toBe('claude-sonnet-4-6');
|
|
108
|
+
expect(sent[0].temperature).toBe(0.2);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it.each(['claude-opus-4-6', 'claude-sonnet-4-5-20250929', 'claude-haiku-4-5-20251001'])(
|
|
112
|
+
'still sends temperature for %s',
|
|
113
|
+
async (model) => {
|
|
114
|
+
await provider(model).generate('hi', 'sys', { temperature: 0.35 });
|
|
115
|
+
expect(sent[0].temperature).toBe(0.35);
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
it('omits temperature for a model id it has never heard of', async () => {
|
|
120
|
+
// The whole point of the allowlist shape: an id that does not exist yet gets
|
|
121
|
+
// the request that CANNOT 400, not the one that historically could.
|
|
122
|
+
await provider('claude-sonnet-7-hypothetical').generate('hi', 'sys', { temperature: 0.2 });
|
|
123
|
+
expect(sent[0]).not.toHaveProperty('temperature');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('names a model that is actually callable as the package default', async () => {
|
|
127
|
+
// The old default, claude-sonnet-4-20250514, was retired and 404s. A default
|
|
128
|
+
// nobody can call is a broken default.
|
|
129
|
+
const p = new AnthropicProvider({ apiKey: 'sk-ant-test' });
|
|
130
|
+
await p.generate('hi', 'sys');
|
|
131
|
+
expect(sent[0].model).toBe(p.defaultModel);
|
|
132
|
+
expect(p.availableModels).toContain(p.defaultModel);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('AnthropicProvider — recovering from a parameter the model rejects', () => {
|
|
137
|
+
it('drops the named parameter and retries once when the API calls it deprecated', async () => {
|
|
138
|
+
// A model on the allowlist that the API has since changed its mind about:
|
|
139
|
+
// the safety net must un-break it without waiting for a release.
|
|
140
|
+
fetchMock.mockImplementationOnce(async (_url: unknown, init: RequestInit) => {
|
|
141
|
+
sent.push(JSON.parse(String(init.body)) as Sent);
|
|
142
|
+
return deprecated('temperature');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const response = await provider('claude-opus-4-6').generate('hi', 'sys', { temperature: 0.2 });
|
|
146
|
+
|
|
147
|
+
expect(sent).toHaveLength(2);
|
|
148
|
+
expect(sent[0].temperature).toBe(0.2);
|
|
149
|
+
expect(sent[1]).not.toHaveProperty('temperature');
|
|
150
|
+
expect(response.content).toBe('{"ok":true}');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('does not retry a 400 that is not about a deprecated parameter', async () => {
|
|
154
|
+
fetchMock.mockImplementation(async (_url: unknown, init: RequestInit) => {
|
|
155
|
+
sent.push(JSON.parse(String(init.body)) as Sent);
|
|
156
|
+
return {
|
|
157
|
+
ok: false,
|
|
158
|
+
status: 400,
|
|
159
|
+
json: async () => ({
|
|
160
|
+
type: 'error',
|
|
161
|
+
error: { type: 'invalid_request_error', message: 'messages: at least one message required' },
|
|
162
|
+
}),
|
|
163
|
+
} as unknown as Response;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await expect(provider('claude-sonnet-4-6').generate('hi', 'sys')).rejects.toBeInstanceOf(
|
|
167
|
+
LLMProviderError,
|
|
168
|
+
);
|
|
169
|
+
expect(sent).toHaveLength(1);
|
|
170
|
+
});
|
|
171
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -23,7 +23,11 @@ export type { ILLMProvider } from './providers/interface';
|
|
|
23
23
|
|
|
24
24
|
// Provider implementations
|
|
25
25
|
export { OpenAIProvider, createOpenAIProvider } from './providers/openai';
|
|
26
|
-
export {
|
|
26
|
+
export {
|
|
27
|
+
AnthropicProvider,
|
|
28
|
+
createAnthropicProvider,
|
|
29
|
+
supportsSamplingParams,
|
|
30
|
+
} from './providers/anthropic';
|
|
27
31
|
export { MockLLMProvider, setMockBehavior, resetMockBehavior } from './providers/mock';
|
|
28
32
|
export type { MockBehavior } from './providers/mock';
|
|
29
33
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Anthropic Provider Implementation
|
|
3
3
|
*
|
|
4
4
|
* Uses native fetch to minimize dependencies.
|
|
5
|
-
* Supports Claude
|
|
5
|
+
* Supports the Claude 5 family and the Claude 4.x models still served.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ILLMProvider } from './interface';
|
|
@@ -21,6 +21,10 @@ interface AnthropicRequest {
|
|
|
21
21
|
max_tokens: number;
|
|
22
22
|
messages: AnthropicMessage[];
|
|
23
23
|
system?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Sent ONLY for models known to accept it — see `supportsSamplingParams`.
|
|
26
|
+
* The Claude 5 family rejects it outright.
|
|
27
|
+
*/
|
|
24
28
|
temperature?: number;
|
|
25
29
|
}
|
|
26
30
|
|
|
@@ -45,30 +49,94 @@ interface AnthropicErrorResponse {
|
|
|
45
49
|
};
|
|
46
50
|
}
|
|
47
51
|
|
|
52
|
+
/**
|
|
53
|
+
* The models this provider advertises. Verified callable against
|
|
54
|
+
* api.anthropic.com on 2026-08-10 — every id in the previous version of this
|
|
55
|
+
* list except the two 4.6 entries had been retired and returned 404, which is
|
|
56
|
+
* the same staleness failure the temperature handling below is designed around.
|
|
57
|
+
*/
|
|
48
58
|
const ANTHROPIC_MODELS = [
|
|
59
|
+
'claude-opus-5',
|
|
60
|
+
'claude-sonnet-5',
|
|
61
|
+
'claude-fable-5',
|
|
62
|
+
'claude-opus-4-8',
|
|
63
|
+
'claude-opus-4-7',
|
|
49
64
|
'claude-opus-4-6',
|
|
50
|
-
'claude-opus-4-20250514',
|
|
51
65
|
'claude-sonnet-4-6',
|
|
52
|
-
'claude-
|
|
53
|
-
'claude-
|
|
54
|
-
'claude-
|
|
55
|
-
'claude-3-opus-20240229',
|
|
56
|
-
'claude-3-sonnet-20240229',
|
|
57
|
-
'claude-3-haiku-20240307',
|
|
66
|
+
'claude-opus-4-5-20251101',
|
|
67
|
+
'claude-sonnet-4-5-20250929',
|
|
68
|
+
'claude-haiku-4-5-20251001',
|
|
58
69
|
] as const;
|
|
59
70
|
|
|
71
|
+
/** USD per 1K tokens (list prices per million, divided by 1000). */
|
|
60
72
|
const MODEL_COSTS: Record<string, { input: number; output: number }> = {
|
|
61
|
-
'claude-
|
|
62
|
-
'claude-opus-
|
|
73
|
+
'claude-fable-5': { input: 0.01, output: 0.05 },
|
|
74
|
+
'claude-opus-5': { input: 0.005, output: 0.025 },
|
|
75
|
+
'claude-opus-4-8': { input: 0.005, output: 0.025 },
|
|
76
|
+
'claude-opus-4-7': { input: 0.005, output: 0.025 },
|
|
77
|
+
'claude-opus-4-6': { input: 0.005, output: 0.025 },
|
|
78
|
+
'claude-opus-4-5-20251101': { input: 0.005, output: 0.025 },
|
|
79
|
+
'claude-sonnet-5': { input: 0.003, output: 0.015 },
|
|
63
80
|
'claude-sonnet-4-6': { input: 0.003, output: 0.015 },
|
|
64
|
-
'claude-sonnet-4-
|
|
65
|
-
'claude-
|
|
66
|
-
'claude-3-5-haiku-20241022': { input: 0.0008, output: 0.004 },
|
|
67
|
-
'claude-3-opus-20240229': { input: 0.015, output: 0.075 },
|
|
68
|
-
'claude-3-sonnet-20240229': { input: 0.003, output: 0.015 },
|
|
69
|
-
'claude-3-haiku-20240307': { input: 0.00025, output: 0.00125 },
|
|
81
|
+
'claude-sonnet-4-5-20250929': { input: 0.003, output: 0.015 },
|
|
82
|
+
'claude-haiku-4-5-20251001': { input: 0.001, output: 0.005 },
|
|
70
83
|
};
|
|
71
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Model-id prefixes KNOWN to accept the sampling parameters (`temperature`,
|
|
87
|
+
* `top_p`, `top_k`). Anything not on this list is sent WITHOUT them.
|
|
88
|
+
*
|
|
89
|
+
* WHY AN ALLOWLIST, NOT A LIST OF MODELS THAT REJECT IT. A deny-list of literal
|
|
90
|
+
* ids — the shape `OpenAIProvider` uses for its `gpt-5` temperature=1 special
|
|
91
|
+
* case — is correct exactly until the next model ships, and then it silently
|
|
92
|
+
* sends a parameter the API refuses. That is not hypothetical: it is the bug
|
|
93
|
+
* this list exists to fix, and the failure is total (every call 400s) rather
|
|
94
|
+
* than degraded. Inverting it makes an unknown id — i.e. every future model —
|
|
95
|
+
* get the request that CANNOT 400. The two error cases are not symmetric:
|
|
96
|
+
* omitting temperature from a model that would have accepted it costs some
|
|
97
|
+
* determinism; sending it to a model that rejects it costs the whole feature.
|
|
98
|
+
*
|
|
99
|
+
* WHY NOT A VERSION COMPARISON. Measured against api.anthropic.com 2026-08-10:
|
|
100
|
+
* 4.5 and 4.6 accept `temperature`; 4.7, 4.8 and the entire 5 family return
|
|
101
|
+
* `400 \`temperature\` is deprecated for this model.` The boundary sits between
|
|
102
|
+
* 4.6 and 4.7, which no plausible "removed in v5" or "removed in 4.8" rule
|
|
103
|
+
* predicts — so this is recorded from observation, not derived from a guess.
|
|
104
|
+
*
|
|
105
|
+
* The list is also closed in the direction that matters: it can only ever go
|
|
106
|
+
* stale by omitting a NEW model that would have accepted the parameter, never
|
|
107
|
+
* by naming one that rejects it. Should that happen, the retry in `generate()`
|
|
108
|
+
* covers it without a release.
|
|
109
|
+
*/
|
|
110
|
+
const SAMPLING_PARAM_MODEL_PREFIXES = [
|
|
111
|
+
'claude-3',
|
|
112
|
+
'claude-opus-4-5',
|
|
113
|
+
'claude-opus-4-6',
|
|
114
|
+
'claude-sonnet-4-5',
|
|
115
|
+
'claude-sonnet-4-6',
|
|
116
|
+
'claude-haiku-4-5',
|
|
117
|
+
'claude-haiku-4-6',
|
|
118
|
+
'claude-opus-4-2025',
|
|
119
|
+
'claude-sonnet-4-2025',
|
|
120
|
+
'claude-opus-4-1',
|
|
121
|
+
] as const;
|
|
122
|
+
|
|
123
|
+
/** True when `model` is known to accept `temperature` / `top_p` / `top_k`. */
|
|
124
|
+
export function supportsSamplingParams(model: string): boolean {
|
|
125
|
+
const id = model.trim().toLowerCase();
|
|
126
|
+
return SAMPLING_PARAM_MODEL_PREFIXES.some((prefix) => id.startsWith(prefix));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The parameter named by a "deprecated for this model" 400, if that is what the
|
|
131
|
+
* error is. Anthropic's message is `` `temperature` is deprecated for this model. ``
|
|
132
|
+
* and the same wording covers `top_p` and `top_k`, so one matcher handles every
|
|
133
|
+
* sampling parameter — including ones not yet deprecated when this was written.
|
|
134
|
+
*/
|
|
135
|
+
export function deprecatedParameter(message: string): string | null {
|
|
136
|
+
const match = /`([a-z_]+)`\s+is\s+deprecated\s+for\s+this\s+model/i.exec(message);
|
|
137
|
+
return match ? match[1] : null;
|
|
138
|
+
}
|
|
139
|
+
|
|
72
140
|
const DEFAULTS = {
|
|
73
141
|
baseUrl: 'https://api.anthropic.com/v1',
|
|
74
142
|
model: LLM_DEFAULTS.anthropicModel,
|
|
@@ -133,35 +201,32 @@ Please respond with a valid JSON object only. Do not include any text before or
|
|
|
133
201
|
max_tokens: maxTokens,
|
|
134
202
|
messages: [{ role: 'user', content: jsonWrappedPrompt }],
|
|
135
203
|
system: systemPrompt,
|
|
136
|
-
temperature,
|
|
137
204
|
};
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
205
|
+
// Only for models known to take it. The Claude 5 family answers a request
|
|
206
|
+
// carrying `temperature` with a 400 and nothing else.
|
|
207
|
+
if (supportsSamplingParams(model)) {
|
|
208
|
+
requestBody.temperature = temperature;
|
|
209
|
+
}
|
|
141
210
|
|
|
142
211
|
try {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}))) as AnthropicErrorResponse;
|
|
160
|
-
throw this.handleApiError(response.status, errorData);
|
|
212
|
+
let data: AnthropicResponse;
|
|
213
|
+
try {
|
|
214
|
+
data = await this.post(requestBody, timeout);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
// SAFETY NET for a parameter the API has since stopped accepting on a
|
|
217
|
+
// model the allowlist still vouches for. It also covers parameters this
|
|
218
|
+
// provider does not send today (`top_p`, `top_k`), so adding one later
|
|
219
|
+
// cannot reintroduce this failure. One retry, only when the API itself
|
|
220
|
+
// named the offending key — never a blind resend.
|
|
221
|
+
const param =
|
|
222
|
+
error instanceof LLMProviderError && error.statusCode === 400
|
|
223
|
+
? deprecatedParameter(error.message)
|
|
224
|
+
: null;
|
|
225
|
+
if (!param || !(param in requestBody)) throw error;
|
|
226
|
+
delete (requestBody as unknown as Record<string, unknown>)[param];
|
|
227
|
+
data = await this.post(requestBody, timeout);
|
|
161
228
|
}
|
|
162
229
|
|
|
163
|
-
const data = (await response.json()) as AnthropicResponse;
|
|
164
|
-
|
|
165
230
|
const textContent = data.content.find((c) => c.type === 'text');
|
|
166
231
|
if (!textContent?.text) {
|
|
167
232
|
throw new LLMProviderError(
|
|
@@ -185,8 +250,6 @@ Please respond with a valid JSON object only. Do not include any text before or
|
|
|
185
250
|
finishReason: data.stop_reason ?? undefined,
|
|
186
251
|
};
|
|
187
252
|
} catch (error) {
|
|
188
|
-
clearTimeout(timeoutId);
|
|
189
|
-
|
|
190
253
|
if (error instanceof LLMProviderError) {
|
|
191
254
|
throw error;
|
|
192
255
|
}
|
|
@@ -211,6 +274,39 @@ Please respond with a valid JSON object only. Do not include any text before or
|
|
|
211
274
|
}
|
|
212
275
|
}
|
|
213
276
|
|
|
277
|
+
/**
|
|
278
|
+
* One POST to /messages. Each attempt gets its own timeout — a retry that
|
|
279
|
+
* strips a rejected parameter is a fresh request, and inheriting an
|
|
280
|
+
* already-consumed deadline would abort it before it could succeed.
|
|
281
|
+
*/
|
|
282
|
+
private async post(body: AnthropicRequest, timeout: number): Promise<AnthropicResponse> {
|
|
283
|
+
const controller = new AbortController();
|
|
284
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
285
|
+
try {
|
|
286
|
+
const response = await fetch(`${this.config.baseUrl}/messages`, {
|
|
287
|
+
method: 'POST',
|
|
288
|
+
headers: {
|
|
289
|
+
'Content-Type': 'application/json',
|
|
290
|
+
'x-api-key': this.config.apiKey,
|
|
291
|
+
'anthropic-version': this.apiVersion,
|
|
292
|
+
},
|
|
293
|
+
body: JSON.stringify(body),
|
|
294
|
+
signal: controller.signal,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
if (!response.ok) {
|
|
298
|
+
const errorData = (await response.json().catch(() => ({
|
|
299
|
+
error: { type: 'unknown', message: 'Unknown error' },
|
|
300
|
+
}))) as AnthropicErrorResponse;
|
|
301
|
+
throw this.handleApiError(response.status, errorData);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return (await response.json()) as AnthropicResponse;
|
|
305
|
+
} finally {
|
|
306
|
+
clearTimeout(timeoutId);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
214
310
|
estimateCost(
|
|
215
311
|
promptTokens: number,
|
|
216
312
|
completionTokens: number,
|
|
@@ -269,5 +365,13 @@ export function createAnthropicProvider(): AnthropicProvider | null {
|
|
|
269
365
|
apiKey,
|
|
270
366
|
baseUrl: process.env.ANTHROPIC_BASE_URL,
|
|
271
367
|
defaultModel: process.env.ANTHROPIC_MODEL,
|
|
368
|
+
// A DEPLOYMENT CAN BUY ITSELF MORE TIME, and one needs to. The 60-second
|
|
369
|
+
// default is right for an interactive answer and wrong for a batch job: a
|
|
370
|
+
// commitment-extraction call over two meeting summaries takes longer than
|
|
371
|
+
// that (measured — one document 19.6s, two documents timed out), and the
|
|
372
|
+
// failure is silent, because the caller is written to keep its deterministic
|
|
373
|
+
// half when the model is unreachable. So the symptom of "too big a batch" is
|
|
374
|
+
// an extraction that quietly finds nothing.
|
|
375
|
+
timeoutMs: Number(process.env.LLM_TIMEOUT_MS) || undefined,
|
|
272
376
|
});
|
|
273
377
|
}
|
package/src/providers/openai.ts
CHANGED
|
@@ -430,5 +430,9 @@ export function createOpenAIProvider(): OpenAIProvider | null {
|
|
|
430
430
|
apiKey,
|
|
431
431
|
baseUrl: process.env.OPENAI_BASE_URL,
|
|
432
432
|
defaultModel: process.env.OPENAI_MODEL,
|
|
433
|
+
// Same knob as the Anthropic factory next door — one provider honouring a
|
|
434
|
+
// deployment's timeout and the other ignoring it is the kind of asymmetry
|
|
435
|
+
// that only shows up as a provider-dependent bug at 3am.
|
|
436
|
+
timeoutMs: Number(process.env.LLM_TIMEOUT_MS) || undefined,
|
|
433
437
|
});
|
|
434
438
|
}
|
package/src/types.ts
CHANGED
|
@@ -133,11 +133,17 @@ export interface LLMServiceConfig {
|
|
|
133
133
|
*/
|
|
134
134
|
export const LLM_DEFAULTS = {
|
|
135
135
|
openaiModel: 'gpt-4o-mini',
|
|
136
|
-
|
|
136
|
+
/**
|
|
137
|
+
* The previous default, `claude-sonnet-4-20250514`, was retired and returns
|
|
138
|
+
* 404 — a default nobody can call. Verified callable 2026-08-10.
|
|
139
|
+
*/
|
|
140
|
+
anthropicModel: 'claude-sonnet-5',
|
|
137
141
|
/**
|
|
138
142
|
* Default sampling temperature.
|
|
139
143
|
* Note: base gpt-5 models (gpt-5, gpt-5.1, gpt-5.2) require temperature=1.0
|
|
140
|
-
* and the OpenAI provider enforces this automatically.
|
|
144
|
+
* and the OpenAI provider enforces this automatically. On Anthropic the
|
|
145
|
+
* parameter is not sent at all to models that reject it (Claude 4.7+),
|
|
146
|
+
* so this is a request, not a guarantee — see providers/anthropic.ts.
|
|
141
147
|
*/
|
|
142
148
|
temperature: 0.7,
|
|
143
149
|
maxTokens: 8192,
|