@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 +12 -1
- package/package.json +5 -5
- package/src/error/flags.ts +12 -0
- package/src/providers/openai-completions.ts +33 -20
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.
|
|
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.
|
|
41
|
-
"@oh-my-pi/pi-catalog": "18.0.
|
|
42
|
-
"@oh-my-pi/pi-utils": "18.0.
|
|
43
|
-
"@oh-my-pi/pi-wire": "18.0.
|
|
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"
|
package/src/error/flags.ts
CHANGED
|
@@ -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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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" },
|