@oh-my-pi/pi-ai 18.0.1 → 18.0.3

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,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.0.3] - 2026-08-23
6
+
7
+ ### Fixed
8
+
9
+ - Fixed a Fireworks-hosted model aborting mid-generation with an HTTP 400 `Floating point NaN (not-a-number) is detected in generation` killing the turn instead of retrying; this model-side numerical fault is now classified transient and retried, matching the existing treatment of Copilot fleet-skew 400s ([#9458](https://github.com/can1357/oh-my-pi/issues/9458)).
10
+
11
+ ## [18.0.2] - 2026-08-23
12
+
13
+ ### Fixed
14
+
15
+ - Fixed OpenAI-compatible completions hosts that stream content then terminate with the `[DONE]` sentinel while omitting (or `null`ing) `finish_reason` failing every turn with `OpenAI completions stream closed before a finish_reason was received`; a `[DONE]`-terminated stream now finalizes as a clean stop and only a genuine transport EOF (no `[DONE]`, no finish reason) surfaces the incomplete-stream error ([#9433](https://github.com/can1357/oh-my-pi/issues/9433)).
16
+
5
17
  ## [18.0.1] - 2026-08-23
6
18
 
7
19
  ### Changed
@@ -29,7 +41,6 @@
29
41
 
30
42
  - Added Amazon Bedrock Converse guardrail configuration with provider-scoped identifier, version, and trace settings.
31
43
 
32
- ### Changed
33
44
  ## [18.0.0] - 2026-08-22
34
45
 
35
46
  ### Added
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "18.0.1",
4
+ "version": "18.0.3",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Stencil Labs, Inc.",
@@ -37,10 +37,10 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@oh-my-pi/omptype": "18.0.1",
41
- "@oh-my-pi/pi-catalog": "18.0.1",
42
- "@oh-my-pi/pi-utils": "18.0.1",
43
- "@oh-my-pi/pi-wire": "18.0.1"
40
+ "@oh-my-pi/omptype": "18.0.3",
41
+ "@oh-my-pi/pi-catalog": "18.0.3",
42
+ "@oh-my-pi/pi-utils": "18.0.3",
43
+ "@oh-my-pi/pi-wire": "18.0.3"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/bun": "^1.3.14"
@@ -165,6 +165,15 @@ const COPILOT_TRANSIENT_MODEL_CODES: Record<string, true> = {
165
165
  model_not_supported: true,
166
166
  };
167
167
  const COPILOT_TRANSIENT_MODEL_PATTERN = /model_not_supported/i;
168
+ // Fireworks (and other OpenAI-compat backends) can abort mid-generation with an
169
+ // HTTP 400 `invalid_request_error` whose body reports a model-side numerical
170
+ // fault: "Floating point NaN (not-a-number) is detected in generation". Despite
171
+ // the request-validation wrapper this is a decode-time logits overflow, not a
172
+ // bad request — a byte-identical replay of the same payload succeeds. The fault
173
+ // fires before any content is emitted, so retry is replay-safe. Kept narrow
174
+ // (both the NaN wording and "detected in generation") so it cannot swallow
175
+ // genuine request-validation 400s.
176
+ const GENERATION_NAN_PATTERN = /floating[ _-]?point nan\b.*\bdetected in generation/is;
168
177
  // Anthropic strict-tool grammar too large / schema too complex (400 invalid_request_error).
169
178
  // Feature-gated deployments (Azure Foundry, Baseten, …) reject `strict: true`
170
179
  // tools outright when the hosted model lacks structured outputs, e.g.
@@ -435,6 +444,9 @@ function classifyText(
435
444
 
436
445
  // Copilot's `model_not_supported` fleet-skew rejection is transient.
437
446
  if (statusClean === 400 && COPILOT_TRANSIENT_MODEL_PATTERN.test(cleanMessage)) kinds |= Flag.Transient;
447
+ // Fireworks mid-generation NaN 400 is a model-side decode fault, not a bad
448
+ // request; a byte-identical replay succeeds, so treat it as transient.
449
+ if (statusClean === 400 && GENERATION_NAN_PATTERN.test(cleanMessage)) kinds |= Flag.Transient;
438
450
  if (matchesStrictToolsRejection(cleanMessage, statusClean)) kinds |= Flag.Grammar;
439
451
  if (matchesFastModeUnsupported(cleanMessage, statusClean)) kinds |= Flag.FastModeUnsupported;
440
452
  }
@@ -603,26 +603,31 @@ const streamOpenAICompletionsOnce = (
603
603
  );
604
604
  const { requestAbortController, requestSignal } = abortTracker;
605
605
  const onSseEvent = options?.onSseEvent;
606
- const rawSseObserver = onSseEvent
607
- ? (event: RawSseEvent) => {
608
- if (!event.event && event.data && event.data !== "[DONE]") {
609
- try {
610
- const parsed = JSON.parse(event.data);
611
- const resolvedEvent =
612
- typeof parsed.type === "string"
613
- ? parsed.type
614
- : typeof parsed.object === "string"
615
- ? parsed.object
616
- : null;
617
- if (resolvedEvent) {
618
- event.event = resolvedEvent;
619
- event.raw = [`event: ${resolvedEvent}`, ...event.raw];
620
- }
621
- } catch {}
622
- }
623
- onSseEvent(event, model);
606
+ // Track the OpenAI `[DONE]` sentinel independently of `onSseEvent`: it is
607
+ // the streaming protocol's terminal signal, so a stream that ends with it
608
+ // completed by server agreement even when no `finish_reason` chunk arrived.
609
+ let sawDoneSentinel = false;
610
+ const rawSseObserver = (event: RawSseEvent) => {
611
+ if (event.data === "[DONE]") sawDoneSentinel = true;
612
+ if (onSseEvent) {
613
+ if (!event.event && event.data && event.data !== "[DONE]") {
614
+ try {
615
+ const parsed = JSON.parse(event.data);
616
+ const resolvedEvent =
617
+ typeof parsed.type === "string"
618
+ ? parsed.type
619
+ : typeof parsed.object === "string"
620
+ ? parsed.object
621
+ : null;
622
+ if (resolvedEvent) {
623
+ event.event = resolvedEvent;
624
+ event.raw = [`event: ${resolvedEvent}`, ...event.raw];
625
+ }
626
+ } catch {}
624
627
  }
625
- : undefined;
628
+ onSseEvent(event, model);
629
+ }
630
+ };
626
631
  // Assigned once the block helpers exist (they are scoped to the `try`);
627
632
  // the catch handler uses it to close open blocks before emitting the
628
633
  // terminal error so both exit paths obey the same block lifecycle.
@@ -1301,7 +1306,15 @@ const streamOpenAICompletionsOnce = (
1301
1306
  // Detect premature stream closure before the normal block-finalization
1302
1307
  // sweep. Throwing after that sweep would make the error handler emit a
1303
1308
  // second text_end/thinking_end for the same partial block.
1304
- if (streamFinishedAt === undefined && output.content.length > 0) {
1309
+ //
1310
+ // Only a genuine truncation — transport EOF with neither a
1311
+ // `finish_reason` chunk nor the `[DONE]` sentinel — is incomplete. A
1312
+ // stream terminated by `[DONE]` completed by server agreement; some
1313
+ // OpenAI-compatible hosts omit or `null` the `finish_reason` and rely on
1314
+ // `[DONE]` alone, so finalize those as the default `stop`
1315
+ // (mapStopReason(null)) instead of surfacing a false incomplete-stream
1316
+ // error and retrying every turn.
1317
+ if (streamFinishedAt === undefined && !sawDoneSentinel && output.content.length > 0) {
1305
1318
  throw new AIError.ProviderResponseError(
1306
1319
  "OpenAI completions stream closed before a finish_reason was received",
1307
1320
  { provider: model.provider, kind: "incomplete-stream" },