@startsimpli/llm 0.4.20 → 0.4.21

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startsimpli/llm",
3
- "version": "0.4.20",
3
+ "version": "0.4.21",
4
4
  "description": "Generic LLM service infrastructure — provider abstraction, retry logic, JSON extraction",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -144,6 +144,21 @@ describe('adapting to what a model refuses', () => {
144
144
  expect(sent).toHaveLength(3);
145
145
  });
146
146
 
147
+ it('parallel first calls all succeed: a lesson learned by one request applies to the others in flight', async () => {
148
+ // Regression: crochet's assembly pipeline fires its connection calls in
149
+ // parallel. All of them sent 'none' before any answer; the first 400 taught
150
+ // the provider 'low', and the others saw that lesson as already learned and
151
+ // threw instead of retrying (gpt-6-astra: 12 of 12 connections failed).
152
+ const p = provider('gpt-6.1-sol');
153
+ const effortErr = { code: 'unsupported_value', param: 'reasoning_effort', message: "Unsupported value: 'reasoning_effort' does not support 'none' with this model. Supported values are: 'low', 'medium', 'high', and 'xhigh'." };
154
+ reject(effortErr);
155
+ reject(effortErr);
156
+ reject(effortErr);
157
+ const results = await Promise.all([1, 2, 3].map(() => p.generate('u', 's', { reasoningEffort: 'none' })));
158
+ expect(results.every((r) => r.content === '{"ok":true}')).toBe(true);
159
+ expect(sent.slice(3).every((b) => b.reasoning_effort === 'low')).toBe(true);
160
+ });
161
+
147
162
  it('adapts on the tool-calling path too', async () => {
148
163
  const p = provider('gpt-6-sol');
149
164
  reject({ code: 'unsupported_value', param: 'temperature', message: "Unsupported value: 'temperature' does not support 0.2 with this model. Only the default (1) value is supported." });
@@ -270,20 +270,27 @@ export class OpenAIProvider implements ILLMProvider {
270
270
  ): Promise<T> {
271
271
  const q = this.quirksFor(model);
272
272
  for (let attempt = 0; ; attempt++) {
273
+ const body = JSON.stringify(build(q));
273
274
  const response = await fetch(`${this.config.baseUrl}/chat/completions`, {
274
275
  method: 'POST',
275
276
  headers: {
276
277
  'Content-Type': 'application/json',
277
278
  Authorization: `Bearer ${this.config.apiKey}`,
278
279
  },
279
- body: JSON.stringify(build(q)),
280
+ body,
280
281
  signal,
281
282
  });
282
283
  if (response.ok) return (await response.json()) as T;
283
284
  const errorData = (await response.json().catch(() => ({
284
285
  error: { message: 'Unknown error' },
285
286
  }))) as OpenAIErrorResponse;
286
- if (response.status === 400 && attempt < MAX_ADAPTIVE_RETRIES && learnFromError(q, errorData.error, sent)) continue;
287
+ if (response.status === 400 && attempt < MAX_ADAPTIVE_RETRIES) {
288
+ const learned = learnFromError(q, errorData.error, sent);
289
+ // Calls to a new model often go out in parallel: by the time this one
290
+ // fails, another may already have learned the lesson, so retry whenever
291
+ // the request would now be built differently from the one just sent.
292
+ if (learned || JSON.stringify(build(q)) !== body) continue;
293
+ }
287
294
  throw this.handleApiError(response.status, errorData);
288
295
  }
289
296
  }