@startsimpli/llm 0.4.17 → 0.4.18
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
CHANGED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the OpenAI provider actually PUTS ON THE WIRE.
|
|
3
|
+
*
|
|
4
|
+
* The sibling of `anthropic-provider.test.ts`, and it exists for the same class
|
|
5
|
+
* of bug: a 400 caused by a parameter WE send, which no amount of response
|
|
6
|
+
* handling can recover from.
|
|
7
|
+
*
|
|
8
|
+
* The acceptance boundary below is not invented. It was measured against
|
|
9
|
+
* api.openai.com with a real key on 2026-08-19, while debugging why a live
|
|
10
|
+
* translation on marketing-agents failed:
|
|
11
|
+
*
|
|
12
|
+
* gpt-5.2 max_tokens -> 400 Unsupported parameter: 'max_tokens' is not
|
|
13
|
+
* supported with this model. Use
|
|
14
|
+
* 'max_completion_tokens' instead.
|
|
15
|
+
* gpt-5.2 max_completion_tokens -> 200
|
|
16
|
+
* gpt-4o max_tokens -> 200
|
|
17
|
+
*
|
|
18
|
+
* So the provider as written could never call ANY gpt-5 model: it always sent
|
|
19
|
+
* `max_tokens`. That is a total outage of the newest family, not an edge case.
|
|
20
|
+
*
|
|
21
|
+
* The second thing pinned here is the FAMILY TEST. bd startsim-768w.38.26 filed
|
|
22
|
+
* the literal-id deny-list (`model === 'gpt-5' || 'gpt-5.1' || 'gpt-5.2'`) as a
|
|
23
|
+
* bug, because it is "correct only until OpenAI ships gpt-5.3". These tests use
|
|
24
|
+
* an unreleased id on purpose, so the rule has to be about the FAMILY.
|
|
25
|
+
*/
|
|
26
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
27
|
+
|
|
28
|
+
import { OpenAIProvider } from '../providers/openai';
|
|
29
|
+
|
|
30
|
+
interface Sent {
|
|
31
|
+
model: string;
|
|
32
|
+
temperature?: number;
|
|
33
|
+
max_tokens?: number;
|
|
34
|
+
max_completion_tokens?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const OK_BODY = {
|
|
38
|
+
id: 'chatcmpl-1',
|
|
39
|
+
object: 'chat.completion',
|
|
40
|
+
created: 0,
|
|
41
|
+
model: 'server-reported-model',
|
|
42
|
+
choices: [{ index: 0, message: { role: 'assistant', content: '{"ok":true}' }, finish_reason: 'stop' }],
|
|
43
|
+
usage: { prompt_tokens: 10, completion_tokens: 4, total_tokens: 14 },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
let sent: Sent[];
|
|
47
|
+
let fetchMock: ReturnType<typeof vi.fn>;
|
|
48
|
+
|
|
49
|
+
function provider(defaultModel: string) {
|
|
50
|
+
return new OpenAIProvider({ apiKey: 'sk-test', defaultModel, defaultTemperature: 0.2 });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
sent = [];
|
|
55
|
+
fetchMock = vi.fn(async (_url: unknown, init: RequestInit) => {
|
|
56
|
+
sent.push(JSON.parse(String(init.body)) as Sent);
|
|
57
|
+
return { ok: true, status: 200, json: async () => OK_BODY } as unknown as Response;
|
|
58
|
+
});
|
|
59
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
vi.unstubAllGlobals();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('the token-limit parameter', () => {
|
|
67
|
+
it.each(['gpt-5', 'gpt-5.1', 'gpt-5.2'])(
|
|
68
|
+
'%s gets max_completion_tokens and never max_tokens',
|
|
69
|
+
async (model) => {
|
|
70
|
+
await provider(model).generate('u', 's');
|
|
71
|
+
|
|
72
|
+
expect(sent[0]!.max_completion_tokens).toBeGreaterThan(0);
|
|
73
|
+
expect(sent[0]!.max_tokens).toBeUndefined();
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
it('an UNRELEASED gpt-5 model is handled by family, not by a hardcoded list', async () => {
|
|
78
|
+
// bd startsim-768w.38.26. The day this id ships, a literal deny-list sends
|
|
79
|
+
// it max_tokens and every call 400s — silently, from the caller's view.
|
|
80
|
+
await provider('gpt-5.3').generate('u', 's');
|
|
81
|
+
|
|
82
|
+
expect(sent[0]!.max_completion_tokens).toBeGreaterThan(0);
|
|
83
|
+
expect(sent[0]!.max_tokens).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('gpt-4o keeps max_tokens, which it accepts', async () => {
|
|
87
|
+
// The fix must not break the models that were working.
|
|
88
|
+
await provider('gpt-4o').generate('u', 's');
|
|
89
|
+
|
|
90
|
+
expect(sent[0]!.max_tokens).toBeGreaterThan(0);
|
|
91
|
+
expect(sent[0]!.max_completion_tokens).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('honours an explicit maxTokens on whichever parameter the model takes', async () => {
|
|
95
|
+
await provider('gpt-5.2').generate('u', 's', { maxTokens: 123 });
|
|
96
|
+
await provider('gpt-4o').generate('u', 's', { maxTokens: 123 });
|
|
97
|
+
|
|
98
|
+
expect(sent[0]!.max_completion_tokens).toBe(123);
|
|
99
|
+
expect(sent[1]!.max_tokens).toBe(123);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('temperature', () => {
|
|
104
|
+
it.each(['gpt-5', 'gpt-5.1', 'gpt-5.2', 'gpt-5.3'])(
|
|
105
|
+
'%s is sent temperature 1.0, whatever the deployment configured',
|
|
106
|
+
async (model) => {
|
|
107
|
+
// Base gpt-5 models reject any other value.
|
|
108
|
+
await provider(model).generate('u', 's', { temperature: 0.2 });
|
|
109
|
+
|
|
110
|
+
expect(sent[0]!.temperature).toBe(1.0);
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
it('a FINE-TUNED gpt-5 keeps the configured temperature', async () => {
|
|
115
|
+
// The existing carve-out, preserved: ft: models do accept it.
|
|
116
|
+
await provider('ft:gpt-5.2:acme::abc123').generate('u', 's', { temperature: 0.2 });
|
|
117
|
+
|
|
118
|
+
expect(sent[0]!.temperature).toBe(0.2);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('gpt-4o keeps the configured temperature', async () => {
|
|
122
|
+
await provider('gpt-4o').generate('u', 's', { temperature: 0.2 });
|
|
123
|
+
|
|
124
|
+
expect(sent[0]!.temperature).toBe(0.2);
|
|
125
|
+
});
|
|
126
|
+
});
|
package/src/providers/openai.ts
CHANGED
|
@@ -31,10 +31,35 @@ interface OpenAIRequest {
|
|
|
31
31
|
model: string;
|
|
32
32
|
messages: OpenAIMessage[];
|
|
33
33
|
temperature?: number;
|
|
34
|
+
/** Pre-gpt-5 models. Rejected outright by the gpt-5 family. */
|
|
34
35
|
max_tokens?: number;
|
|
36
|
+
/** The gpt-5 family's replacement for `max_tokens`. */
|
|
37
|
+
max_completion_tokens?: number;
|
|
35
38
|
response_format?: { type: 'json_object' | 'text' };
|
|
36
39
|
}
|
|
37
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Is this the gpt-5 FAMILY? Matched by prefix, deliberately, not by a list of
|
|
43
|
+
* ids (bd startsim-768w.38.26 filed the literal-id deny-list as a bug: it is
|
|
44
|
+
* "correct only until OpenAI ships gpt-5.3", and the failure is a silent 400).
|
|
45
|
+
* `ft:gpt-5…` fine-tunes are still gpt-5 models on the wire.
|
|
46
|
+
*/
|
|
47
|
+
function isGpt5Family(model: string): boolean {
|
|
48
|
+
return /^gpt-5/.test(model.startsWith('ft:') ? model.slice(3) : model);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The token-limit parameter this model accepts. MEASURED against api.openai.com
|
|
53
|
+
* on 2026-08-19: gpt-5.2 rejects `max_tokens` with "Unsupported parameter ...
|
|
54
|
+
* Use 'max_completion_tokens' instead", while gpt-4o accepts `max_tokens`.
|
|
55
|
+
* Sending the wrong one is a total outage of that model, not a degraded call.
|
|
56
|
+
*/
|
|
57
|
+
function tokenLimitParam(model: string, maxTokens: number): Partial<OpenAIRequest> {
|
|
58
|
+
return isGpt5Family(model)
|
|
59
|
+
? { max_completion_tokens: maxTokens }
|
|
60
|
+
: { max_tokens: maxTokens };
|
|
61
|
+
}
|
|
62
|
+
|
|
38
63
|
interface OpenAIResponse {
|
|
39
64
|
id: string;
|
|
40
65
|
object: string;
|
|
@@ -149,9 +174,9 @@ export class OpenAIProvider implements ILLMProvider {
|
|
|
149
174
|
|
|
150
175
|
const model = options?.model ?? this.config.defaultModel;
|
|
151
176
|
// Base gpt-5 models require temperature=1.0, but fine-tuned models use configured temperature
|
|
152
|
-
const isBaseGpt5 = model === 'gpt-5' || model === 'gpt-5.1' || model === 'gpt-5.2';
|
|
153
177
|
const isFineTuned = model.startsWith('ft:');
|
|
154
|
-
const
|
|
178
|
+
const isBaseGpt5 = isGpt5Family(model) && !isFineTuned;
|
|
179
|
+
const temperature = isBaseGpt5
|
|
155
180
|
? 1.0
|
|
156
181
|
: (options?.temperature ?? this.config.defaultTemperature);
|
|
157
182
|
const maxTokens = options?.maxTokens ?? this.config.defaultMaxTokens;
|
|
@@ -164,7 +189,7 @@ export class OpenAIProvider implements ILLMProvider {
|
|
|
164
189
|
{ role: 'user', content: userPrompt },
|
|
165
190
|
],
|
|
166
191
|
temperature,
|
|
167
|
-
|
|
192
|
+
...tokenLimitParam(model, maxTokens),
|
|
168
193
|
response_format: { type: 'json_object' },
|
|
169
194
|
};
|
|
170
195
|
|
|
@@ -264,9 +289,9 @@ export class OpenAIProvider implements ILLMProvider {
|
|
|
264
289
|
}
|
|
265
290
|
|
|
266
291
|
const model = options?.model ?? this.config.defaultModel;
|
|
267
|
-
const isBaseGpt5 = model === 'gpt-5' || model === 'gpt-5.1' || model === 'gpt-5.2';
|
|
268
292
|
const isFineTuned = model.startsWith('ft:');
|
|
269
|
-
const
|
|
293
|
+
const isBaseGpt5 = isGpt5Family(model) && !isFineTuned;
|
|
294
|
+
const temperature = isBaseGpt5
|
|
270
295
|
? 1.0
|
|
271
296
|
: (options?.temperature ?? this.config.defaultTemperature);
|
|
272
297
|
const maxTokens = options?.maxTokens ?? this.config.defaultMaxTokens;
|
|
@@ -297,7 +322,7 @@ export class OpenAIProvider implements ILLMProvider {
|
|
|
297
322
|
model,
|
|
298
323
|
messages: oaiMessages,
|
|
299
324
|
temperature,
|
|
300
|
-
|
|
325
|
+
...tokenLimitParam(model, maxTokens),
|
|
301
326
|
tools: tools.map((t) => ({
|
|
302
327
|
type: 'function' as const,
|
|
303
328
|
function: { name: t.name, description: t.description, parameters: t.parameters },
|