@oh-my-pi/pi-ai 16.3.9 → 16.3.10
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 +7 -0
- package/dist/types/error/flags.d.ts +3 -2
- package/dist/types/providers/ollama.d.ts +1 -0
- package/package.json +4 -4
- package/src/error/flags.ts +11 -4
- package/src/providers/anthropic.ts +1 -1
- package/src/providers/ollama.ts +7 -2
- package/src/providers/openai-shared.ts +1 -1
- package/src/utils/empty-completion-retry.ts +4 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.3.10] - 2026-07-06
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed Ollama/Ollama Cloud EOS-only completions to retry empty stops with a single output token before the agent loop can halt silently. ([#4659](https://github.com/can1357/oh-my-pi/issues/4659))
|
|
10
|
+
- Fixed Claude Sonnet 5 failing every request on feature-gated gateways (Azure Foundry, OpenAI-compatible relays) that reject strict tools with "structured_outputs not supported" — the rejection is now classified as a strict-tool rejection, so the request retries without strict tools and the session remembers the downgrade.
|
|
11
|
+
|
|
5
12
|
## [16.3.7] - 2026-07-05
|
|
6
13
|
|
|
7
14
|
### Fixed
|
|
@@ -13,7 +13,7 @@ export declare const Flag: {
|
|
|
13
13
|
readonly SilentAbort: 33554432;
|
|
14
14
|
readonly UserInterrupt: 67108864;
|
|
15
15
|
readonly Abort: 134217728;
|
|
16
|
-
/**
|
|
16
|
+
/** Strict-tool rejection (400): grammar too large, schema too complex, or structured outputs unsupported by the model/endpoint. */
|
|
17
17
|
readonly Grammar: 268435456;
|
|
18
18
|
/** Anthropic model/account does not support fast mode / the `speed` parameter. */
|
|
19
19
|
readonly FastModeUnsupported: 536870912;
|
|
@@ -48,7 +48,8 @@ export declare function classify(error: unknown, api?: Api): number;
|
|
|
48
48
|
*/
|
|
49
49
|
export declare function isUsageLimit(error: unknown, api?: Api): boolean;
|
|
50
50
|
/**
|
|
51
|
-
*
|
|
51
|
+
* Strict-tool rejection: grammar too large, schema too complex, or structured
|
|
52
|
+
* outputs unsupported by the model/endpoint.
|
|
52
53
|
* Accessor for {@link Flag.Grammar}.
|
|
53
54
|
*/
|
|
54
55
|
export declare function isGrammarError(error: unknown): boolean;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "16.3.
|
|
4
|
+
"version": "16.3.10",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bufbuild/protobuf": "^2.12.0",
|
|
41
|
-
"@oh-my-pi/pi-catalog": "16.3.
|
|
42
|
-
"@oh-my-pi/pi-utils": "16.3.
|
|
43
|
-
"@oh-my-pi/pi-wire": "16.3.
|
|
41
|
+
"@oh-my-pi/pi-catalog": "16.3.10",
|
|
42
|
+
"@oh-my-pi/pi-utils": "16.3.10",
|
|
43
|
+
"@oh-my-pi/pi-wire": "16.3.10",
|
|
44
44
|
"arktype": "^2.2.0",
|
|
45
45
|
"zod": "^4"
|
|
46
46
|
},
|
package/src/error/flags.ts
CHANGED
|
@@ -22,7 +22,7 @@ export const Flag = {
|
|
|
22
22
|
SilentAbort: 0x0200_0000,
|
|
23
23
|
UserInterrupt: 0x0400_0000,
|
|
24
24
|
Abort: 0x0800_0000,
|
|
25
|
-
/**
|
|
25
|
+
/** Strict-tool rejection (400): grammar too large, schema too complex, or structured outputs unsupported by the model/endpoint. */
|
|
26
26
|
Grammar: 0x1000_0000,
|
|
27
27
|
/** Anthropic model/account does not support fast mode / the `speed` parameter. */
|
|
28
28
|
FastModeUnsupported: 0x2000_0000,
|
|
@@ -108,12 +108,17 @@ export const LLAMA_CPP_TOOL_CALL_PARSE_PATTERN =
|
|
|
108
108
|
// on a backend that has the model.
|
|
109
109
|
const COPILOT_MODEL_NOT_SUPPORTED_PATTERN = /model_not_supported/i;
|
|
110
110
|
// Anthropic strict-tool grammar too large / schema too complex (400 invalid_request_error).
|
|
111
|
+
// Feature-gated deployments (Azure Foundry, Baseten, …) reject `strict: true`
|
|
112
|
+
// tools outright when the hosted model lacks structured outputs, e.g.
|
|
113
|
+
// "structured_outputs not supported" — without an invalid_request_error wrapper.
|
|
111
114
|
const GRAMMAR_TOO_LARGE_PATTERN = /compiled grammar/i;
|
|
112
115
|
const GRAMMAR_TOO_LARGE_DETAIL_PATTERN = /too large/i;
|
|
113
116
|
const SCHEMA_TOO_COMPLEX_PATTERN = /schema/i;
|
|
114
117
|
const SCHEMA_TOO_COMPLEX_DETAIL_PATTERN = /too complex/i;
|
|
115
118
|
const SCHEMA_COMPILE_PATTERN = /compil/i;
|
|
116
119
|
const INVALID_REQUEST_PATTERN = /invalid_request_error/i;
|
|
120
|
+
const STRUCTURED_OUTPUTS_PATTERN = /structured[_ -]?outputs?/i;
|
|
121
|
+
const FEATURE_NOT_SUPPORTED_PATTERN = /not (?:supported|available|enabled)|unsupported|does(?: not|n'?t) support/i;
|
|
117
122
|
// Anthropic fast-mode unsupported: 400 rejecting `speed`, or 429 rate_limit_error
|
|
118
123
|
// because the account lacks the extra-usage entitlement fast mode requires.
|
|
119
124
|
const FAST_MODE_SPEED_PARAM_PATTERN = /\bspeed\b/i;
|
|
@@ -127,8 +132,9 @@ const OAUTH_TRANSIENT_FAILURE_PATTERN =
|
|
|
127
132
|
/timeout|network|fetch failed|ECONN(?:REFUSED|RESET)|ETIMEDOUT|EAI_AGAIN|socket hang up|\b(?:408|425|429|5\d{2})\b|rate.?limit|too many requests|temporar|unavailable|forbidden|permission_denied|cloudflare|captcha/i;
|
|
128
133
|
const OAUTH_HTTP_AUTH_PATTERN = /\b401\b/;
|
|
129
134
|
|
|
130
|
-
function
|
|
135
|
+
function matchesStrictToolsRejection(message: string, errorStatus: number | undefined): boolean {
|
|
131
136
|
if (errorStatus !== 400) return false;
|
|
137
|
+
if (STRUCTURED_OUTPUTS_PATTERN.test(message) && FEATURE_NOT_SUPPORTED_PATTERN.test(message)) return true;
|
|
132
138
|
if (!INVALID_REQUEST_PATTERN.test(message)) return false;
|
|
133
139
|
const grammarTooLarge = GRAMMAR_TOO_LARGE_PATTERN.test(message) && GRAMMAR_TOO_LARGE_DETAIL_PATTERN.test(message);
|
|
134
140
|
const schemaTooComplex =
|
|
@@ -317,7 +323,7 @@ function classifyText(errorMessage: string | undefined, errorStatus: number | un
|
|
|
317
323
|
|
|
318
324
|
// Copilot per-client routing flap is transient.
|
|
319
325
|
if (statusClean === 400 && COPILOT_MODEL_NOT_SUPPORTED_PATTERN.test(cleanMessage)) kinds |= Flag.Transient;
|
|
320
|
-
if (
|
|
326
|
+
if (matchesStrictToolsRejection(cleanMessage, statusClean)) kinds |= Flag.Grammar;
|
|
321
327
|
if (matchesFastModeUnsupported(cleanMessage, statusClean)) kinds |= Flag.FastModeUnsupported;
|
|
322
328
|
}
|
|
323
329
|
if (kinds !== 0) return create(kinds);
|
|
@@ -411,7 +417,8 @@ export function isUsageLimit(error: unknown, api?: Api): boolean {
|
|
|
411
417
|
}
|
|
412
418
|
|
|
413
419
|
/**
|
|
414
|
-
*
|
|
420
|
+
* Strict-tool rejection: grammar too large, schema too complex, or structured
|
|
421
|
+
* outputs unsupported by the model/endpoint.
|
|
415
422
|
* Accessor for {@link Flag.Grammar}.
|
|
416
423
|
*/
|
|
417
424
|
export function isGrammarError(error: unknown): boolean {
|
|
@@ -2396,7 +2396,7 @@ const streamAnthropicOnce = (
|
|
|
2396
2396
|
) {
|
|
2397
2397
|
// Log-only: the retried turn must not carry an errorMessage on
|
|
2398
2398
|
// success (consumers treat its presence as failure).
|
|
2399
|
-
logger.warn("anthropic: strict
|
|
2399
|
+
logger.warn("anthropic: strict tools rejected, retrying without strict tools", {
|
|
2400
2400
|
model: model.id,
|
|
2401
2401
|
error: await finalizeErrorMessage(streamFailure, rawRequestDump),
|
|
2402
2402
|
});
|
package/src/providers/ollama.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
} from "../types";
|
|
17
17
|
import { normalizeSystemPrompts } from "../utils";
|
|
18
18
|
import { clearStreamingPartialJson, kStreamingPartialJson } from "../utils/block-symbols";
|
|
19
|
+
import { withEmptyCompletionRetry } from "../utils/empty-completion-retry";
|
|
19
20
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
20
21
|
import type { CapturedHttpErrorResponse, RawHttpRequestDump } from "../utils/http-inspector";
|
|
21
22
|
import {
|
|
@@ -449,10 +450,10 @@ function hasVisibleAssistantContent(output: AssistantMessage): boolean {
|
|
|
449
450
|
|
|
450
451
|
const OLLAMA_RETRY_DELAYS_MS = [2_000, 5_000, 10_000];
|
|
451
452
|
|
|
452
|
-
|
|
453
|
+
const streamOllamaOnce = (
|
|
453
454
|
model: Model<"ollama-chat">,
|
|
454
455
|
context: Context,
|
|
455
|
-
options: OllamaChatOptions,
|
|
456
|
+
options: OllamaChatOptions = {},
|
|
456
457
|
): AssistantMessageEventStream => {
|
|
457
458
|
const stream = new AssistantMessageEventStream();
|
|
458
459
|
void (async () => {
|
|
@@ -771,3 +772,7 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
|
771
772
|
})();
|
|
772
773
|
return stream;
|
|
773
774
|
};
|
|
775
|
+
|
|
776
|
+
/** Retry EOS-only Ollama completions before the agent loop sees an empty stop. */
|
|
777
|
+
export const streamOllama: StreamFunction<"ollama-chat"> = (model, context, options) =>
|
|
778
|
+
withEmptyCompletionRetry(model, context, options, streamOllamaOnce);
|
|
@@ -1041,7 +1041,7 @@ export function shouldRetryWithoutStrictTools(
|
|
|
1041
1041
|
const messageParts = [error instanceof Error ? error.message : undefined, capturedErrorResponse?.bodyText]
|
|
1042
1042
|
.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
|
1043
1043
|
.join("\n");
|
|
1044
|
-
return /wrong_api_format|mixed values for 'strict'|tool[s]?\b.*strict|\bstrict\b.*tool|tool parameters? schema|invalid schema for function/i.test(
|
|
1044
|
+
return /wrong_api_format|mixed values for 'strict'|tool[s]?\b.*strict|\bstrict\b.*tool|tool parameters? schema|invalid schema for function|structured[_ -]?outputs?\b[^\n]*(?:not (?:supported|available|enabled)|unsupported)|(?:not support|unsupported)[^\n]*structured[_ -]?outputs?\b/i.test(
|
|
1045
1045
|
messageParts,
|
|
1046
1046
|
);
|
|
1047
1047
|
}
|
|
@@ -109,17 +109,16 @@ export function withEmptyCompletionRetry<M, O extends EmptyCompletionRetryOption
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// Retry only a genuinely degenerate completion: a normal stop that
|
|
112
|
-
// produced no visible content
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
// is left alone.
|
|
112
|
+
// produced no visible content and reported no generated content tokens.
|
|
113
|
+
// Some providers count the terminal EOS as one output token, so a
|
|
114
|
+
// one-token invisible stop is still the same empty-completion failure.
|
|
116
115
|
const message = terminal?.type === "done" ? terminal.message : undefined;
|
|
117
116
|
const isRetryableEmpty =
|
|
118
117
|
!committed &&
|
|
119
118
|
message !== undefined &&
|
|
120
119
|
message.stopReason === "stop" &&
|
|
121
120
|
!message.errorMessage &&
|
|
122
|
-
(message.usage?.output ?? 0) <=
|
|
121
|
+
(message.usage?.output ?? 0) <= 1 &&
|
|
123
122
|
!hasVisibleAssistantContent(message);
|
|
124
123
|
|
|
125
124
|
if (isRetryableEmpty && emptyAttempt < MAX_EMPTY_COMPLETION_RETRIES && !signal?.aborted) {
|