@oh-my-pi/pi-ai 16.2.1 → 16.2.2
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 +27 -0
- package/dist/types/auth-gateway/server.d.ts +0 -19
- package/dist/types/auth-retry.d.ts +2 -10
- package/dist/types/auth-storage.d.ts +1 -2
- package/dist/types/dialect/demotion.d.ts +22 -0
- package/dist/types/dialect/index.d.ts +2 -0
- package/dist/types/error/abort.d.ts +14 -0
- package/dist/types/error/auth-classify.d.ts +16 -0
- package/dist/types/error/auth.d.ts +27 -0
- package/dist/types/error/aws.d.ts +23 -0
- package/dist/types/error/classes.d.ts +102 -0
- package/dist/types/error/finalize.d.ts +39 -0
- package/dist/types/error/flags.d.ts +70 -0
- package/dist/types/error/format.d.ts +20 -0
- package/dist/types/error/gateway.d.ts +20 -0
- package/dist/types/error/index.d.ts +13 -0
- package/dist/types/error/oauth.d.ts +43 -0
- package/dist/types/error/provider.d.ts +42 -0
- package/dist/types/{rate-limit-utils.d.ts → error/rate-limit.d.ts} +14 -1
- package/dist/types/error/retryable.d.ts +27 -0
- package/dist/types/error/validation.d.ts +32 -0
- package/dist/types/index.d.ts +1 -3
- package/dist/types/providers/amazon-bedrock.d.ts +0 -5
- package/dist/types/providers/anthropic-client.d.ts +2 -16
- package/dist/types/providers/anthropic.d.ts +6 -1
- package/dist/types/providers/aws-eventstream.d.ts +2 -1
- package/dist/types/providers/cursor.d.ts +8 -7
- package/dist/types/providers/google-gemini-cli.d.ts +0 -5
- package/dist/types/providers/google-shared.d.ts +0 -5
- package/dist/types/providers/ollama.d.ts +0 -5
- package/dist/types/providers/openai-codex/request-transformer.d.ts +2 -2
- package/dist/types/providers/openai-codex/response-handler.d.ts +1 -1
- package/dist/types/providers/openai-codex-responses.d.ts +2 -2
- package/dist/types/providers/openai-responses-server-schema.d.ts +6 -0
- package/dist/types/providers/openai-responses-wire.d.ts +1 -1
- package/dist/types/providers/openai-responses.d.ts +1 -0
- package/dist/types/providers/openai-shared.d.ts +8 -6
- package/dist/types/providers/pi-native-client.d.ts +0 -9
- package/dist/types/registry/oauth/xai-oauth.d.ts +0 -9
- package/dist/types/types.d.ts +6 -0
- package/dist/types/utils/block-symbols.d.ts +20 -0
- package/dist/types/utils/openai-http.d.ts +4 -8
- package/dist/types/utils/retry.d.ts +2 -14
- package/dist/types/utils/thinking-loop.d.ts +3 -1
- package/package.json +8 -4
- package/src/api-registry.ts +3 -1
- package/src/auth-broker/discover.ts +7 -2
- package/src/auth-broker/remote-store.ts +8 -7
- package/src/auth-gateway/server.ts +27 -115
- package/src/auth-retry.ts +9 -21
- package/src/auth-storage.ts +34 -49
- package/src/dialect/demotion.ts +31 -0
- package/src/dialect/factory.ts +0 -2
- package/src/dialect/index.ts +2 -0
- package/src/dialect/owned-stream.ts +0 -1
- package/src/dialect/thinking.ts +22 -10
- package/src/error/abort.ts +18 -0
- package/src/error/auth-classify.ts +30 -0
- package/src/error/auth.ts +48 -0
- package/src/error/aws.ts +31 -0
- package/src/error/classes.ts +186 -0
- package/src/error/finalize.ts +69 -0
- package/src/error/flags.ts +486 -0
- package/src/error/format.ts +36 -0
- package/src/error/gateway.ts +96 -0
- package/src/error/index.ts +13 -0
- package/src/error/oauth.ts +58 -0
- package/src/error/provider.ts +56 -0
- package/src/{rate-limit-utils.ts → error/rate-limit.ts} +9 -3
- package/src/error/retryable.ts +70 -0
- package/src/error/validation.ts +44 -0
- package/src/index.ts +1 -3
- package/src/providers/amazon-bedrock.ts +61 -57
- package/src/providers/anthropic-client.ts +13 -41
- package/src/providers/anthropic-messages-server.ts +9 -2
- package/src/providers/anthropic.ts +84 -147
- package/src/providers/aws-credentials.ts +41 -11
- package/src/providers/aws-eventstream.ts +12 -21
- package/src/providers/azure-openai-responses.ts +27 -17
- package/src/providers/cursor.ts +59 -53
- package/src/providers/devin.ts +28 -20
- package/src/providers/gitlab-duo-workflow.ts +30 -10
- package/src/providers/gitlab-duo.ts +22 -6
- package/src/providers/google-auth.ts +15 -5
- package/src/providers/google-gemini-cli.ts +46 -60
- package/src/providers/google-shared.ts +40 -38
- package/src/providers/google-vertex.ts +3 -2
- package/src/providers/google.ts +5 -1
- package/src/providers/mock.ts +10 -7
- package/src/providers/ollama.ts +34 -29
- package/src/providers/openai-chat-server.ts +2 -1
- package/src/providers/openai-codex/request-transformer.ts +13 -12
- package/src/providers/openai-codex/response-handler.ts +1 -1
- package/src/providers/openai-codex-responses.ts +931 -1012
- package/src/providers/openai-completions.ts +46 -36
- package/src/providers/openai-responses-server-schema.ts +3 -0
- package/src/providers/openai-responses-server.ts +82 -30
- package/src/providers/openai-responses-wire.ts +1 -1
- package/src/providers/openai-responses.ts +46 -22
- package/src/providers/openai-shared.ts +118 -51
- package/src/providers/pi-native-client.ts +12 -18
- package/src/providers/pi-native-server.ts +9 -6
- package/src/providers/register-builtins.ts +5 -2
- package/src/providers/transform-messages.ts +31 -63
- package/src/registry/alibaba-coding-plan.ts +6 -5
- package/src/registry/api-key-login.ts +4 -3
- package/src/registry/api-key-validation.ts +4 -3
- package/src/registry/cloudflare-ai-gateway.ts +4 -3
- package/src/registry/coreweave.ts +2 -1
- package/src/registry/deepseek.ts +2 -1
- package/src/registry/google-antigravity.ts +2 -1
- package/src/registry/google-gemini-cli.ts +2 -1
- package/src/registry/kagi.ts +4 -3
- package/src/registry/kilo.ts +32 -10
- package/src/registry/litellm.ts +4 -3
- package/src/registry/llama-cpp.ts +3 -2
- package/src/registry/lm-studio.ts +3 -2
- package/src/registry/nvidia.ts +7 -7
- package/src/registry/oauth/anthropic.ts +23 -6
- package/src/registry/oauth/callback-server.ts +5 -3
- package/src/registry/oauth/cursor.ts +18 -4
- package/src/registry/oauth/devin.ts +14 -3
- package/src/registry/oauth/github-copilot.ts +21 -10
- package/src/registry/oauth/gitlab-duo-workflow.ts +15 -3
- package/src/registry/oauth/gitlab-duo.ts +30 -6
- package/src/registry/oauth/google-antigravity.ts +14 -5
- package/src/registry/oauth/google-gemini-cli.ts +24 -7
- package/src/registry/oauth/google-oauth-shared.ts +6 -3
- package/src/registry/oauth/index.ts +19 -8
- package/src/registry/oauth/kimi.ts +40 -10
- package/src/registry/oauth/openai-codex.ts +26 -11
- package/src/registry/oauth/opencode.ts +4 -3
- package/src/registry/oauth/perplexity.ts +33 -11
- package/src/registry/oauth/xai-oauth.ts +62 -18
- package/src/registry/oauth/xiaomi.ts +21 -9
- package/src/registry/ollama-cloud.ts +5 -4
- package/src/registry/ollama.ts +3 -2
- package/src/registry/parallel.ts +4 -3
- package/src/registry/qwen-portal.ts +4 -3
- package/src/registry/tavily.ts +4 -3
- package/src/registry/vercel-ai-gateway.ts +4 -3
- package/src/registry/vllm.ts +3 -2
- package/src/stream.ts +79 -24
- package/src/types.ts +6 -0
- package/src/usage/claude.ts +2 -1
- package/src/usage/github-copilot.ts +4 -3
- package/src/usage/google-antigravity.ts +2 -1
- package/src/utils/abort.ts +4 -2
- package/src/utils/block-symbols.ts +32 -0
- package/src/utils/event-stream.ts +15 -3
- package/src/utils/http-inspector.ts +4 -4
- package/src/utils/idle-iterator.ts +6 -5
- package/src/utils/openai-http.ts +11 -46
- package/src/utils/parse-bind.ts +7 -5
- package/src/utils/proxy.ts +2 -1
- package/src/utils/retry.ts +7 -23
- package/src/utils/schema/normalize.ts +3 -2
- package/src/utils/thinking-loop.ts +15 -11
- package/src/utils/validation.ts +4 -3
- package/dist/types/dialect/pi.d.ts +0 -9
- package/dist/types/errors.d.ts +0 -24
- package/dist/types/utils/overflow.d.ts +0 -55
- package/src/dialect/pi.md +0 -55
- package/src/dialect/pi.ts +0 -600
- package/src/errors.ts +0 -32
- package/src/utils/overflow.ts +0 -140
package/src/utils/openai-http.ts
CHANGED
|
@@ -15,7 +15,11 @@
|
|
|
15
15
|
* chain-state detectors, which regex over `error.message`.
|
|
16
16
|
*/
|
|
17
17
|
import { fetchWithRetry, readSseJson, type SseEventObserver } from "@oh-my-pi/pi-utils";
|
|
18
|
-
import
|
|
18
|
+
import * as AIError from "../error";
|
|
19
|
+
import { OpenAIHttpError } from "../error";
|
|
20
|
+
|
|
21
|
+
export { OpenAIHttpError };
|
|
22
|
+
|
|
19
23
|
import type { FetchImpl } from "../types";
|
|
20
24
|
import type { CapturedHttpErrorResponse } from "./http-inspector";
|
|
21
25
|
|
|
@@ -28,17 +32,6 @@ const DEFAULT_MAX_ATTEMPTS = 6;
|
|
|
28
32
|
/** Bound the `Error.message` allocation for proxy HTML error pages and the like. */
|
|
29
33
|
const MAX_DETAIL_CHARS = 4096;
|
|
30
34
|
|
|
31
|
-
/** Non-2xx response from an OpenAI-wire endpoint, with the decoded body attached. */
|
|
32
|
-
export class OpenAIHttpError extends ProviderHttpError {
|
|
33
|
-
readonly captured: CapturedHttpErrorResponse;
|
|
34
|
-
|
|
35
|
-
constructor(message: string, captured: CapturedHttpErrorResponse, code: string | undefined) {
|
|
36
|
-
super(message, captured.status, { headers: captured.headers, code });
|
|
37
|
-
this.name = "OpenAIHttpError";
|
|
38
|
-
this.captured = captured;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
35
|
export interface OpenAIStreamRequestInit {
|
|
43
36
|
url: string;
|
|
44
37
|
headers: Record<string, string>;
|
|
@@ -88,7 +81,9 @@ export async function postOpenAIStream<TEvent>(init: OpenAIStreamRequestInit): P
|
|
|
88
81
|
throw await captureOpenAIHttpError(response);
|
|
89
82
|
}
|
|
90
83
|
if (!response.body) {
|
|
91
|
-
throw new
|
|
84
|
+
throw new AIError.ProviderResponseError(`OpenAI stream response has no body (status ${response.status})`, {
|
|
85
|
+
kind: "envelope",
|
|
86
|
+
});
|
|
92
87
|
}
|
|
93
88
|
return {
|
|
94
89
|
events: readSseJson<TEvent>(response.body, init.signal, init.onSseEvent),
|
|
@@ -98,7 +93,7 @@ export async function postOpenAIStream<TEvent>(init: OpenAIStreamRequestInit): P
|
|
|
98
93
|
}
|
|
99
94
|
|
|
100
95
|
/** Decode a non-2xx response into an {@link OpenAIHttpError} without consuming it twice. */
|
|
101
|
-
export async function captureOpenAIHttpError(response: Response): Promise<OpenAIHttpError> {
|
|
96
|
+
export async function captureOpenAIHttpError(response: Response): Promise<AIError.OpenAIHttpError> {
|
|
102
97
|
let bodyText: string | undefined;
|
|
103
98
|
let bodyJson: unknown;
|
|
104
99
|
try {
|
|
@@ -117,41 +112,11 @@ export async function captureOpenAIHttpError(response: Response): Promise<OpenAI
|
|
|
117
112
|
bodyText,
|
|
118
113
|
bodyJson,
|
|
119
114
|
};
|
|
120
|
-
const { detail, code } =
|
|
115
|
+
const { detail, code } = OpenAIHttpError.parseEnvelope(bodyJson, bodyText);
|
|
121
116
|
// "status code (no body)" matches the SDK's former APIError phrasing;
|
|
122
117
|
// `finalizeErrorMessage` keys a repair path on that exact wording.
|
|
123
118
|
const message = detail
|
|
124
119
|
? `${response.status} ${detail.length > MAX_DETAIL_CHARS ? detail.slice(0, MAX_DETAIL_CHARS) : detail}`
|
|
125
120
|
: `${response.status} status code (no body)`;
|
|
126
|
-
return new OpenAIHttpError(message, captured, code);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Pull a human-readable message and machine code out of an OpenAI-style error
|
|
131
|
-
* envelope (`{ error: { message, code, type } }`), tolerating the flat shapes
|
|
132
|
-
* compat hosts return (`{ error: "..." }`, `{ message: "..." }`) and falling
|
|
133
|
-
* back to the raw body text.
|
|
134
|
-
*/
|
|
135
|
-
function extractErrorDetail(
|
|
136
|
-
bodyJson: unknown,
|
|
137
|
-
bodyText: string | undefined,
|
|
138
|
-
): { detail: string | undefined; code: string | undefined } {
|
|
139
|
-
if (typeof bodyJson === "object" && bodyJson !== null) {
|
|
140
|
-
const envelope = bodyJson as { error?: unknown; message?: unknown };
|
|
141
|
-
const error = envelope.error;
|
|
142
|
-
if (typeof error === "object" && error !== null) {
|
|
143
|
-
const { message, code, type } = error as { message?: unknown; code?: unknown; type?: unknown };
|
|
144
|
-
return {
|
|
145
|
-
detail: typeof message === "string" && message.length > 0 ? message : bodyText,
|
|
146
|
-
code: typeof code === "string" ? code : typeof type === "string" ? type : undefined,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
if (typeof error === "string" && error.length > 0) {
|
|
150
|
-
return { detail: error, code: undefined };
|
|
151
|
-
}
|
|
152
|
-
if (typeof envelope.message === "string" && envelope.message.length > 0) {
|
|
153
|
-
return { detail: envelope.message, code: undefined };
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
return { detail: bodyText, code: undefined };
|
|
121
|
+
return new AIError.OpenAIHttpError(message, captured, code);
|
|
157
122
|
}
|
package/src/utils/parse-bind.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* gateway used to silently allow empty hostnames; this fixes it).
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import * as AIError from "../error";
|
|
8
|
+
|
|
7
9
|
export interface ParsedBind {
|
|
8
10
|
hostname: string;
|
|
9
11
|
port: number;
|
|
@@ -11,11 +13,11 @@ export interface ParsedBind {
|
|
|
11
13
|
|
|
12
14
|
function parsePort(raw: string, bind: string): number {
|
|
13
15
|
if (!/^\d+$/.test(raw)) {
|
|
14
|
-
throw new
|
|
16
|
+
throw new AIError.ConfigurationError(`Invalid bind '${bind}'; port must be an integer.`);
|
|
15
17
|
}
|
|
16
18
|
const port = Number.parseInt(raw, 10);
|
|
17
19
|
if (!Number.isFinite(port) || port < 0 || port > 65535) {
|
|
18
|
-
throw new
|
|
20
|
+
throw new AIError.ConfigurationError(`Invalid bind '${bind}'; port out of range.`);
|
|
19
21
|
}
|
|
20
22
|
return port;
|
|
21
23
|
}
|
|
@@ -36,19 +38,19 @@ function parsePort(raw: string, bind: string): number {
|
|
|
36
38
|
export function parseBind(raw: string): ParsedBind {
|
|
37
39
|
const trimmed = raw.trim();
|
|
38
40
|
if (trimmed.length === 0) {
|
|
39
|
-
throw new
|
|
41
|
+
throw new AIError.ConfigurationError("Invalid bind; expected 'host:port' or 'port'.");
|
|
40
42
|
}
|
|
41
43
|
if (/^\d+$/.test(trimmed)) {
|
|
42
44
|
return { hostname: "127.0.0.1", port: parsePort(trimmed, raw) };
|
|
43
45
|
}
|
|
44
46
|
const lastColon = trimmed.lastIndexOf(":");
|
|
45
47
|
if (lastColon < 0) {
|
|
46
|
-
throw new
|
|
48
|
+
throw new AIError.ConfigurationError(`Invalid bind '${raw}'; expected 'host:port' or 'port'.`);
|
|
47
49
|
}
|
|
48
50
|
const hostPart = trimmed.slice(0, lastColon);
|
|
49
51
|
const portPart = trimmed.slice(lastColon + 1);
|
|
50
52
|
if (hostPart.length === 0) {
|
|
51
|
-
throw new
|
|
53
|
+
throw new AIError.ConfigurationError(`Invalid bind '${raw}'; host must not be empty.`);
|
|
52
54
|
}
|
|
53
55
|
return { hostname: hostPart, port: parsePort(portPart, raw) };
|
|
54
56
|
}
|
package/src/utils/proxy.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as net from "node:net";
|
|
2
2
|
import * as tls from "node:tls";
|
|
3
|
+
import * as AIError from "../error";
|
|
3
4
|
import type { FetchImpl } from "../types";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -228,7 +229,7 @@ export async function connectProxiedSocket(proxyUrlStr: string, targetUrlStr: st
|
|
|
228
229
|
tlsSocket.once("error", reject);
|
|
229
230
|
} else {
|
|
230
231
|
rawSocket.destroy();
|
|
231
|
-
reject(new
|
|
232
|
+
reject(new AIError.ValidationError(`Proxy tunnel failed: ${firstLine}`));
|
|
232
233
|
}
|
|
233
234
|
}
|
|
234
235
|
};
|
package/src/utils/retry.ts
CHANGED
|
@@ -1,27 +1,11 @@
|
|
|
1
1
|
import { scheduler } from "node:timers/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { isRetryableError } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { isCopilotTransientModelError, status } from "../error/flags";
|
|
3
4
|
import { getHeadersFromError, getRetryAfterMsFromHeaders } from "./retry-after";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* though the model is listed as enabled on the user's account via `/models`.
|
|
9
|
-
*
|
|
10
|
-
* Root cause: Copilot's request-routing backend is rolled out per OAuth
|
|
11
|
-
* client. Our OAuth client id is shared with opencode; VS Code uses its own
|
|
12
|
-
* client and sees full availability, so the same account may succeed in VS
|
|
13
|
-
* Code and flap between 200/400 here. See opencode#13313 and copilot-cli#2597.
|
|
14
|
-
*
|
|
15
|
-
* Retrying the identical request 2-3 times almost always lands on a backend
|
|
16
|
-
* that has the model, so we wrap the initial request with a short retry loop.
|
|
17
|
-
*/
|
|
18
|
-
export function isCopilotTransientModelError(error: unknown): boolean {
|
|
19
|
-
if (extractHttpStatusFromError(error) !== 400) return false;
|
|
20
|
-
if (!error || typeof error !== "object") return false;
|
|
21
|
-
const info = error as { code?: unknown; error?: { code?: unknown } | null };
|
|
22
|
-
const code = typeof info.code === "string" ? info.code : info.error?.code;
|
|
23
|
-
return code === "model_not_supported";
|
|
24
|
-
}
|
|
6
|
+
// `isCopilotTransientModelError` now lives in the error module (its classifier
|
|
7
|
+
// home). Re-exported here so existing `../utils/retry` importers keep working.
|
|
8
|
+
export { isCopilotTransientModelError };
|
|
25
9
|
|
|
26
10
|
const COPILOT_MODEL_RETRY_MAX_ATTEMPTS = 3;
|
|
27
11
|
const COPILOT_MODEL_RETRY_BASE_DELAY_MS = 400;
|
|
@@ -57,8 +41,8 @@ export async function callWithCopilotModelRetry<T>(
|
|
|
57
41
|
if (attempt === COPILOT_MODEL_RETRY_MAX_ATTEMPTS - 1) break;
|
|
58
42
|
let delayMs = retryBaseDelayMs * (attempt + 1);
|
|
59
43
|
if (!transientModelError) {
|
|
60
|
-
const
|
|
61
|
-
if (
|
|
44
|
+
const errorStatus = status(error);
|
|
45
|
+
if (errorStatus !== undefined) {
|
|
62
46
|
// Status-bearing retryable errors (429/5xx) are only re-sent when
|
|
63
47
|
// the server told us when to come back — a blind fixed-delay retry
|
|
64
48
|
// of a rate limit just burns the remaining attempts. Status-less
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* for each target.
|
|
8
8
|
*/
|
|
9
9
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
10
|
+
import * as AIError from "../../error";
|
|
10
11
|
import { dereferenceJsonSchema } from "./dereference";
|
|
11
12
|
import { upgradeJsonSchemaTo202012 } from "./draft";
|
|
12
13
|
import { areJsonValuesEqual, mergeCompatibleEnumSchemas, mergePropertySchemas } from "./equality";
|
|
@@ -1711,7 +1712,7 @@ export function enforceStrictSchema(
|
|
|
1711
1712
|
cache: WeakMap<Record<string, unknown>, Record<string, unknown>> = new WeakMap(),
|
|
1712
1713
|
): Record<string, unknown> {
|
|
1713
1714
|
if (!enter(schema)) {
|
|
1714
|
-
throw new
|
|
1715
|
+
throw new AIError.ValidationError("Schema contains a circular object graph — cannot enforce strict mode");
|
|
1715
1716
|
}
|
|
1716
1717
|
try {
|
|
1717
1718
|
const cached = cache.get(schema);
|
|
@@ -1857,7 +1858,7 @@ function enforceStrictSchemaBody(
|
|
|
1857
1858
|
!COMBINATOR_KEYS.some(key => Array.isArray(result[key])) &&
|
|
1858
1859
|
!isJsonObject(result.not)
|
|
1859
1860
|
) {
|
|
1860
|
-
throw new
|
|
1861
|
+
throw new AIError.ValidationError("Schema node has no type, combinator, or $ref — cannot enforce strict mode");
|
|
1861
1862
|
}
|
|
1862
1863
|
return result;
|
|
1863
1864
|
}
|
|
@@ -10,11 +10,9 @@
|
|
|
10
10
|
*
|
|
11
11
|
* This guard watches the streamed `thinking` deltas and, on a match, terminates
|
|
12
12
|
* the stream with a synthetic `error` {@link AssistantMessage} that carries
|
|
13
|
-
* **no observable content**. An empty-content `stopReason: "error"`
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* replay-unsafe and is never retried), so the turn is discarded and re-sampled
|
|
17
|
-
* instead of committing the garbage transcript.
|
|
13
|
+
* **no observable content**. An empty-content `stopReason: "error"` message tagged
|
|
14
|
+
* with `AIError.Flag.ThinkingLoop` lets result consumers and `AgentSession` discard
|
|
15
|
+
* the runaway and re-sample instead of committing garbage transcript.
|
|
18
16
|
*
|
|
19
17
|
* Three failure shapes are detected:
|
|
20
18
|
* 1. **Verbatim tail repetition** — a short unit repeated back-to-back (e.g.
|
|
@@ -33,12 +31,13 @@
|
|
|
33
31
|
*
|
|
34
32
|
* Scope is narrow: guarded Gemini/DeepSeek streams before any tool call. Native
|
|
35
33
|
* thinking is checked first; assistant text can also be checked for providers
|
|
36
|
-
* that surface reasoning as visible prose. On a hit
|
|
37
|
-
*
|
|
38
|
-
* it
|
|
39
|
-
* `PI_NO_THINKING_LOOP_GUARD=1`.
|
|
34
|
+
* that surface reasoning as visible prose. On a hit the failed turn is emitted as
|
|
35
|
+
* an empty retryable stream-stall error; result-awaiting callers (`complete`,
|
|
36
|
+
* `completeSimple`) re-sample it a few times and then let a stubborn loop cook
|
|
37
|
+
* through one unguarded pass. Disable detection with `PI_NO_THINKING_LOOP_GUARD=1`.
|
|
40
38
|
*/
|
|
41
39
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
40
|
+
import * as AIError from "../error";
|
|
42
41
|
import type { Api, AssistantMessage, Model, StreamOptions } from "../types";
|
|
43
42
|
import { AssistantMessageEventStream } from "./event-stream";
|
|
44
43
|
|
|
@@ -403,7 +402,9 @@ export function guardThinkingLoopStream(
|
|
|
403
402
|
provider: model.provider,
|
|
404
403
|
detail,
|
|
405
404
|
});
|
|
406
|
-
controller.abort(
|
|
405
|
+
controller.abort(
|
|
406
|
+
AIError.attach(new Error(THINKING_LOOP_ERROR_MARKER), AIError.create(AIError.Flag.ThinkingLoop)),
|
|
407
|
+
);
|
|
407
408
|
outer.push({
|
|
408
409
|
type: "error",
|
|
409
410
|
reason: "error",
|
|
@@ -433,7 +434,9 @@ export function guardThinkingLoopStream(
|
|
|
433
434
|
* Apply the loop guard around a provider dispatch. For non-guarded models
|
|
434
435
|
* (or when disabled) this is a transparent pass-through. For guarded models it injects a
|
|
435
436
|
* guard abort signal into the provider call so a detected loop tears down the
|
|
436
|
-
* upstream, then wraps the returned stream.
|
|
437
|
+
* upstream, then wraps the returned stream. The guard only raises the retryable
|
|
438
|
+
* stall; bounding the re-samples and the final cook pass lives in the
|
|
439
|
+
* result-awaiting caller.
|
|
437
440
|
*/
|
|
438
441
|
export function withGeminiThinkingLoopGuard<
|
|
439
442
|
O extends { signal?: AbortSignal; loopGuard?: { enabled?: boolean; checkAssistantContent?: boolean } },
|
|
@@ -474,6 +477,7 @@ function buildThinkingLoopError(model: Model<Api>, detail: string): AssistantMes
|
|
|
474
477
|
// "stream stall" makes the transport/session retry classifiers treat this
|
|
475
478
|
// as a transient (retryable) failure with no bespoke rule.
|
|
476
479
|
errorMessage: `${THINKING_LOOP_ERROR_MARKER}: the model repeated near-identical content (${detail}). Treating as a stream stall and retrying.`,
|
|
480
|
+
errorId: AIError.create(AIError.Flag.ThinkingLoop),
|
|
477
481
|
timestamp: Date.now(),
|
|
478
482
|
};
|
|
479
483
|
}
|
package/src/utils/validation.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { structuredCloneJSON } from "@oh-my-pi/pi-utils";
|
|
|
26
26
|
import { type Type, type } from "arktype";
|
|
27
27
|
import type { ZodType } from "zod/v4";
|
|
28
28
|
import type { $ZodIssue as ZodIssue } from "zod/v4/core";
|
|
29
|
+
import * as AIError from "../error";
|
|
29
30
|
import type { Tool, ToolCall } from "../types";
|
|
30
31
|
import { upgradeJsonSchemaTo202012 } from "./schema/draft";
|
|
31
32
|
import {
|
|
@@ -1365,7 +1366,7 @@ const MAX_COERCION_PASSES = 5;
|
|
|
1365
1366
|
export function validateToolCall(tools: Tool[], toolCall: ToolCall): ToolCall["arguments"] {
|
|
1366
1367
|
const tool = tools.find(t => t.name === toolCall.name);
|
|
1367
1368
|
if (!tool) {
|
|
1368
|
-
throw new
|
|
1369
|
+
throw new AIError.ToolNotFoundError(toolCall.name);
|
|
1369
1370
|
}
|
|
1370
1371
|
return validateToolArguments(tool, toolCall);
|
|
1371
1372
|
}
|
|
@@ -1405,7 +1406,7 @@ export function validateToolArguments(tool: Tool, toolCall: ToolCall): ToolCall[
|
|
|
1405
1406
|
rawJson.length <= maxLen
|
|
1406
1407
|
? rawJson
|
|
1407
1408
|
: `${rawJson.slice(0, maxLen)}… [truncated ${rawJson.length - maxLen} chars]`;
|
|
1408
|
-
throw new
|
|
1409
|
+
throw new AIError.ValidationError(
|
|
1409
1410
|
`Validation failed for tool "${toolCall.name}": Tool call arguments are not valid JSON.\nParse Error: ${parseError}\nRaw JSON:\n${truncatedRawJson}`,
|
|
1410
1411
|
);
|
|
1411
1412
|
}
|
|
@@ -1501,5 +1502,5 @@ export function validateToolArguments(tool: Tool, toolCall: ToolCall): ToolCall[
|
|
|
1501
1502
|
toolCall.name
|
|
1502
1503
|
}":\n${errors}\n\nReceived arguments:\n${JSON.stringify(receivedArgs, null, 2)}`;
|
|
1503
1504
|
|
|
1504
|
-
throw new
|
|
1505
|
+
throw new AIError.ValidationError(errorMessage);
|
|
1505
1506
|
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { DialectDefinition, InbandScanEvent, InbandScanner, InbandScannerOptions } from "./types";
|
|
2
|
-
export declare class PiNativeInbandScanner implements InbandScanner {
|
|
3
|
-
#private;
|
|
4
|
-
constructor(options?: InbandScannerOptions);
|
|
5
|
-
feed(text: string): InbandScanEvent[];
|
|
6
|
-
flush(): InbandScanEvent[];
|
|
7
|
-
}
|
|
8
|
-
declare const definition: DialectDefinition;
|
|
9
|
-
export default definition;
|
package/dist/types/errors.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Structured HTTP errors thrown by provider clients.
|
|
3
|
-
*
|
|
4
|
-
* Downstream classification reads these fields structurally rather than via
|
|
5
|
-
* `instanceof`: `extractHttpStatusFromError` (pi-utils) reads `status`,
|
|
6
|
-
* `getHeadersFromError` (retry-after extraction) reads `headers`, and retry
|
|
7
|
-
* policies such as `isCopilotTransientModelError` read `code`. Per-provider
|
|
8
|
-
* subclasses exist so call sites can narrow with `instanceof` and logs carry
|
|
9
|
-
* a meaningful `error.name`.
|
|
10
|
-
*/
|
|
11
|
-
export interface ProviderHttpErrorOptions {
|
|
12
|
-
/** Response headers; enables `retry-after`/rate-limit extraction downstream. */
|
|
13
|
-
headers?: Headers;
|
|
14
|
-
/** Machine-readable error code from the response body (`error.code` / `error.type`). */
|
|
15
|
-
code?: string;
|
|
16
|
-
cause?: unknown;
|
|
17
|
-
}
|
|
18
|
-
/** Non-2xx HTTP response from a provider endpoint. */
|
|
19
|
-
export declare class ProviderHttpError extends Error {
|
|
20
|
-
readonly status: number;
|
|
21
|
-
readonly headers: Headers | undefined;
|
|
22
|
-
readonly code: string | undefined;
|
|
23
|
-
constructor(message: string, status: number, options?: ProviderHttpErrorOptions);
|
|
24
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import type { AssistantMessage } from "../types";
|
|
2
|
-
/**
|
|
3
|
-
* Check if an assistant message represents a context overflow error.
|
|
4
|
-
*
|
|
5
|
-
* This handles two cases:
|
|
6
|
-
* 1. Error-based overflow: Most providers return stopReason "error" with a
|
|
7
|
-
* specific error message pattern.
|
|
8
|
-
* 2. Silent overflow: Some providers accept overflow requests and return
|
|
9
|
-
* successfully. For these, we check if usage.input exceeds the context window.
|
|
10
|
-
*
|
|
11
|
-
* ## Reliability by Provider
|
|
12
|
-
*
|
|
13
|
-
* **Reliable detection (returns error with detectable message):**
|
|
14
|
-
* - Anthropic: "prompt is too long: X tokens > Y maximum"
|
|
15
|
-
* - OpenAI (Completions & Responses): "exceeds the context window"
|
|
16
|
-
* - Google Gemini: "input token count exceeds the maximum"
|
|
17
|
-
* - xAI (Grok): "maximum prompt length is X but request contains Y"
|
|
18
|
-
* - Groq: "reduce the length of the messages"
|
|
19
|
-
* - Cerebras: 400/413 status code (no body)
|
|
20
|
-
* - Mistral: 400/413 status code (no body)
|
|
21
|
-
* - HTTP 413 payload/entity-too-large variants
|
|
22
|
-
* - OpenRouter (all backends): "maximum context length is X tokens"
|
|
23
|
-
* - llama.cpp: "exceeds the available context size"
|
|
24
|
-
* - LM Studio: "greater than the context length"
|
|
25
|
-
* - Kimi For Coding: "exceeded model token limit: X (requested: Y)"
|
|
26
|
-
* - Anthropic 413: "request_too_large" (request body exceeds size limit)
|
|
27
|
-
* - HTTP 413: "Payload Too Large" / "Request Entity Too Large"
|
|
28
|
-
* - Ollama OpenAI-compatible: "prompt filled the context window"
|
|
29
|
-
*
|
|
30
|
-
* **Unreliable detection:**
|
|
31
|
-
* - z.ai: Sometimes accepts overflow silently (detectable via usage.input > contextWindow),
|
|
32
|
-
* sometimes returns rate limit errors. Pass contextWindow param to detect silent overflow.
|
|
33
|
-
* - Ollama native: Silently truncates input without error. Cannot be detected via this function.
|
|
34
|
-
* The response will have usage.input < expected, but we don't know the expected value.
|
|
35
|
-
*
|
|
36
|
-
* ## Custom Providers
|
|
37
|
-
*
|
|
38
|
-
* If you've added custom models via settings.json, this function may not detect
|
|
39
|
-
* overflow errors from those providers. To add support:
|
|
40
|
-
*
|
|
41
|
-
* 1. Send a request that exceeds the model's context window
|
|
42
|
-
* 2. Check the errorMessage in the response
|
|
43
|
-
* 3. Create a regex pattern that matches the error
|
|
44
|
-
* 4. The pattern should be added to OVERFLOW_PATTERNS in this file, or
|
|
45
|
-
* check the errorMessage yourself before calling this function
|
|
46
|
-
*
|
|
47
|
-
* @param message - The assistant message to check
|
|
48
|
-
* @param contextWindow - Optional context window size for detecting silent overflow (z.ai)
|
|
49
|
-
* @returns true if the message indicates a context overflow
|
|
50
|
-
*/
|
|
51
|
-
export declare function isContextOverflow(message: AssistantMessage, contextWindow?: number): boolean;
|
|
52
|
-
/**
|
|
53
|
-
* Get the overflow patterns for testing purposes.
|
|
54
|
-
*/
|
|
55
|
-
export declare function getOverflowPatterns(): RegExp[];
|
package/src/dialect/pi.md
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
## Format guide
|
|
2
|
-
|
|
3
|
-
A tool call begins with `§` immediately followed by the function NAME (start each call on its own line). Scalar arguments follow on the same line as `key=value` pairs; a single large or multi-line string argument goes in a verbatim body fenced by `«…»` right after the header.
|
|
4
|
-
|
|
5
|
-
Scalar-only call (the line ends the call):
|
|
6
|
-
|
|
7
|
-
```text
|
|
8
|
-
§read path=src/a.ts offset=50 limit=200
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Call with a verbatim body — everything between `«` and `»` is taken literally, no quoting or escaping:
|
|
12
|
-
|
|
13
|
-
```text
|
|
14
|
-
§edit path=src/server/auth.ts«
|
|
15
|
-
*** Begin Patch
|
|
16
|
-
*** Update File: src/server/auth.ts
|
|
17
|
-
@@ class AuthService
|
|
18
|
-
- login(user) {
|
|
19
|
-
+ async login(user, opts) {
|
|
20
|
-
*** End Patch
|
|
21
|
-
»
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Argument values:
|
|
25
|
-
|
|
26
|
-
- Strings are written bare and verbatim (`path=src/a.ts`). Quote with `"…"` only when the value contains spaces or starts with `"`, `[`, or `{` (`i="run the tests"`).
|
|
27
|
-
- Numbers, booleans, and `null` are JSON literals (`offset=50`, `force=true`).
|
|
28
|
-
- Arrays and objects are inline JSON (`paths=["src","test"]`).
|
|
29
|
-
- The body fence holds the call's first long/multi-line string parameter; its key is implied, never written.
|
|
30
|
-
|
|
31
|
-
Private reasoning goes in a `¤…¤` block before your calls:
|
|
32
|
-
|
|
33
|
-
```text
|
|
34
|
-
¤
|
|
35
|
-
brief reasoning
|
|
36
|
-
¤
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
Tool results arrive in `‡‡…‡‡` blocks, read in call order:
|
|
40
|
-
|
|
41
|
-
```text
|
|
42
|
-
‡‡
|
|
43
|
-
verbatim tool result
|
|
44
|
-
‡‡
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## Rules
|
|
48
|
-
|
|
49
|
-
- `NAME` MUST match a listed function; never wrap calls in JSON or fences.
|
|
50
|
-
- Put each scalar argument once as `key=value`; reserve the `«…»` body for the one dominant string argument (file contents, patches, commands, queries).
|
|
51
|
-
- Body text is verbatim — include no surrounding quotes. If the body itself contains `»`, widen BOTH guillemet fences equally (`««…»»`, `«««…»»»`).
|
|
52
|
-
- Emit parallel calls as consecutive `§…` blocks. NEVER invent call ids; results are positional.
|
|
53
|
-
- Private reasoning goes in a `¤…¤` block before your calls; NEVER put calls inside it, and keep a literal `¤` out of the reasoning text.
|
|
54
|
-
- Read each `‡‡…‡‡` result in call order. NEVER emit a `‡‡` block yourself.
|
|
55
|
-
- Emit the stop sequence ONLY after the call is fully written — NEVER announce a tool then stop (e.g. halting at "Let's run `cargo clippy`" with no `§` call emitted). Write the complete call, THEN the stop sequence, THEN halt.
|