@oh-my-pi/pi-ai 17.3.2 → 17.3.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,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.3.3] - 2026-08-14
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Distinguished Gemini thought-only `STOP` responses from empty transports, avoiding repeated identical reasoning requests and duplicate Antigravity endpoint streams while surfacing the missing final output for session-level recovery.
|
|
10
|
+
|
|
5
11
|
## [17.3.2] - 2026-08-13
|
|
6
12
|
|
|
7
13
|
### Fixed
|
|
@@ -8,6 +8,7 @@ export declare const Flag: {
|
|
|
8
8
|
readonly StaleResponsesItem: 1048576;
|
|
9
9
|
readonly MalformedFunctionCall: 2097152;
|
|
10
10
|
readonly ProviderFinishError: 4194304;
|
|
11
|
+
readonly EmptyResponse: 8192;
|
|
11
12
|
readonly ContentBlocked: 32768;
|
|
12
13
|
/** Account-scoped provider policy denial that may succeed with another credential. */
|
|
13
14
|
readonly AccountPolicy: 16384;
|
|
@@ -7,6 +7,8 @@ export type ProviderResponseErrorKind =
|
|
|
7
7
|
| "output"
|
|
8
8
|
/** Response body was empty/missing when content was required. */
|
|
9
9
|
| "empty-body"
|
|
10
|
+
/** Response completed without actionable output (for example, thoughts only). */
|
|
11
|
+
| "empty-output"
|
|
10
12
|
/** Malformed wire envelope (unexpected message ordering / shape). */
|
|
11
13
|
| "envelope"
|
|
12
14
|
/** Content was blocked by a provider safety filter. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "17.3.
|
|
4
|
+
"version": "17.3.3",
|
|
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,10 +38,10 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bufbuild/protobuf": "^2.12.1",
|
|
41
|
-
"@oh-my-pi/omptype": "17.3.
|
|
42
|
-
"@oh-my-pi/pi-catalog": "17.3.
|
|
43
|
-
"@oh-my-pi/pi-utils": "17.3.
|
|
44
|
-
"@oh-my-pi/pi-wire": "17.3.
|
|
41
|
+
"@oh-my-pi/omptype": "17.3.3",
|
|
42
|
+
"@oh-my-pi/pi-catalog": "17.3.3",
|
|
43
|
+
"@oh-my-pi/pi-utils": "17.3.3",
|
|
44
|
+
"@oh-my-pi/pi-wire": "17.3.3"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@bufbuild/protoc-gen-es": "^2.12.1",
|
package/src/error/flags.ts
CHANGED
|
@@ -24,6 +24,7 @@ export const Flag = {
|
|
|
24
24
|
StaleResponsesItem: 0x0010_0000,
|
|
25
25
|
MalformedFunctionCall: 0x0020_0000,
|
|
26
26
|
ProviderFinishError: 0x0040_0000,
|
|
27
|
+
EmptyResponse: 0x0000_2000,
|
|
27
28
|
ContentBlocked: 0x0000_8000,
|
|
28
29
|
/** Account-scoped provider policy denial that may succeed with another credential. */
|
|
29
30
|
AccountPolicy: 0x0000_4000,
|
|
@@ -50,6 +51,7 @@ const KIND_MASK =
|
|
|
50
51
|
Flag.StaleResponsesItem |
|
|
51
52
|
Flag.MalformedFunctionCall |
|
|
52
53
|
Flag.ProviderFinishError |
|
|
54
|
+
Flag.EmptyResponse |
|
|
53
55
|
Flag.ContentBlocked |
|
|
54
56
|
Flag.AccountPolicy |
|
|
55
57
|
Flag.ContextOverflow |
|
|
@@ -62,7 +64,12 @@ const KIND_MASK =
|
|
|
62
64
|
Flag.OAuthExpiry;
|
|
63
65
|
|
|
64
66
|
const RETRIABLE_KINDS =
|
|
65
|
-
Flag.Transient |
|
|
67
|
+
Flag.Transient |
|
|
68
|
+
Flag.UsageLimit |
|
|
69
|
+
Flag.ThinkingLoop |
|
|
70
|
+
Flag.StaleResponsesItem |
|
|
71
|
+
Flag.ProviderFinishError |
|
|
72
|
+
Flag.EmptyResponse;
|
|
66
73
|
|
|
67
74
|
const OVERFLOW_PATTERNS = [
|
|
68
75
|
/prompt is too long/i, // Anthropic
|
|
@@ -104,6 +111,7 @@ const AUTH_FAILURE_PATTERN =
|
|
|
104
111
|
/\b(?:401|403|unauthorized|forbidden|authentication|auth[_ ]?unavailable|no auth available|(?:invalid|no)[_ ]?api[_ ]?key)\b/i;
|
|
105
112
|
const MALFORMED_FUNCTION_CALL_PATTERN = /\bmalformed.?function.?call\b/i;
|
|
106
113
|
const PROVIDER_FINISH_ERROR_PATTERN = /\bProvider (?:returned error finish_reason|finish_reason:\s*error)\b/i;
|
|
114
|
+
const EMPTY_RESPONSE_PATTERN = /\bthought-only response without final output\b/i;
|
|
107
115
|
const CONTENT_FILTER_PATTERN = /\b(?:incomplete:\s*)?content_filter\b/i;
|
|
108
116
|
const ACCOUNT_POLICY_PATTERN = /\bcyber_policy\b|trusted access for cyber/i;
|
|
109
117
|
const STALE_RESPONSE_ITEM_PATTERNS = [/\bItem with id ['"][^'"]+['"] not found\.?/i, /previous[ _]?response/i] as const;
|
|
@@ -197,6 +205,7 @@ const ERROR_KIND_LABELS: readonly [Flag, string][] = [
|
|
|
197
205
|
[Flag.StaleResponsesItem, "stale-responses-item"],
|
|
198
206
|
[Flag.MalformedFunctionCall, "malformed-function-call"],
|
|
199
207
|
[Flag.ProviderFinishError, "provider-finish-error"],
|
|
208
|
+
[Flag.EmptyResponse, "empty-response"],
|
|
200
209
|
[Flag.ContentBlocked, "content-blocked"],
|
|
201
210
|
[Flag.AccountPolicy, "account-policy"],
|
|
202
211
|
[Flag.ContextOverflow, "context-overflow"],
|
|
@@ -340,6 +349,7 @@ function classifyText(errorMessage: string | undefined, errorStatus: number | un
|
|
|
340
349
|
if (matchesOverflowText(errorMessage)) kinds |= Flag.ContextOverflow;
|
|
341
350
|
if (isMalformedFunctionCallText(errorMessage)) kinds |= Flag.MalformedFunctionCall;
|
|
342
351
|
if (isProviderFinishErrorText(errorMessage)) kinds |= Flag.ProviderFinishError;
|
|
352
|
+
if (EMPTY_RESPONSE_PATTERN.test(errorMessage)) kinds |= Flag.EmptyResponse | Flag.Transient;
|
|
343
353
|
if (isContentBlockedText(errorMessage)) kinds |= Flag.ContentBlocked;
|
|
344
354
|
if (ACCOUNT_POLICY_PATTERN.test(errorMessage)) kinds |= Flag.AccountPolicy | Flag.ContentBlocked;
|
|
345
355
|
if (isAuthFailureText(errorMessage)) kinds |= Flag.AuthFailed;
|
package/src/error/provider.ts
CHANGED
|
@@ -9,6 +9,8 @@ export type ProviderResponseErrorKind =
|
|
|
9
9
|
| "output"
|
|
10
10
|
/** Response body was empty/missing when content was required. */
|
|
11
11
|
| "empty-body"
|
|
12
|
+
/** Response completed without actionable output (for example, thoughts only). */
|
|
13
|
+
| "empty-output"
|
|
12
14
|
/** Malformed wire envelope (unexpected message ordering / shape). */
|
|
13
15
|
| "envelope"
|
|
14
16
|
/** Content was blocked by a provider safety filter. */
|
|
@@ -38,11 +40,10 @@ export class ProviderResponseError extends Error {
|
|
|
38
40
|
this.kind = options.kind ?? "output";
|
|
39
41
|
// A safety filter block is terminal and intentionally non-retryable.
|
|
40
42
|
if (this.kind === "content-blocked") attach(this, create(Flag.ContentBlocked));
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
// output was already emitted.
|
|
43
|
+
// A logically empty completed output needs a session-level reminder that
|
|
44
|
+
// asks for the missing final answer. Empty bodies and incomplete streams
|
|
45
|
+
// stay on the generic transient retry/model-fallback path.
|
|
46
|
+
else if (this.kind === "empty-output") attach(this, create(Flag.Transient, Flag.EmptyResponse));
|
|
46
47
|
else if (this.kind === "incomplete-stream" || this.kind === "empty-body") attach(this, create(Flag.Transient));
|
|
47
48
|
}
|
|
48
49
|
}
|
|
@@ -622,11 +622,8 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
622
622
|
const isFlashLeakModel = model.id.includes("flash");
|
|
623
623
|
|
|
624
624
|
let started = false;
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
// hidden thought part also flips it (via `ensureStarted`); a thinking-only
|
|
628
|
-
// STOP must still fail over to the alternate Antigravity endpoint (#8480).
|
|
629
|
-
let emittedVisibleContent = false;
|
|
625
|
+
// Once any stream event starts, the endpoint is committed downstream.
|
|
626
|
+
// Failover remains safe only while `started` is false.
|
|
630
627
|
let sawFinishReason = false;
|
|
631
628
|
let lastResponseId: string | undefined;
|
|
632
629
|
const ensureStarted = () => {
|
|
@@ -705,7 +702,6 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
705
702
|
|
|
706
703
|
const emitVisibleText = (delta: string, thoughtSignature?: string): void => {
|
|
707
704
|
if (!delta) return;
|
|
708
|
-
emittedVisibleContent = true;
|
|
709
705
|
const block = startTextBlock();
|
|
710
706
|
block.text += delta;
|
|
711
707
|
block.textSignature = retainThoughtSignature(block.textSignature, thoughtSignature);
|
|
@@ -864,7 +860,6 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
864
860
|
};
|
|
865
861
|
|
|
866
862
|
output.content.push(toolCall);
|
|
867
|
-
emittedVisibleContent = true;
|
|
868
863
|
ensureStarted();
|
|
869
864
|
pushToolCallEvents(toolCall, blockIndex(), output, stream);
|
|
870
865
|
}
|
|
@@ -932,13 +927,17 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
932
927
|
};
|
|
933
928
|
|
|
934
929
|
let receivedContent = false;
|
|
930
|
+
const hasThinkingOutput = () =>
|
|
931
|
+
output.content.some(
|
|
932
|
+
block =>
|
|
933
|
+
block.type === "thinking" && (block.thinking.trim().length > 0 || Boolean(block.thinkingSignature)),
|
|
934
|
+
);
|
|
935
935
|
|
|
936
936
|
for (let i = 0; i < endpoints.length; i++) {
|
|
937
937
|
const endpoint = endpoints[i];
|
|
938
938
|
const isLastEndpoint = i === endpoints.length - 1;
|
|
939
939
|
try {
|
|
940
940
|
started = false;
|
|
941
|
-
emittedVisibleContent = false;
|
|
942
941
|
resetOutput();
|
|
943
942
|
|
|
944
943
|
// Per attempt: arm a pre-response (TTFT) timer, cleared the instant
|
|
@@ -1022,17 +1021,26 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
1022
1021
|
}
|
|
1023
1022
|
|
|
1024
1023
|
const streamed = await streamResponse(currentResponse);
|
|
1025
|
-
//
|
|
1026
|
-
// endpoint
|
|
1027
|
-
//
|
|
1028
|
-
//
|
|
1024
|
+
// Eventless silence may fail over to the alternate Antigravity
|
|
1025
|
+
// endpoint. Once thinking has streamed, the endpoint is already
|
|
1026
|
+
// committed downstream; Advisor mode may accept that silence,
|
|
1027
|
+
// while normal sessions surface it to final-output recovery.
|
|
1028
|
+
const thoughtOnly = hasThinkingOutput();
|
|
1029
1029
|
const acceptedSilence =
|
|
1030
|
-
options?.acceptEmptyResponse === true &&
|
|
1030
|
+
options?.acceptEmptyResponse === true &&
|
|
1031
|
+
!streamed.strippedPlanningLeak &&
|
|
1032
|
+
(isLastEndpoint || thoughtOnly);
|
|
1031
1033
|
if (output.stopReason !== "stop" || streamed.meaningful || acceptedSilence) {
|
|
1032
1034
|
receivedContent = streamed.meaningful || acceptedSilence;
|
|
1033
1035
|
break;
|
|
1034
1036
|
}
|
|
1035
1037
|
|
|
1038
|
+
// A thought-only STOP is a complete provider response, not a
|
|
1039
|
+
// transiently empty transport. Replaying the identical request
|
|
1040
|
+
// burns another full reasoning pass; let session recovery add
|
|
1041
|
+
// an explicit final-output reminder instead.
|
|
1042
|
+
if (thoughtOnly) break;
|
|
1043
|
+
|
|
1036
1044
|
if (emptyAttempt < MAX_EMPTY_STREAM_RETRIES) {
|
|
1037
1045
|
resetOutput();
|
|
1038
1046
|
}
|
|
@@ -1046,10 +1054,16 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
1046
1054
|
}
|
|
1047
1055
|
|
|
1048
1056
|
if (!receivedContent) {
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1057
|
+
const thoughtOnly = hasThinkingOutput();
|
|
1058
|
+
throw new AIError.ProviderResponseError(
|
|
1059
|
+
thoughtOnly
|
|
1060
|
+
? "Cloud Code Assist API returned a thought-only response without final output"
|
|
1061
|
+
: "Cloud Code Assist API returned an empty response",
|
|
1062
|
+
{
|
|
1063
|
+
provider: model.provider,
|
|
1064
|
+
kind: thoughtOnly ? "empty-output" : "empty-body",
|
|
1065
|
+
},
|
|
1066
|
+
);
|
|
1053
1067
|
}
|
|
1054
1068
|
|
|
1055
1069
|
if (options?.signal?.aborted) {
|
|
@@ -1081,7 +1095,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
1081
1095
|
const status = extractHttpStatusFromError(error);
|
|
1082
1096
|
if (
|
|
1083
1097
|
!isLastEndpoint &&
|
|
1084
|
-
!
|
|
1098
|
+
!started &&
|
|
1085
1099
|
(AIError.isTransientStatus(status) ||
|
|
1086
1100
|
(status === undefined &&
|
|
1087
1101
|
!(error instanceof AIError.ProviderResponseError && error.kind === "output") &&
|