@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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as AIError from "../error";
|
|
1
2
|
import type { OAuthController, OAuthLoginCallbacks } from "./oauth/types";
|
|
2
3
|
import type { ProviderDefinition } from "./types";
|
|
3
4
|
|
|
@@ -5,7 +6,7 @@ const AUTH_URL = "https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai-gateway%2Fapi
|
|
|
5
6
|
|
|
6
7
|
export async function loginVercelAiGateway(options: OAuthController): Promise<string> {
|
|
7
8
|
if (!options.onPrompt) {
|
|
8
|
-
throw new
|
|
9
|
+
throw new AIError.OnPromptRequiredError("Vercel AI Gateway");
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
options.onAuth?.({
|
|
@@ -19,12 +20,12 @@ export async function loginVercelAiGateway(options: OAuthController): Promise<st
|
|
|
19
20
|
});
|
|
20
21
|
|
|
21
22
|
if (options.signal?.aborted) {
|
|
22
|
-
throw new
|
|
23
|
+
throw new AIError.LoginCancelledError();
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const trimmed = apiKey.trim();
|
|
26
27
|
if (!trimmed) {
|
|
27
|
-
throw new
|
|
28
|
+
throw new AIError.ApiKeyRequiredError();
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
return trimmed;
|
package/src/registry/vllm.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as AIError from "../error";
|
|
1
2
|
import type { OAuthController, OAuthLoginCallbacks, OAuthProvider } from "./oauth/types";
|
|
2
3
|
import type { ProviderDefinition } from "./types";
|
|
3
4
|
|
|
@@ -8,7 +9,7 @@ const DEFAULT_LOCAL_TOKEN = "vllm-local";
|
|
|
8
9
|
|
|
9
10
|
export async function loginVllm(options: OAuthController): Promise<string> {
|
|
10
11
|
if (!options.onPrompt) {
|
|
11
|
-
throw new
|
|
12
|
+
throw new AIError.OnPromptRequiredError(PROVIDER_ID);
|
|
12
13
|
}
|
|
13
14
|
options.onAuth?.({
|
|
14
15
|
url: AUTH_URL,
|
|
@@ -20,7 +21,7 @@ export async function loginVllm(options: OAuthController): Promise<string> {
|
|
|
20
21
|
allowEmpty: true,
|
|
21
22
|
});
|
|
22
23
|
if (options.signal?.aborted) {
|
|
23
|
-
throw new
|
|
24
|
+
throw new AIError.LoginCancelledError();
|
|
24
25
|
}
|
|
25
26
|
const trimmed = apiKey.trim();
|
|
26
27
|
return trimmed || DEFAULT_LOCAL_TOKEN;
|
package/src/stream.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as crypto from "node:crypto";
|
|
|
2
2
|
import * as fsSync from "node:fs";
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { scheduler } from "node:timers/promises";
|
|
5
6
|
import type { Effort } from "@oh-my-pi/pi-catalog/effort";
|
|
6
7
|
import { isVertexExpressOpenAIUrl, isVertexRawPredictUrl } from "@oh-my-pi/pi-catalog/hosts";
|
|
7
8
|
import {
|
|
@@ -12,10 +13,12 @@ import {
|
|
|
12
13
|
resolveWireModelId,
|
|
13
14
|
} from "@oh-my-pi/pi-catalog/model-thinking";
|
|
14
15
|
import { CATALOG_PROVIDERS, type ProviderCatalogEntry } from "@oh-my-pi/pi-catalog/provider-models";
|
|
15
|
-
import { $env, $pickenv,
|
|
16
|
+
import { $env, $pickenv, getConfigRootDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
16
17
|
import { getCustomApi } from "./api-registry";
|
|
17
18
|
import { AUTH_RETRY_STEPS, isApiKeyResolver, resolveRetryKey } from "./auth-retry";
|
|
18
|
-
import
|
|
19
|
+
import * as AIError from "./error";
|
|
20
|
+
import { ProviderHttpError } from "./error";
|
|
21
|
+
import { isUsageLimitOutcome } from "./error/rate-limit";
|
|
19
22
|
import type { BedrockOptions } from "./providers/amazon-bedrock";
|
|
20
23
|
import type { AnthropicOptions } from "./providers/anthropic";
|
|
21
24
|
import type { CursorOptions } from "./providers/cursor";
|
|
@@ -53,7 +56,6 @@ import {
|
|
|
53
56
|
streamOpenAIResponses,
|
|
54
57
|
} from "./providers/register-builtins";
|
|
55
58
|
import { isSyntheticModel, streamSynthetic } from "./providers/synthetic";
|
|
56
|
-
import { isUsageLimitOutcome } from "./rate-limit-utils";
|
|
57
59
|
import { PROVIDER_REGISTRY } from "./registry";
|
|
58
60
|
import type {
|
|
59
61
|
Api,
|
|
@@ -269,7 +271,7 @@ async function acquireProviderInFlightLock(provider: string, signal?: AbortSigna
|
|
|
269
271
|
await fs.mkdir(path.dirname(lockDir), { recursive: true });
|
|
270
272
|
|
|
271
273
|
while (true) {
|
|
272
|
-
if (signal?.aborted) throw signal.reason ?? new
|
|
274
|
+
if (signal?.aborted) throw signal.reason ?? new AIError.AbortError("Provider request aborted before dispatch");
|
|
273
275
|
try {
|
|
274
276
|
await fs.mkdir(lockDir);
|
|
275
277
|
const lockIdentity = await readProviderInFlightLockIdentity(lockDir);
|
|
@@ -374,7 +376,8 @@ async function signalProviderInFlightWaiters(provider: string): Promise<void> {
|
|
|
374
376
|
}
|
|
375
377
|
|
|
376
378
|
function waitForProviderInFlightSignal(provider: string, signal?: AbortSignal): Promise<void> {
|
|
377
|
-
if (signal?.aborted)
|
|
379
|
+
if (signal?.aborted)
|
|
380
|
+
return Promise.reject(signal.reason ?? new AIError.AbortError("Provider request aborted before dispatch"));
|
|
378
381
|
const signalPath = providerInFlightSignalPath(provider);
|
|
379
382
|
const waitStarted = Date.now();
|
|
380
383
|
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
@@ -390,7 +393,7 @@ function waitForProviderInFlightSignal(provider: string, signal?: AbortSignal):
|
|
|
390
393
|
settle();
|
|
391
394
|
};
|
|
392
395
|
const onAbort = () => {
|
|
393
|
-
finish(() => reject(signal?.reason ?? new
|
|
396
|
+
finish(() => reject(signal?.reason ?? new AIError.AbortError("Provider request aborted before dispatch")));
|
|
394
397
|
};
|
|
395
398
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
396
399
|
try {
|
|
@@ -446,7 +449,7 @@ async function acquireProviderInFlightSlot(
|
|
|
446
449
|
if (limit === undefined) return async () => {};
|
|
447
450
|
let loggedWait = false;
|
|
448
451
|
while (true) {
|
|
449
|
-
if (signal?.aborted) throw signal.reason ?? new
|
|
452
|
+
if (signal?.aborted) throw signal.reason ?? new AIError.AbortError("Provider request aborted before dispatch");
|
|
450
453
|
const lease = await tryAcquireProviderInFlightLease(provider, limit, signal);
|
|
451
454
|
if (lease) return () => releaseProviderInFlightLease(provider, lease);
|
|
452
455
|
if (!loggedWait) {
|
|
@@ -508,7 +511,7 @@ function withProviderInFlightLimit<TOptions extends Pick<StreamOptions, "signal"
|
|
|
508
511
|
logger.debug("Provider in-flight limit wait completed", { provider: model.provider, limit });
|
|
509
512
|
}
|
|
510
513
|
if (options?.signal?.aborted) {
|
|
511
|
-
throw options.signal.reason ?? new
|
|
514
|
+
throw options.signal.reason ?? new AIError.AbortError("Provider request aborted before dispatch");
|
|
512
515
|
}
|
|
513
516
|
const inner = dispatch();
|
|
514
517
|
try {
|
|
@@ -710,7 +713,7 @@ function streamDispatch<TApi extends Api>(
|
|
|
710
713
|
if (isGitLabDuoModel(model)) {
|
|
711
714
|
const apiKey = requestOptions.apiKey || getEnvApiKey(model.provider);
|
|
712
715
|
if (!apiKey) {
|
|
713
|
-
throw new
|
|
716
|
+
throw new AIError.MissingApiKeyError(model.provider);
|
|
714
717
|
}
|
|
715
718
|
return streamGitLabDuo(model, context, {
|
|
716
719
|
...(requestOptions as SimpleStreamOptions),
|
|
@@ -721,7 +724,7 @@ function streamDispatch<TApi extends Api>(
|
|
|
721
724
|
if (model.api === "gitlab-duo-agent") {
|
|
722
725
|
const apiKey = (requestOptions as StreamOptions | undefined)?.apiKey || getEnvApiKey(model.provider);
|
|
723
726
|
if (!apiKey) {
|
|
724
|
-
throw new
|
|
727
|
+
throw new AIError.MissingApiKeyError(model.provider);
|
|
725
728
|
}
|
|
726
729
|
return streamGitLabDuoWorkflow(model as Model<"gitlab-duo-agent">, context, {
|
|
727
730
|
...(requestOptions as StreamOptions | undefined),
|
|
@@ -739,7 +742,7 @@ function streamDispatch<TApi extends Api>(
|
|
|
739
742
|
|
|
740
743
|
const apiKey = requestOptions.apiKey || getEnvApiKey(model.provider);
|
|
741
744
|
if (!apiKey) {
|
|
742
|
-
throw new
|
|
745
|
+
throw new AIError.MissingApiKeyError(model.provider);
|
|
743
746
|
}
|
|
744
747
|
const providerOptions = isGoogleVertexAuthenticatedModel(model)
|
|
745
748
|
? {
|
|
@@ -823,17 +826,61 @@ function streamDispatch<TApi extends Api>(
|
|
|
823
826
|
return streamDevin(model as Model<"devin-agent">, context, providerOptions as DevinOptions);
|
|
824
827
|
|
|
825
828
|
default:
|
|
826
|
-
throw new
|
|
829
|
+
throw new AIError.ConfigurationError(`Unhandled API: ${api}`);
|
|
827
830
|
}
|
|
828
831
|
}
|
|
829
832
|
|
|
833
|
+
/** Thinking-loop re-samples spent before {@link resolveWithThinkingLoopCook} cooks. */
|
|
834
|
+
const THINKING_LOOP_MAX_ABORTS = 3;
|
|
835
|
+
const THINKING_LOOP_RETRY_BASE_DELAY_MS = 500;
|
|
836
|
+
const THINKING_LOOP_RETRY_MAX_DELAY_MS = 8_000;
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Resolve a completion, re-sampling a thinking-loop stall up to
|
|
840
|
+
* {@link THINKING_LOOP_MAX_ABORTS} times before letting it cook. The loop guard
|
|
841
|
+
* raises an empty `stopReason: "error"` stall on each guarded attempt; this
|
|
842
|
+
* result-path consumer re-dispatches a fresh request per stall and, once the abort
|
|
843
|
+
* budget is spent, runs one final pass with the guard disabled so a stubborn loop
|
|
844
|
+
* returns the model's raw output instead of a fatal stall. Non-stall results —
|
|
845
|
+
* including genuine errors — return immediately; a caller abort during backoff
|
|
846
|
+
* propagates so cancellation surfaces as an abort, never a stale stall result.
|
|
847
|
+
*/
|
|
848
|
+
async function resolveWithThinkingLoopCook(
|
|
849
|
+
signal: AbortSignal | undefined,
|
|
850
|
+
dispatch: () => AssistantMessageEventStream,
|
|
851
|
+
cook: () => AssistantMessageEventStream,
|
|
852
|
+
): Promise<AssistantMessage> {
|
|
853
|
+
let message = await dispatch().result();
|
|
854
|
+
let thinkingLoopRetry = AIError.is(message.errorId, AIError.Flag.ThinkingLoop);
|
|
855
|
+
for (let attempt = 0; thinkingLoopRetry && attempt < THINKING_LOOP_MAX_ABORTS - 1; attempt += 1) {
|
|
856
|
+
// A caller abort surfaces as a thrown abort (never the stall, which would
|
|
857
|
+
// misclassify as a 502): throwIfAborted before backoff, and scheduler.wait
|
|
858
|
+
// rejects if the abort lands mid-delay.
|
|
859
|
+
signal?.throwIfAborted();
|
|
860
|
+
const delay = Math.min(THINKING_LOOP_RETRY_BASE_DELAY_MS * 2 ** attempt, THINKING_LOOP_RETRY_MAX_DELAY_MS);
|
|
861
|
+
await scheduler.wait(delay, { signal });
|
|
862
|
+
message = await dispatch().result();
|
|
863
|
+
thinkingLoopRetry =
|
|
864
|
+
message.stopReason === "error" &&
|
|
865
|
+
message.content.length === 0 &&
|
|
866
|
+
AIError.is(message.errorId, AIError.Flag.ThinkingLoop);
|
|
867
|
+
}
|
|
868
|
+
if (!thinkingLoopRetry) return message;
|
|
869
|
+
signal?.throwIfAborted();
|
|
870
|
+
// Abort budget spent and still looping: let it cook with the guard disabled.
|
|
871
|
+
return cook().result();
|
|
872
|
+
}
|
|
873
|
+
|
|
830
874
|
export async function complete<TApi extends Api>(
|
|
831
875
|
model: Model<TApi>,
|
|
832
876
|
context: Context,
|
|
833
877
|
options?: OptionsForApi<TApi>,
|
|
834
878
|
): Promise<AssistantMessage> {
|
|
835
|
-
|
|
836
|
-
|
|
879
|
+
return resolveWithThinkingLoopCook(
|
|
880
|
+
options?.signal,
|
|
881
|
+
() => stream(model, context, options),
|
|
882
|
+
() => stream(model, context, { ...options, loopGuard: { ...options?.loopGuard, enabled: false } }),
|
|
883
|
+
);
|
|
837
884
|
}
|
|
838
885
|
|
|
839
886
|
type AuthRetryFailure = {
|
|
@@ -845,7 +892,7 @@ type AuthRetryFailure = {
|
|
|
845
892
|
function extractStatusFromAssistantError(message: AssistantMessage): number | undefined {
|
|
846
893
|
if (message.errorStatus !== undefined) return message.errorStatus;
|
|
847
894
|
if (!message.errorMessage) return undefined;
|
|
848
|
-
return
|
|
895
|
+
return AIError.status({ message: message.errorMessage });
|
|
849
896
|
}
|
|
850
897
|
|
|
851
898
|
function isRetryableUpstreamError(error: unknown, status: number | undefined, message: string | undefined): boolean {
|
|
@@ -868,7 +915,9 @@ function isRetryableUpstreamError(error: unknown, status: number | undefined, me
|
|
|
868
915
|
function createAssistantAuthError(message: AssistantMessage): Error {
|
|
869
916
|
const text = message.errorMessage ?? "Provider authentication failed";
|
|
870
917
|
const status = extractStatusFromAssistantError(message);
|
|
871
|
-
return status === undefined
|
|
918
|
+
return status === undefined
|
|
919
|
+
? new AIError.ProviderResponseError(text, { kind: "runtime" })
|
|
920
|
+
: new ProviderHttpError(text, status);
|
|
872
921
|
}
|
|
873
922
|
|
|
874
923
|
function emitBufferedEvents(stream: AssistantMessageEventStream, events: AssistantMessageEvent[]): void {
|
|
@@ -937,7 +986,7 @@ export function streamSimple<TApi extends Api>(
|
|
|
937
986
|
captureAuthFailure &&
|
|
938
987
|
isRetryableUpstreamError(
|
|
939
988
|
error,
|
|
940
|
-
|
|
989
|
+
AIError.status(error),
|
|
941
990
|
error instanceof Error ? error.message : undefined,
|
|
942
991
|
)
|
|
943
992
|
) {
|
|
@@ -965,7 +1014,7 @@ export function streamSimple<TApi extends Api>(
|
|
|
965
1014
|
// A thrown resolver is a broker/OAuth/network failure, not a missing
|
|
966
1015
|
// key — surface the cause instead of masking it as "No API key".
|
|
967
1016
|
outer.fail(
|
|
968
|
-
new
|
|
1017
|
+
new AIError.ConfigurationError(
|
|
969
1018
|
`Failed to resolve API key for provider ${model.provider}: ${error instanceof Error ? error.message : String(error)}`,
|
|
970
1019
|
{ cause: error },
|
|
971
1020
|
),
|
|
@@ -973,7 +1022,7 @@ export function streamSimple<TApi extends Api>(
|
|
|
973
1022
|
return;
|
|
974
1023
|
}
|
|
975
1024
|
if (lastKey === undefined) {
|
|
976
|
-
outer.fail(new
|
|
1025
|
+
outer.fail(new AIError.MissingApiKeyError(model.provider));
|
|
977
1026
|
return;
|
|
978
1027
|
}
|
|
979
1028
|
let failure = await runAttempt(lastKey, true);
|
|
@@ -1034,7 +1083,7 @@ export function streamSimple<TApi extends Api>(
|
|
|
1034
1083
|
const apiKey =
|
|
1035
1084
|
(typeof requestOptions?.apiKey === "string" ? requestOptions.apiKey : undefined) || getEnvApiKey(model.provider);
|
|
1036
1085
|
if (!apiKey) {
|
|
1037
|
-
throw new
|
|
1086
|
+
throw new AIError.MissingApiKeyError(model.provider);
|
|
1038
1087
|
}
|
|
1039
1088
|
|
|
1040
1089
|
// GitLab Duo - wraps Anthropic/OpenAI behind GitLab AI Gateway direct access tokens
|
|
@@ -1087,8 +1136,11 @@ export async function completeSimple<TApi extends Api>(
|
|
|
1087
1136
|
context: Context,
|
|
1088
1137
|
options?: SimpleStreamOptions,
|
|
1089
1138
|
): Promise<AssistantMessage> {
|
|
1090
|
-
|
|
1091
|
-
|
|
1139
|
+
return resolveWithThinkingLoopCook(
|
|
1140
|
+
options?.signal,
|
|
1141
|
+
() => streamSimple(model, context, options),
|
|
1142
|
+
() => streamSimple(model, context, { ...options, loopGuard: { ...options?.loopGuard, enabled: false } }),
|
|
1143
|
+
);
|
|
1092
1144
|
}
|
|
1093
1145
|
|
|
1094
1146
|
const MIN_OUTPUT_TOKENS = 1024;
|
|
@@ -1441,6 +1493,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1441
1493
|
openrouterVariant: options?.openrouterVariant,
|
|
1442
1494
|
maxTokensExplicit: rawOptions?.maxTokens !== undefined,
|
|
1443
1495
|
disableReasoning: options?.disableReasoning,
|
|
1496
|
+
textVerbosity: options?.textVerbosity,
|
|
1444
1497
|
});
|
|
1445
1498
|
}
|
|
1446
1499
|
return castApi<"openai-completions">({
|
|
@@ -1475,6 +1528,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1475
1528
|
openrouterVariant: options?.openrouterVariant,
|
|
1476
1529
|
maxTokensExplicit: rawOptions?.maxTokens !== undefined,
|
|
1477
1530
|
disableReasoning: options?.disableReasoning,
|
|
1531
|
+
textVerbosity: options?.textVerbosity,
|
|
1478
1532
|
});
|
|
1479
1533
|
|
|
1480
1534
|
case "azure-openai-responses":
|
|
@@ -1493,7 +1547,8 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1493
1547
|
toolChoice: mapOpenAiToolChoice(options?.toolChoice),
|
|
1494
1548
|
serviceTier: options?.serviceTier,
|
|
1495
1549
|
preferWebsockets: options?.preferWebsockets,
|
|
1496
|
-
reasoningSummary: options?.hideThinkingSummary ? null :
|
|
1550
|
+
reasoningSummary: options?.hideThinkingSummary ? null : "detailed",
|
|
1551
|
+
textVerbosity: options?.textVerbosity,
|
|
1497
1552
|
});
|
|
1498
1553
|
|
|
1499
1554
|
case "google-generative-ai": {
|
|
@@ -1665,7 +1720,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1665
1720
|
});
|
|
1666
1721
|
}
|
|
1667
1722
|
default:
|
|
1668
|
-
throw new
|
|
1723
|
+
throw new AIError.ConfigurationError(`Unhandled API in mapOptionsForApi: ${model.api}`);
|
|
1669
1724
|
}
|
|
1670
1725
|
}
|
|
1671
1726
|
|
package/src/types.ts
CHANGED
|
@@ -37,6 +37,7 @@ import type { OllamaChatOptions } from "./providers/ollama";
|
|
|
37
37
|
import type { OpenAICodexResponsesOptions } from "./providers/openai-codex-responses";
|
|
38
38
|
import type { OpenAICompletionsOptions } from "./providers/openai-completions";
|
|
39
39
|
import type { OpenAIResponsesOptions } from "./providers/openai-responses";
|
|
40
|
+
import type { kStreamingPartialJson } from "./utils/block-symbols";
|
|
40
41
|
import type { AssistantMessageEventStream } from "./utils/event-stream";
|
|
41
42
|
|
|
42
43
|
export type { StopDetails } from "./providers/anthropic-wire";
|
|
@@ -379,6 +380,8 @@ export interface SimpleStreamOptions extends Omit<StreamOptions, "apiKey"> {
|
|
|
379
380
|
* Useful when the UI hides thinking blocks anyway and the summary is wasted bandwidth.
|
|
380
381
|
*/
|
|
381
382
|
hideThinkingSummary?: boolean;
|
|
383
|
+
/** OpenAI Responses/Codex `text.verbosity` response detail level. */
|
|
384
|
+
textVerbosity?: "low" | "medium" | "high";
|
|
382
385
|
/** Custom token budgets for thinking levels (token-based providers only) */
|
|
383
386
|
thinkingBudgets?: ThinkingBudgets;
|
|
384
387
|
/** Cursor exec handlers for local tool execution */
|
|
@@ -457,6 +460,7 @@ export interface ToolCall {
|
|
|
457
460
|
id: string;
|
|
458
461
|
name: string;
|
|
459
462
|
arguments: Record<string, unknown>;
|
|
463
|
+
[kStreamingPartialJson]?: string;
|
|
460
464
|
thoughtSignature?: string; // Google-specific: opaque signature for reusing thought context
|
|
461
465
|
intent?: string; // Harness-level intent metadata extracted from traced tool arguments
|
|
462
466
|
/**
|
|
@@ -537,6 +541,8 @@ export interface AssistantMessage {
|
|
|
537
541
|
errorMessage?: string;
|
|
538
542
|
/** HTTP status surfaced by the provider when the request failed. Populated by every provider's catch block alongside `errorMessage` so consumers (auth retry, telemetry, UI) can branch without regex-scraping the message. */
|
|
539
543
|
errorStatus?: number;
|
|
544
|
+
/** Structured machine-readable error classifier; see `utils/error-id.ts` for bit layout and helpers. */
|
|
545
|
+
errorId?: number;
|
|
540
546
|
/**
|
|
541
547
|
* Stable identifiers for request features the provider silently dropped
|
|
542
548
|
* during this turn (e.g. `"priority"`). Set when a server-side rejection
|
package/src/usage/claude.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { scheduler } from "node:timers/promises";
|
|
2
2
|
import { toNumber } from "@oh-my-pi/pi-catalog/utils";
|
|
3
|
+
import * as AIError from "../error";
|
|
3
4
|
import { claudeCodeVersion } from "../providers/anthropic";
|
|
4
5
|
import type {
|
|
5
6
|
CredentialRankingStrategy,
|
|
@@ -148,7 +149,7 @@ function hasUsageData(payload: ClaudeUsageResponse): boolean {
|
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
function isRetryableStatus(status: number): boolean {
|
|
151
|
-
return
|
|
152
|
+
return AIError.isTransientStatus(status);
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
function isAbortError(error: unknown, signal?: AbortSignal): boolean {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { toBoolean, toNumber } from "@oh-my-pi/pi-catalog/utils";
|
|
8
8
|
import { OPENCODE_HEADERS } from "@oh-my-pi/pi-catalog/wire/github-copilot";
|
|
9
|
+
import * as AIError from "../error";
|
|
9
10
|
import type {
|
|
10
11
|
UsageAmount,
|
|
11
12
|
UsageFetchContext,
|
|
@@ -142,7 +143,7 @@ async function fetchJson(ctx: UsageFetchContext, url: string, init: RequestInit)
|
|
|
142
143
|
const response = await ctx.fetch(url, init);
|
|
143
144
|
if (!response.ok) {
|
|
144
145
|
const text = await response.text();
|
|
145
|
-
throw new
|
|
146
|
+
throw new AIError.ProviderHttpError(`${response.status} ${response.statusText}: ${text}`, response.status);
|
|
146
147
|
}
|
|
147
148
|
return response.json();
|
|
148
149
|
}
|
|
@@ -182,7 +183,7 @@ async function fetchInternalUsage(
|
|
|
182
183
|
...OPENCODE_HEADERS,
|
|
183
184
|
};
|
|
184
185
|
const data = await fetchJson(ctx, `${githubApiBaseUrl}/copilot_internal/user`, { headers, signal });
|
|
185
|
-
if (!isRecord(data)) throw new
|
|
186
|
+
if (!isRecord(data)) throw new AIError.ProviderHttpError("Invalid Copilot usage response", 200);
|
|
186
187
|
return data as CopilotUsageResponse;
|
|
187
188
|
}
|
|
188
189
|
|
|
@@ -206,7 +207,7 @@ async function fetchBillingUsage(
|
|
|
206
207
|
},
|
|
207
208
|
);
|
|
208
209
|
|
|
209
|
-
if (!isRecord(data)) throw new
|
|
210
|
+
if (!isRecord(data)) throw new AIError.ProviderHttpError("Invalid Copilot billing usage response", 200);
|
|
210
211
|
return data as BillingUsageResponse;
|
|
211
212
|
}
|
|
212
213
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getAntigravityUserAgent } from "@oh-my-pi/pi-catalog/wire/gemini-headers";
|
|
2
|
+
import * as AIError from "../error";
|
|
2
3
|
import type {
|
|
3
4
|
CredentialRankingContext,
|
|
4
5
|
CredentialRankingStrategy,
|
|
@@ -181,7 +182,7 @@ async function fetchAntigravityUsage(params: UsageFetchParams, ctx: UsageFetchCo
|
|
|
181
182
|
break;
|
|
182
183
|
}
|
|
183
184
|
|
|
184
|
-
if (
|
|
185
|
+
if (AIError.isTransientStatus(response.status)) {
|
|
185
186
|
continue;
|
|
186
187
|
}
|
|
187
188
|
break;
|
package/src/utils/abort.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as AIError from "../error";
|
|
2
|
+
|
|
1
3
|
export interface AbortSourceTracker {
|
|
2
4
|
requestAbortController: AbortController;
|
|
3
5
|
requestSignal: AbortSignal;
|
|
@@ -57,9 +59,9 @@ export function createAbortSourceTracker(callerSignal?: AbortSignal): AbortSourc
|
|
|
57
59
|
*/
|
|
58
60
|
export function raceWithSignal<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T> {
|
|
59
61
|
if (!signal) return promise;
|
|
60
|
-
if (signal.aborted) return Promise.reject(signal.reason ?? new
|
|
62
|
+
if (signal.aborted) return Promise.reject(signal.reason ?? new AIError.AbortError());
|
|
61
63
|
const { promise: aborted, reject } = Promise.withResolvers<never>();
|
|
62
|
-
const onAbort = () => reject(signal.reason ?? new
|
|
64
|
+
const onAbort = () => reject(signal.reason ?? new AIError.AbortError());
|
|
63
65
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
64
66
|
return Promise.race([promise, aborted]).finally(() => signal.removeEventListener("abort", onAbort));
|
|
65
67
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Stores streamed tool-call argument JSON for live renderers and parser recovery. */
|
|
2
|
+
export const kStreamingPartialJson = Symbol("provider.block.partialJson");
|
|
3
|
+
|
|
4
|
+
/** Carries streamed tool-call argument JSON without exposing a string-keyed property. */
|
|
5
|
+
export type StreamingPartialJsonCarrier = object & { [kStreamingPartialJson]?: string };
|
|
6
|
+
|
|
7
|
+
/** Reads streamed tool-call argument JSON from a block or event snapshot. */
|
|
8
|
+
export function getStreamingPartialJson(block: StreamingPartialJsonCarrier | null | undefined): string | undefined {
|
|
9
|
+
return block?.[kStreamingPartialJson];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Writes streamed tool-call argument JSON to a block or clears it with `undefined`. */
|
|
13
|
+
export function setStreamingPartialJson(block: StreamingPartialJsonCarrier, value: string | undefined): void {
|
|
14
|
+
block[kStreamingPartialJson] = value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Clears streamed tool-call argument JSON without deleting or changing object shape. */
|
|
18
|
+
export function clearStreamingPartialJson(block: StreamingPartialJsonCarrier): void {
|
|
19
|
+
if (Object.hasOwn(block, kStreamingPartialJson)) block[kStreamingPartialJson] = undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Stores a provider-local stream block index without exposing a string-keyed property. */
|
|
23
|
+
export const kStreamingBlockIndex = Symbol("provider.block.index");
|
|
24
|
+
|
|
25
|
+
/** Stores the last parsed argument prefix length for throttled streaming JSON parsing. */
|
|
26
|
+
export const kStreamingLastParseLen = Symbol("provider.block.lastParseLen");
|
|
27
|
+
|
|
28
|
+
/** Marks streamed tool-call arguments that already received an authoritative done payload. */
|
|
29
|
+
export const kStreamingArgumentsDone = Symbol("provider.block.argumentsDone");
|
|
30
|
+
|
|
31
|
+
/** Classifies Cursor's in-flight tool-call kind without leaking provider-private state. */
|
|
32
|
+
export const kStreamingBlockKind = Symbol("provider.block.kind");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as AIError from "../error";
|
|
1
2
|
import type { AssistantMessage, AssistantMessageEvent } from "../types";
|
|
2
3
|
|
|
3
4
|
// Generic event stream class for async iteration
|
|
@@ -63,7 +64,9 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
|
63
64
|
// end() without a terminal value must still settle result() —
|
|
64
65
|
// otherwise complete()/result() awaits hang forever.
|
|
65
66
|
this.resultSettled = true;
|
|
66
|
-
this.rejectFinalResult(
|
|
67
|
+
this.rejectFinalResult(
|
|
68
|
+
new AIError.ProviderResponseError("Stream ended without a final result", { kind: "envelope" }),
|
|
69
|
+
);
|
|
67
70
|
}
|
|
68
71
|
// Notify all waiting consumers that we're done
|
|
69
72
|
while (this.waiting.length > 0) {
|
|
@@ -125,7 +128,7 @@ export class AssistantMessageEventStream extends EventStream<AssistantMessageEve
|
|
|
125
128
|
} else if (event.type === "error") {
|
|
126
129
|
return event.error;
|
|
127
130
|
}
|
|
128
|
-
throw new
|
|
131
|
+
throw new AIError.ProviderResponseError("Unexpected event type for final result", { kind: "envelope" });
|
|
129
132
|
},
|
|
130
133
|
);
|
|
131
134
|
}
|
|
@@ -133,6 +136,10 @@ export class AssistantMessageEventStream extends EventStream<AssistantMessageEve
|
|
|
133
136
|
override push(event: AssistantMessageEvent): void {
|
|
134
137
|
if (this.done) return;
|
|
135
138
|
|
|
139
|
+
if (event.type === "error" && event.error.stopReason === "error") {
|
|
140
|
+
AIError.classifyMessage(event.error);
|
|
141
|
+
}
|
|
142
|
+
|
|
136
143
|
// Completion resolves the final result and still emits the terminal event.
|
|
137
144
|
if (this.isComplete(event)) {
|
|
138
145
|
this.done = true;
|
|
@@ -146,13 +153,18 @@ export class AssistantMessageEventStream extends EventStream<AssistantMessageEve
|
|
|
146
153
|
override end(result?: AssistantMessage): void {
|
|
147
154
|
this.done = true;
|
|
148
155
|
if (result !== undefined) {
|
|
156
|
+
if (result.stopReason === "error") {
|
|
157
|
+
AIError.classifyMessage(result);
|
|
158
|
+
}
|
|
149
159
|
this.resultSettled = true;
|
|
150
160
|
this.resolveFinalResult(result);
|
|
151
161
|
} else if (!this.resultSettled) {
|
|
152
162
|
// Mirror the base class: a result-less end() must not leave
|
|
153
163
|
// result() pending forever.
|
|
154
164
|
this.resultSettled = true;
|
|
155
|
-
this.rejectFinalResult(
|
|
165
|
+
this.rejectFinalResult(
|
|
166
|
+
new AIError.ProviderResponseError("Stream ended without a final result", { kind: "envelope" }),
|
|
167
|
+
);
|
|
156
168
|
}
|
|
157
169
|
this.endWaiting();
|
|
158
170
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { getLogsDir, isBunTestRuntime } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import * as AIError from "../error/flags";
|
|
3
4
|
import { isCopilotTransientModelError } from "./retry.js";
|
|
4
5
|
import { formatErrorMessageWithRetryAfter } from "./retry-after.js";
|
|
5
6
|
|
|
@@ -28,8 +29,7 @@ export async function appendRawHttpRequestDumpFor400(
|
|
|
28
29
|
dump: RawHttpRequestDump | undefined,
|
|
29
30
|
): Promise<string> {
|
|
30
31
|
// Never persist dumps under the test runner: providers exercise the 400 path
|
|
31
|
-
|
|
32
|
-
if (!dump || isBunTestRuntime() || extractHttpStatusFromError(error) !== 400) {
|
|
32
|
+
if (!dump || isBunTestRuntime() || AIError.status(error) !== 400) {
|
|
33
33
|
return message;
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -77,7 +77,7 @@ export async function finalizeErrorMessage(
|
|
|
77
77
|
*/
|
|
78
78
|
export function rewriteCopilotError(errorMessage: string, error: unknown, provider: string): string {
|
|
79
79
|
if (provider !== "github-copilot") return errorMessage;
|
|
80
|
-
const status =
|
|
80
|
+
const status = AIError.status(error);
|
|
81
81
|
if (status === 401) {
|
|
82
82
|
return `GitHub Copilot authentication failed (HTTP 401). Your token may have been revoked. Please re-login with /login github-copilot`;
|
|
83
83
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { $env } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import * as AIError from "../error";
|
|
2
3
|
|
|
3
4
|
const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 120_000;
|
|
4
5
|
const DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS = 100_000;
|
|
@@ -292,7 +293,7 @@ export async function* iterateWithIdleTimeout<T>(
|
|
|
292
293
|
if (activeTimeoutMs <= 0) {
|
|
293
294
|
options.onFirstItemTimeout?.();
|
|
294
295
|
closeIterator();
|
|
295
|
-
throw new
|
|
296
|
+
throw new AIError.StreamTimeoutError(options.firstItemErrorMessage ?? options.errorMessage);
|
|
296
297
|
}
|
|
297
298
|
}
|
|
298
299
|
} else if (options.idleTimeoutMs !== undefined && options.idleTimeoutMs > 0) {
|
|
@@ -300,7 +301,7 @@ export async function* iterateWithIdleTimeout<T>(
|
|
|
300
301
|
if (activeTimeoutMs <= 0) {
|
|
301
302
|
options.onIdle?.();
|
|
302
303
|
closeIterator();
|
|
303
|
-
throw new
|
|
304
|
+
throw new AIError.StreamTimeoutError(options.errorMessage);
|
|
304
305
|
}
|
|
305
306
|
}
|
|
306
307
|
|
|
@@ -343,7 +344,7 @@ export async function* iterateWithIdleTimeout<T>(
|
|
|
343
344
|
options.onFirstItemTimeout?.();
|
|
344
345
|
}
|
|
345
346
|
closeIterator();
|
|
346
|
-
throw new
|
|
347
|
+
throw new AIError.StreamTimeoutError(
|
|
347
348
|
!awaitingFirstItem ? options.errorMessage : (options.firstItemErrorMessage ?? options.errorMessage),
|
|
348
349
|
);
|
|
349
350
|
}
|
|
@@ -467,6 +468,6 @@ export async function* iterateWithTerminalGrace<T>(
|
|
|
467
468
|
function abortReason(signal: AbortSignal): Error {
|
|
468
469
|
const reason = signal.reason;
|
|
469
470
|
if (reason instanceof Error) return reason;
|
|
470
|
-
if (typeof reason === "string") return new
|
|
471
|
-
return new
|
|
471
|
+
if (typeof reason === "string") return new AIError.AbortError(reason);
|
|
472
|
+
return new AIError.AbortError();
|
|
472
473
|
}
|