@oh-my-pi/pi-ai 18.2.6 → 18.2.7
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 +13 -1
- package/THIRD-PARTY-NOTICES.txt +0 -37
- package/dist/types/auth-retry.d.ts +2 -0
- package/dist/types/auth-storage.d.ts +2 -2
- package/dist/types/error/classes.d.ts +4 -1
- package/dist/types/error/flags.d.ts +12 -4
- package/dist/types/index.d.ts +14 -12
- package/dist/types/judgment/types.d.ts +5 -2
- package/dist/types/judgment/typesafe.d.ts +16 -22
- package/dist/types/providers/anthropic-compaction.d.ts +10 -0
- package/dist/types/providers/anthropic-identity.d.ts +18 -0
- package/dist/types/providers/anthropic-state.d.ts +13 -0
- package/dist/types/providers/anthropic.d.ts +6 -59
- package/dist/types/providers/gitlab-duo.d.ts +0 -1
- package/dist/types/providers/kimi.d.ts +1 -5
- package/dist/types/providers/openai-codex-attestation.d.ts +6 -0
- package/dist/types/providers/openai-codex-compaction.d.ts +6 -0
- package/dist/types/providers/openai-codex-responses.d.ts +3 -27
- package/dist/types/providers/openai-codex-transport.d.ts +7 -0
- package/dist/types/providers/openai-shared.d.ts +0 -9
- package/dist/types/providers/register-builtins.d.ts +20 -22
- package/dist/types/providers/synthetic.d.ts +1 -5
- package/package.json +6 -6
- package/src/auth-retry.ts +3 -0
- package/src/auth-storage.ts +28 -22
- package/src/error/auth-classify.ts +10 -2
- package/src/error/classes.ts +23 -3
- package/src/error/flags.ts +51 -16
- package/src/index.ts +15 -12
- package/src/judgment/types.ts +6 -3
- package/src/judgment/typesafe.ts +53 -48
- package/src/provider-session-state.ts +1 -1
- package/src/providers/anthropic-compaction.ts +54 -0
- package/src/providers/anthropic-identity.ts +136 -0
- package/src/providers/anthropic-state.ts +55 -0
- package/src/providers/anthropic.ts +39 -296
- package/src/providers/bedrock-mantle.ts +1 -1
- package/src/providers/gitlab-duo.ts +0 -4
- package/src/providers/kimi.ts +1 -8
- package/src/providers/openai-codex-attestation.ts +19 -0
- package/src/providers/openai-codex-compaction.ts +18 -0
- package/src/providers/openai-codex-responses.ts +9 -68
- package/src/providers/openai-codex-transport.ts +18 -0
- package/src/providers/openai-shared.ts +1 -10
- package/src/providers/register-builtins.ts +113 -320
- package/src/providers/synthetic.ts +1 -8
- package/src/registry/cloudflare-ai-gateway.ts +1 -1
- package/src/stream.ts +7 -15
- package/src/utils/anthropic-auth.ts +3 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "18.2.
|
|
3
|
+
"version": "18.2.7",
|
|
4
4
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -128,11 +128,11 @@
|
|
|
128
128
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
129
129
|
},
|
|
130
130
|
"dependencies": {
|
|
131
|
-
"@oh-my-pi/omptype": "18.2.
|
|
132
|
-
"@oh-my-pi/pi-catalog": "18.2.
|
|
133
|
-
"@oh-my-pi/pi-natives": "18.2.
|
|
134
|
-
"@oh-my-pi/pi-utils": "18.2.
|
|
135
|
-
"@oh-my-pi/pi-wire": "18.2.
|
|
131
|
+
"@oh-my-pi/omptype": "18.2.7",
|
|
132
|
+
"@oh-my-pi/pi-catalog": "18.2.7",
|
|
133
|
+
"@oh-my-pi/pi-natives": "18.2.7",
|
|
134
|
+
"@oh-my-pi/pi-utils": "18.2.7",
|
|
135
|
+
"@oh-my-pi/pi-wire": "18.2.7"
|
|
136
136
|
},
|
|
137
137
|
"devDependencies": {
|
|
138
138
|
"@types/bun": "^1.3.14"
|
package/src/auth-retry.ts
CHANGED
|
@@ -43,6 +43,9 @@ export type ApiKeyResolver = (ctx: ApiKeyResolveContext) => Promise<string | und
|
|
|
43
43
|
/** A static bearer string, or a {@link ApiKeyResolver} that mints/rotates one. */
|
|
44
44
|
export type ApiKey = string | ApiKeyResolver;
|
|
45
45
|
|
|
46
|
+
/** Keyless-provider credential marker; transports must not send it in authentication headers. */
|
|
47
|
+
export const NO_AUTH_SENTINEL = "N/A";
|
|
48
|
+
|
|
46
49
|
/** Narrows {@link ApiKey} to its resolver form. */
|
|
47
50
|
export function isApiKeyResolver(key: ApiKey | undefined): key is ApiKeyResolver {
|
|
48
51
|
return typeof key === "function";
|
package/src/auth-storage.ts
CHANGED
|
@@ -4707,7 +4707,7 @@ export class AuthStorage {
|
|
|
4707
4707
|
async #resolveCredentialTarget(
|
|
4708
4708
|
provider: string,
|
|
4709
4709
|
sessionId: string | undefined,
|
|
4710
|
-
options?: { credentialId?: number; apiKey?: string },
|
|
4710
|
+
options?: { credentialId?: number; apiKey?: string; allowStaleOAuthBearer?: boolean },
|
|
4711
4711
|
): Promise<{ type: AuthCredential["type"]; index: number; explicit: boolean } | undefined> {
|
|
4712
4712
|
const explicit = options?.credentialId !== undefined || options?.apiKey !== undefined;
|
|
4713
4713
|
if (explicit) {
|
|
@@ -4731,6 +4731,15 @@ export class AuthStorage {
|
|
|
4731
4731
|
return { type: entry.credential.type, index, explicit: true };
|
|
4732
4732
|
}
|
|
4733
4733
|
}
|
|
4734
|
+
// Quota and account policy survive token refresh; hard auth failures do not.
|
|
4735
|
+
if (options.allowStaleOAuthBearer && options.credentialId === undefined) {
|
|
4736
|
+
const credentialId = this.#findOAuthCredentialIdForBearer(provider, options.apiKey);
|
|
4737
|
+
const index =
|
|
4738
|
+
credentialId === undefined
|
|
4739
|
+
? -1
|
|
4740
|
+
: stored.findIndex(entry => entry.id === credentialId && entry.credential.type === "oauth");
|
|
4741
|
+
if (index >= 0) return { type: "oauth", index, explicit: true };
|
|
4742
|
+
}
|
|
4734
4743
|
}
|
|
4735
4744
|
if (explicit) return undefined;
|
|
4736
4745
|
const sessionCredential = this.#getSessionCredential(provider, sessionId);
|
|
@@ -4866,23 +4875,11 @@ export class AuthStorage {
|
|
|
4866
4875
|
},
|
|
4867
4876
|
): Promise<UsageLimitMarkResult> {
|
|
4868
4877
|
await this.#adoptExternalCredentialChanges();
|
|
4869
|
-
|
|
4878
|
+
const sessionCredential = await this.#resolveCredentialTarget(provider, sessionId, {
|
|
4870
4879
|
credentialId: options?.credentialId,
|
|
4871
4880
|
apiKey: options?.apiKey,
|
|
4881
|
+
allowStaleOAuthBearer: true,
|
|
4872
4882
|
});
|
|
4873
|
-
if (!sessionCredential && options?.credentialId === undefined && options?.apiKey !== undefined) {
|
|
4874
|
-
// Account quota survives OAuth bearer rotation. Attribute a delayed
|
|
4875
|
-
// usage-limit response through the durable row id captured when this
|
|
4876
|
-
// exact bearer was resolved; never use this alias for hard auth errors.
|
|
4877
|
-
const credentialId = this.#findOAuthCredentialIdForBearer(provider, options.apiKey);
|
|
4878
|
-
const index =
|
|
4879
|
-
credentialId === undefined
|
|
4880
|
-
? -1
|
|
4881
|
-
: this.#getStoredCredentials(provider).findIndex(
|
|
4882
|
-
entry => entry.id === credentialId && entry.credential.type === "oauth",
|
|
4883
|
-
);
|
|
4884
|
-
if (index >= 0) sessionCredential = { type: "oauth", index, explicit: true };
|
|
4885
|
-
}
|
|
4886
4883
|
if (!sessionCredential) return { switched: false };
|
|
4887
4884
|
const target = this.#getStoredCredentials(provider)[sessionCredential.index];
|
|
4888
4885
|
if (!target || target.credential.type !== sessionCredential.type) return { switched: false };
|
|
@@ -6974,8 +6971,8 @@ export class AuthStorage {
|
|
|
6974
6971
|
* stale session stickiness. Fall back to the session-sticky credential only
|
|
6975
6972
|
* when neither explicit target is available. For hard-auth errors, an explicit
|
|
6976
6973
|
* target that no longer matches storage returns `false` without mutation.
|
|
6977
|
-
* Delayed usage-limit errors may instead recover the durable
|
|
6978
|
-
* the bearer fingerprint recorded when the request resolved.
|
|
6974
|
+
* Delayed usage-limit and account-policy errors may instead recover the durable
|
|
6975
|
+
* OAuth row from the bearer fingerprint recorded when the request resolved.
|
|
6979
6976
|
*
|
|
6980
6977
|
* - usage-limit / account-rate-limit error → {@link AuthStorage.markUsageLimitReached}
|
|
6981
6978
|
* (temporary block via its own backoff — default plus server usage-report
|
|
@@ -7024,16 +7021,16 @@ export class AuthStorage {
|
|
|
7024
7021
|
).switched;
|
|
7025
7022
|
}
|
|
7026
7023
|
|
|
7024
|
+
const deniedModel = AIError.codexChatGPTAccountPolicyModel(error);
|
|
7025
|
+
const exactCodexModelPolicy =
|
|
7026
|
+
deniedModel !== undefined && AIError.isCodexChatGPTAccountPolicyError(error, provider, options?.modelId);
|
|
7027
|
+
const exactModelPolicy = exactCodexModelPolicy || exactCursorModelPolicy;
|
|
7027
7028
|
const sessionCredential = await this.#resolveCredentialTarget(provider, sessionId, {
|
|
7028
7029
|
credentialId: options?.credentialId,
|
|
7029
7030
|
apiKey: options?.apiKey,
|
|
7031
|
+
allowStaleOAuthBearer: accountPolicy || exactModelPolicy,
|
|
7030
7032
|
});
|
|
7031
7033
|
if (!sessionCredential) return false;
|
|
7032
|
-
|
|
7033
|
-
const deniedModel = AIError.codexChatGPTAccountPolicyModel(error);
|
|
7034
|
-
const exactCodexModelPolicy =
|
|
7035
|
-
deniedModel !== undefined && AIError.isCodexChatGPTAccountPolicyError(error, provider, options?.modelId);
|
|
7036
|
-
const exactModelPolicy = exactCodexModelPolicy || exactCursorModelPolicy;
|
|
7037
7034
|
// The exact sentence is provider-controlled input. A non-Codex provider,
|
|
7038
7035
|
// absent request model, or mismatched model must not turn it into either a
|
|
7039
7036
|
// global block or a hard-auth invalidation.
|
|
@@ -7049,6 +7046,15 @@ export class AuthStorage {
|
|
|
7049
7046
|
options?.modelId,
|
|
7050
7047
|
modelPolicyScope,
|
|
7051
7048
|
);
|
|
7049
|
+
// Account-wide denials must not inherit a quota scope that healthy usage can heal.
|
|
7050
|
+
routing.blockScope = modelPolicyScope;
|
|
7051
|
+
const sticky = this.#getSessionCredential(provider, sessionId);
|
|
7052
|
+
if (
|
|
7053
|
+
!sessionCredential.explicit ||
|
|
7054
|
+
(sticky?.type === sessionCredential.type && sticky.index === sessionCredential.index)
|
|
7055
|
+
) {
|
|
7056
|
+
this.#clearSessionCredential(provider, sessionId);
|
|
7057
|
+
}
|
|
7052
7058
|
return this.#blockCredentialForRotation(
|
|
7053
7059
|
provider,
|
|
7054
7060
|
sessionCredential.type,
|
|
@@ -41,8 +41,16 @@ export function isAuthRetryableError(error: unknown): boolean {
|
|
|
41
41
|
if (isUsageLimit(error)) return true;
|
|
42
42
|
if (isAccountPolicyError(error)) return true;
|
|
43
43
|
if (isInvalidatedOAuthTokenError(error)) return true;
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
let httpStatus = extractHttpStatusFromError(error);
|
|
45
|
+
let message = error instanceof Error ? error.message : typeof error === "string" ? error : undefined;
|
|
46
|
+
if (typeof error === "object" && error !== null) {
|
|
47
|
+
if (httpStatus === undefined && "errorStatus" in error && typeof error.errorStatus === "number") {
|
|
48
|
+
httpStatus = error.errorStatus;
|
|
49
|
+
}
|
|
50
|
+
if (message === undefined && "errorMessage" in error && typeof error.errorMessage === "string") {
|
|
51
|
+
message = error.errorMessage;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
46
54
|
const embeddedStatus = message ? extractHttpStatusFromError({ message }) : undefined;
|
|
47
55
|
const status = httpStatus ?? embeddedStatus;
|
|
48
56
|
if (isConcurrencyCapExclusion(status, message)) return false;
|
package/src/error/classes.ts
CHANGED
|
@@ -90,8 +90,8 @@ export class AnthropicApiError extends ProviderHttpError {
|
|
|
90
90
|
declare readonly headers: Headers;
|
|
91
91
|
readonly requestId: string | null;
|
|
92
92
|
|
|
93
|
-
constructor(status: number, message: string, headers: Headers) {
|
|
94
|
-
super(message, status, { headers });
|
|
93
|
+
constructor(status: number, message: string, headers: Headers, options?: { code?: string; cause?: unknown }) {
|
|
94
|
+
super(message, status, { headers, code: options?.code, cause: options?.cause });
|
|
95
95
|
this.name = "AnthropicApiError";
|
|
96
96
|
this.requestId = headers.get("request-id");
|
|
97
97
|
}
|
|
@@ -176,7 +176,27 @@ export class AnthropicApiError extends ProviderHttpError {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
const detail = bodyChunks.join("").trim() || "status code (no body)";
|
|
179
|
-
|
|
179
|
+
let code: string | undefined;
|
|
180
|
+
try {
|
|
181
|
+
const parsed: unknown = JSON.parse(detail);
|
|
182
|
+
if (parsed && typeof parsed === "object" && "error" in parsed) {
|
|
183
|
+
const err = parsed.error;
|
|
184
|
+
if (err && typeof err === "object") {
|
|
185
|
+
if (
|
|
186
|
+
"details" in err &&
|
|
187
|
+
err.details &&
|
|
188
|
+
typeof err.details === "object" &&
|
|
189
|
+
"error_code" in err.details &&
|
|
190
|
+
typeof err.details.error_code === "string"
|
|
191
|
+
) {
|
|
192
|
+
code = err.details.error_code;
|
|
193
|
+
} else if ("type" in err && typeof err.type === "string") {
|
|
194
|
+
code = err.type;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} catch {}
|
|
199
|
+
return new AnthropicApiError(response.status, `${response.status} ${detail}`, response.headers, { code });
|
|
180
200
|
}
|
|
181
201
|
}
|
|
182
202
|
|
package/src/error/flags.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isUnexpectedSocketCloseMessage } from "@oh-my-pi/pi-utils/fetch-retry";
|
|
2
|
-
import type { Api, AssistantMessage } from "../types";
|
|
2
|
+
import type { Api, AssistantMessage, Usage } from "../types";
|
|
3
3
|
import { AwsCredentialsError } from "./aws";
|
|
4
4
|
import {
|
|
5
5
|
AnthropicConnectionError,
|
|
@@ -194,6 +194,17 @@ const PROVIDER_FINISH_ERROR_PATTERN = /\bProvider (?:returned error finish_reaso
|
|
|
194
194
|
const EMPTY_RESPONSE_PATTERN = /\bthought-only response without final output\b/i;
|
|
195
195
|
const CONTENT_FILTER_PATTERN = /\b(?:incomplete:\s*)?content_filter\b/i;
|
|
196
196
|
const ACCOUNT_POLICY_PATTERN = /\bcyber_policy\b|trusted access for cyber/i;
|
|
197
|
+
export const ANTHROPIC_ACCOUNT_POLICY_PATTERN =
|
|
198
|
+
/\b(?:oauth_not_allowed_for_organization|permission_error)\b|\bOAuth authentication is currently not allowed for this organization\b/i;
|
|
199
|
+
|
|
200
|
+
/** Whether an error message represents an Anthropic account-scoped permission/policy denial. */
|
|
201
|
+
export function isAnthropicAccountPolicyText(text: string, provider?: string, statusArg?: number): boolean {
|
|
202
|
+
if (provider !== undefined && provider !== "anthropic") return false;
|
|
203
|
+
const statusCandidate = statusArg ?? (text ? status({ message: text }) : undefined);
|
|
204
|
+
if (statusCandidate !== undefined && statusCandidate !== 403) return false;
|
|
205
|
+
return ANTHROPIC_ACCOUNT_POLICY_PATTERN.test(text);
|
|
206
|
+
}
|
|
207
|
+
|
|
197
208
|
const CODEX_CHATGPT_ACCOUNT_MODEL_POLICY_PATTERN =
|
|
198
209
|
/\bThe ['"]([^'"\r\n]+)['"] model is not supported when using Codex with a ChatGPT account\./i;
|
|
199
210
|
const CODEX_CHATGPT_ACCOUNT_MODEL_MAX_LENGTH = 256;
|
|
@@ -381,6 +392,9 @@ function statusInternal(error: unknown, depth: number): number | undefined {
|
|
|
381
392
|
if (typeof errObj.statusCode === "number" && errObj.statusCode >= 100 && errObj.statusCode <= 599) {
|
|
382
393
|
return errObj.statusCode;
|
|
383
394
|
}
|
|
395
|
+
if (typeof errObj.errorStatus === "number" && errObj.errorStatus >= 100 && errObj.errorStatus <= 599) {
|
|
396
|
+
return errObj.errorStatus;
|
|
397
|
+
}
|
|
384
398
|
if (typeof errObj.response === "object" && errObj.response !== null) {
|
|
385
399
|
const resp = errObj.response as Record<string, unknown>;
|
|
386
400
|
if (typeof resp.status === "number" && resp.status >= 100 && resp.status <= 599) {
|
|
@@ -490,6 +504,8 @@ function classifyText(
|
|
|
490
504
|
if (isProviderFinishErrorText(errorMessage)) kinds |= Flag.ProviderFinishError;
|
|
491
505
|
if (EMPTY_RESPONSE_PATTERN.test(errorMessage)) kinds |= Flag.EmptyResponse | Flag.Transient;
|
|
492
506
|
if (isContentBlockedText(errorMessage)) kinds |= Flag.ContentBlocked;
|
|
507
|
+
const statusClean = errorStatus ? errorStatus : (status({ message: errorMessage }) ?? undefined);
|
|
508
|
+
|
|
493
509
|
if (
|
|
494
510
|
ACCOUNT_POLICY_PATTERN.test(errorMessage) ||
|
|
495
511
|
isCodexChatGPTAccountPolicyText(errorMessage, provider, modelId) ||
|
|
@@ -497,9 +513,10 @@ function classifyText(
|
|
|
497
513
|
) {
|
|
498
514
|
kinds |= Flag.AccountPolicy | Flag.ContentBlocked;
|
|
499
515
|
}
|
|
516
|
+
if (isAnthropicAccountPolicyText(errorMessage, provider, statusClean)) {
|
|
517
|
+
kinds |= Flag.AccountPolicy;
|
|
518
|
+
}
|
|
500
519
|
if (isAuthFailureText(errorMessage)) kinds |= Flag.AuthFailed;
|
|
501
|
-
|
|
502
|
-
const statusClean = errorStatus ? errorStatus : (status({ message: errorMessage }) ?? undefined);
|
|
503
520
|
const cleanMessage = errorMessage;
|
|
504
521
|
const isOpaque = isOpaqueStatusBody(cleanMessage);
|
|
505
522
|
|
|
@@ -597,8 +614,12 @@ export function classify(error: unknown, api?: Api): number {
|
|
|
597
614
|
if ("errorId" in link && typeof (link as { errorId: unknown }).errorId === "number") {
|
|
598
615
|
kinds |= (link as { errorId: number }).errorId & KIND_MASK;
|
|
599
616
|
}
|
|
600
|
-
if ("code" in link && typeof link.code === "string"
|
|
601
|
-
|
|
617
|
+
if ("code" in link && typeof link.code === "string") {
|
|
618
|
+
if (ACCOUNT_POLICY_PATTERN.test(link.code)) {
|
|
619
|
+
kinds |= Flag.AccountPolicy | Flag.ContentBlocked;
|
|
620
|
+
} else if (ANTHROPIC_ACCOUNT_POLICY_PATTERN.test(link.code)) {
|
|
621
|
+
kinds |= Flag.AccountPolicy;
|
|
622
|
+
}
|
|
602
623
|
}
|
|
603
624
|
}
|
|
604
625
|
|
|
@@ -635,6 +656,13 @@ export function classify(error: unknown, api?: Api): number {
|
|
|
635
656
|
if (code === "overloaded_error" || code === "rate_limit_error") {
|
|
636
657
|
linkKinds |= Flag.Transient;
|
|
637
658
|
}
|
|
659
|
+
if (
|
|
660
|
+
code === "oauth_not_allowed_for_organization" ||
|
|
661
|
+
code === "permission_error" ||
|
|
662
|
+
(codeStatus === 403 && ANTHROPIC_ACCOUNT_POLICY_PATTERN.test(link.message))
|
|
663
|
+
) {
|
|
664
|
+
linkKinds |= Flag.AccountPolicy;
|
|
665
|
+
}
|
|
638
666
|
if (
|
|
639
667
|
(codeStatus === 401 || codeStatus === 403) &&
|
|
640
668
|
!(codeStatus === 403 && parseRateLimitReason(link.message) === "CONCURRENT_LIMIT")
|
|
@@ -655,12 +683,12 @@ export function classify(error: unknown, api?: Api): number {
|
|
|
655
683
|
linkMessage = link.message;
|
|
656
684
|
} else if (typeof link === "string") {
|
|
657
685
|
linkMessage = link;
|
|
658
|
-
} else if (
|
|
659
|
-
typeof link === "
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
686
|
+
} else if (typeof link === "object") {
|
|
687
|
+
if ("message" in link && typeof link.message === "string") {
|
|
688
|
+
linkMessage = link.message;
|
|
689
|
+
} else if ("errorMessage" in link && typeof link.errorMessage === "string") {
|
|
690
|
+
linkMessage = link.errorMessage;
|
|
691
|
+
}
|
|
664
692
|
}
|
|
665
693
|
|
|
666
694
|
const linkStatus = status(link);
|
|
@@ -834,14 +862,21 @@ export function attach<E extends object>(error: E, id: number): E {
|
|
|
834
862
|
return error;
|
|
835
863
|
}
|
|
836
864
|
|
|
865
|
+
/** Overflow-classification evidence, including errors received before token usage is available. */
|
|
866
|
+
export interface ContextOverflowMessage extends Pick<AssistantMessage, "errorId" | "stopReason" | "errorMessage"> {
|
|
867
|
+
readonly usage?: Pick<Usage, "input" | "cacheRead" | "cacheWrite">;
|
|
868
|
+
}
|
|
869
|
+
|
|
837
870
|
/** Provider-reported usage proves context-window excess — authoritative, compaction-owned (#9235). */
|
|
838
|
-
export function isUsageBackedContextOverflow(message:
|
|
839
|
-
|
|
840
|
-
|
|
871
|
+
export function isUsageBackedContextOverflow(message: ContextOverflowMessage, contextWindow?: number): boolean {
|
|
872
|
+
const usage = message.usage;
|
|
873
|
+
if (!contextWindow || !usage) return false;
|
|
874
|
+
const inputTokens = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
841
875
|
return inputTokens > contextWindow;
|
|
842
876
|
}
|
|
843
877
|
|
|
844
|
-
|
|
878
|
+
/** Classify overflow from error flags, available token usage, or provider error text. */
|
|
879
|
+
export function isContextOverflow(message: ContextOverflowMessage, contextWindow?: number): boolean {
|
|
845
880
|
if (is(message.errorId, Flag.ContextOverflow)) return true;
|
|
846
881
|
if (isUsageBackedContextOverflow(message, contextWindow)) return true;
|
|
847
882
|
return message.stopReason === "error" && !!message.errorMessage && matchesOverflowText(message.errorMessage);
|
|
@@ -861,7 +896,7 @@ export function isPayloadRejection(message: AssistantMessage): boolean {
|
|
|
861
896
|
* Usage-backed overflows are authoritative window excesses and never ambiguous. */
|
|
862
897
|
export function isTextAmbiguousContextOverflow(
|
|
863
898
|
errorId: number,
|
|
864
|
-
message:
|
|
899
|
+
message: ContextOverflowMessage | undefined,
|
|
865
900
|
contextWindow?: number,
|
|
866
901
|
): boolean {
|
|
867
902
|
const overflowFlagged =
|
package/src/index.ts
CHANGED
|
@@ -10,22 +10,25 @@ export * from "./judgment";
|
|
|
10
10
|
export * from "./oneshot-retry";
|
|
11
11
|
export * from "./provider-details";
|
|
12
12
|
export * from "./provider-session-state";
|
|
13
|
-
|
|
14
|
-
export * from "./providers/anthropic
|
|
15
|
-
export * from "./providers/
|
|
13
|
+
// Provider-specific runtime APIs are imported from their providers/* subpaths.
|
|
14
|
+
export type * from "./providers/anthropic";
|
|
15
|
+
export * from "./providers/anthropic-identity";
|
|
16
|
+
export * from "./providers/anthropic-state";
|
|
17
|
+
export type * from "./providers/anthropic-client";
|
|
18
|
+
export type * from "./providers/azure-openai-responses";
|
|
16
19
|
export type * from "./providers/cursor";
|
|
17
|
-
export * from "./providers/gitlab-duo";
|
|
18
|
-
export * from "./providers/gitlab-duo-workflow";
|
|
20
|
+
export type * from "./providers/gitlab-duo";
|
|
21
|
+
export type * from "./providers/gitlab-duo-workflow";
|
|
19
22
|
export type * from "./providers/google";
|
|
20
23
|
export type * from "./providers/google-gemini-cli";
|
|
21
24
|
export type * from "./providers/google-vertex";
|
|
22
|
-
export * from "./providers/kimi";
|
|
23
|
-
export * from "./providers/mock";
|
|
24
|
-
export * from "./providers/ollama";
|
|
25
|
-
export * from "./providers/openai-codex-responses";
|
|
26
|
-
export * from "./providers/openai-completions";
|
|
27
|
-
export * from "./providers/openai-responses";
|
|
28
|
-
export * from "./providers/synthetic";
|
|
25
|
+
export type * from "./providers/kimi";
|
|
26
|
+
export type * from "./providers/mock";
|
|
27
|
+
export type * from "./providers/ollama";
|
|
28
|
+
export type * from "./providers/openai-codex-responses";
|
|
29
|
+
export type * from "./providers/openai-completions";
|
|
30
|
+
export type * from "./providers/openai-responses";
|
|
31
|
+
export type * from "./providers/synthetic";
|
|
29
32
|
export * from "./registry";
|
|
30
33
|
export * from "./stream";
|
|
31
34
|
export * from "./types";
|
package/src/judgment/types.ts
CHANGED
|
@@ -115,14 +115,17 @@ export class JudgmentParseError extends Error {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
/**
|
|
119
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Usage from a backend that reports token counts and, optionally, one billed
|
|
120
|
+
* USD amount. Judgment pricing is input-only, so the amount lands on `input`.
|
|
121
|
+
*/
|
|
122
|
+
export function tokenUsage(input: number, output: number, cost = 0): Usage {
|
|
120
123
|
return {
|
|
121
124
|
input,
|
|
122
125
|
output,
|
|
123
126
|
cacheRead: 0,
|
|
124
127
|
cacheWrite: 0,
|
|
125
128
|
totalTokens: input + output,
|
|
126
|
-
cost: { input:
|
|
129
|
+
cost: { input: cost, output: 0, cacheRead: 0, cacheWrite: 0, total: cost },
|
|
127
130
|
};
|
|
128
131
|
}
|
package/src/judgment/typesafe.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TypeSafe System One client: the native {@link Judge} backend.
|
|
3
3
|
*
|
|
4
|
-
* Forwards a {@link JudgmentRequest} verbatim to
|
|
5
|
-
* the typed answers back.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Forwards a {@link JudgmentRequest} verbatim to the judgment route of its
|
|
5
|
+
* API ({@link JUDGMENT_ROUTES}) and maps the typed answers back. TypeSafe's
|
|
6
|
+
* own `POST /v1/systemone` and OpenRouter's `POST /api/alpha/decisions` share
|
|
7
|
+
* the request and answer wire shape, so one client serves both. Credentials
|
|
8
|
+
* flow through {@link withAuth}, so a stored key rotates on 401/403 exactly
|
|
9
|
+
* like chat providers; transient 429/5xx responses retry with bounded,
|
|
10
|
+
* `retry-after`-aware backoff.
|
|
8
11
|
*
|
|
9
12
|
* Environment (mirrors the official SDK): `TYPESAFE_API_KEY` is resolved by
|
|
10
13
|
* the auth registry (`rules/auth/typesafe.kdl`), `TYPESAFE_BASE_URL`
|
|
11
14
|
* overrides the API root, `TYPESAFE_DEFAULT_MODEL` the model.
|
|
12
15
|
*/
|
|
13
|
-
import
|
|
16
|
+
import { TYPESAFE_DEFAULT_BASE_URL } from "@oh-my-pi/pi-catalog/discovery";
|
|
17
|
+
import type { Api, FetchImpl } from "@oh-my-pi/pi-catalog/types";
|
|
14
18
|
import { $env } from "@oh-my-pi/pi-utils";
|
|
15
19
|
import { type ApiKey, withAuth } from "../auth-retry";
|
|
16
20
|
import * as AIError from "../error";
|
|
@@ -26,9 +30,22 @@ import {
|
|
|
26
30
|
} from "./types";
|
|
27
31
|
|
|
28
32
|
export const TYPESAFE_PROVIDER = "typesafe";
|
|
29
|
-
export const TYPESAFE_DEFAULT_BASE_URL = "https://api.typesafe.ai";
|
|
30
33
|
export const TYPESAFE_DEFAULT_MODEL = "jev-latest";
|
|
31
34
|
|
|
35
|
+
/** Judgment `POST` path under a model's base URL, per System One–compatible API. */
|
|
36
|
+
export const JUDGMENT_ROUTES = {
|
|
37
|
+
typesafe: "/v1/systemone",
|
|
38
|
+
"openrouter-decisions": "/decisions",
|
|
39
|
+
} as const satisfies Partial<Record<Api, string>>;
|
|
40
|
+
|
|
41
|
+
/** APIs {@link TypeSafeJudge} can serve. */
|
|
42
|
+
export type JudgmentApi = keyof typeof JUDGMENT_ROUTES;
|
|
43
|
+
|
|
44
|
+
/** Whether a catalog API answers System One judgments natively. */
|
|
45
|
+
export function isJudgmentApi(api: Api): api is JudgmentApi {
|
|
46
|
+
return Object.hasOwn(JUDGMENT_ROUTES, api);
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
/** `TYPESAFE_BASE_URL` when set, else the public API root; trailing slashes stripped. */
|
|
33
50
|
export function typesafeBaseUrl(): string {
|
|
34
51
|
return ($env.TYPESAFE_BASE_URL?.trim() || TYPESAFE_DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
@@ -41,6 +58,10 @@ export function typesafeModel(): string {
|
|
|
41
58
|
|
|
42
59
|
export interface TypeSafeJudgeOptions {
|
|
43
60
|
apiKey: ApiKey;
|
|
61
|
+
/** Wire route; defaults to TypeSafe's own API. */
|
|
62
|
+
api?: JudgmentApi;
|
|
63
|
+
/** Catalog provider reported on results; defaults to {@link TYPESAFE_PROVIDER}. */
|
|
64
|
+
provider?: string;
|
|
44
65
|
/** Defaults to {@link typesafeBaseUrl}. */
|
|
45
66
|
baseUrl?: string;
|
|
46
67
|
/** Defaults to {@link typesafeModel}. */
|
|
@@ -60,17 +81,11 @@ const MAX_ATTEMPTS = 3;
|
|
|
60
81
|
const BACKOFF_BASE_MS = 500;
|
|
61
82
|
const BACKOFF_MAX_MS = 5_000;
|
|
62
83
|
|
|
63
|
-
/** Wire shape of `GET /v1/models`. */
|
|
64
|
-
export interface TypeSafeModelCard {
|
|
65
|
-
name: string;
|
|
66
|
-
description: string;
|
|
67
|
-
release_date: string;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
84
|
interface SystemOneResponse {
|
|
71
85
|
model: string;
|
|
72
86
|
answers: Record<string, Answer>;
|
|
73
|
-
|
|
87
|
+
/** OpenRouter adds the billed `cost` in USD; TypeSafe reports tokens only. */
|
|
88
|
+
usage: { input_tokens: number; output_tokens: number; cost?: number };
|
|
74
89
|
}
|
|
75
90
|
|
|
76
91
|
/** Server hint wins (capped); otherwise exponential backoff from {@link BACKOFF_BASE_MS}. */
|
|
@@ -82,6 +97,8 @@ function backoffMs(attempt: number, headers: Headers | undefined): number {
|
|
|
82
97
|
|
|
83
98
|
export class TypeSafeJudge implements Judge {
|
|
84
99
|
readonly label: string;
|
|
100
|
+
readonly api: JudgmentApi;
|
|
101
|
+
readonly provider: string;
|
|
85
102
|
readonly model: string;
|
|
86
103
|
readonly baseUrl: string;
|
|
87
104
|
readonly #apiKey: ApiKey;
|
|
@@ -90,67 +107,55 @@ export class TypeSafeJudge implements Judge {
|
|
|
90
107
|
|
|
91
108
|
constructor(options: TypeSafeJudgeOptions) {
|
|
92
109
|
this.#apiKey = options.apiKey;
|
|
110
|
+
this.api = options.api ?? TYPESAFE_PROVIDER;
|
|
111
|
+
this.provider = options.provider ?? TYPESAFE_PROVIDER;
|
|
93
112
|
this.baseUrl = (options.baseUrl ?? typesafeBaseUrl()).replace(/\/+$/, "");
|
|
94
113
|
this.model = options.model ?? typesafeModel();
|
|
95
114
|
this.#fetch = options.fetch ?? fetch;
|
|
96
115
|
this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
97
|
-
this.label = `${
|
|
116
|
+
this.label = `${this.provider}/${this.model}`;
|
|
98
117
|
}
|
|
99
118
|
|
|
100
119
|
async judge<Q extends Questions>(request: JudgmentRequest<Q>, options?: JudgeOptions): Promise<JudgmentResult<Q>> {
|
|
101
120
|
const body = JSON.stringify({ state: request.state, model: this.model, questions: request.questions });
|
|
102
|
-
const
|
|
121
|
+
const signal = options?.signal;
|
|
122
|
+
const response = await withAuth(
|
|
123
|
+
this.#apiKey,
|
|
124
|
+
key => this.#attempt<SystemOneResponse>(JUDGMENT_ROUTES[this.api], body, key, signal),
|
|
125
|
+
{ signal },
|
|
126
|
+
);
|
|
103
127
|
for (const id in request.questions) {
|
|
104
128
|
const answer = response.answers[id];
|
|
105
129
|
if (answer === undefined || answer.type !== request.questions[id].type) {
|
|
106
130
|
throw new AIError.ProviderResponseError(
|
|
107
|
-
|
|
108
|
-
{ provider:
|
|
131
|
+
`${this.label} response is missing a "${request.questions[id].type}" answer for question "${id}"`,
|
|
132
|
+
{ provider: this.provider, kind: "envelope" },
|
|
109
133
|
);
|
|
110
134
|
}
|
|
111
135
|
}
|
|
112
136
|
return {
|
|
113
|
-
api:
|
|
114
|
-
provider:
|
|
137
|
+
api: this.api,
|
|
138
|
+
provider: this.provider,
|
|
115
139
|
model: response.model,
|
|
116
140
|
answers: response.answers as JudgmentResult<Q>["answers"],
|
|
117
|
-
usage: tokenUsage(response.usage.input_tokens, response.usage.output_tokens),
|
|
141
|
+
usage: tokenUsage(response.usage.input_tokens, response.usage.output_tokens, response.usage.cost),
|
|
118
142
|
};
|
|
119
143
|
}
|
|
120
144
|
|
|
121
|
-
|
|
122
|
-
async listModels(signal?: AbortSignal): Promise<TypeSafeModelCard[]> {
|
|
123
|
-
const response = await this.#request<{ models: TypeSafeModelCard[] }>("GET", "/v1/models", undefined, signal);
|
|
124
|
-
if (!Array.isArray(response.models)) {
|
|
125
|
-
throw new AIError.ProviderResponseError("TypeSafe /v1/models response is missing `models`", {
|
|
126
|
-
provider: TYPESAFE_PROVIDER,
|
|
127
|
-
kind: "envelope",
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
return response.models;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async #request<T>(method: "GET" | "POST", path: string, body: string | undefined, signal?: AbortSignal): Promise<T> {
|
|
134
|
-
return withAuth(this.#apiKey, key => this.#attempt<T>(method, path, body, key, signal), { signal });
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async #attempt<T>(
|
|
138
|
-
method: "GET" | "POST",
|
|
139
|
-
path: string,
|
|
140
|
-
body: string | undefined,
|
|
141
|
-
key: string,
|
|
142
|
-
signal: AbortSignal | undefined,
|
|
143
|
-
): Promise<T> {
|
|
145
|
+
async #attempt<T>(path: string, body: string, key: string, signal: AbortSignal | undefined): Promise<T> {
|
|
144
146
|
const url = `${this.baseUrl}${path}`;
|
|
145
|
-
const headers: Record<string, string> = {
|
|
146
|
-
|
|
147
|
+
const headers: Record<string, string> = {
|
|
148
|
+
Authorization: `Bearer ${key}`,
|
|
149
|
+
Accept: "application/json",
|
|
150
|
+
"Content-Type": "application/json",
|
|
151
|
+
};
|
|
147
152
|
for (let attempt = 0; ; attempt++) {
|
|
148
153
|
signal?.throwIfAborted();
|
|
149
154
|
const timeout = AbortSignal.timeout(this.#timeoutMs);
|
|
150
155
|
let response: Response;
|
|
151
156
|
try {
|
|
152
157
|
response = await this.#fetch(url, {
|
|
153
|
-
method,
|
|
158
|
+
method: "POST",
|
|
154
159
|
headers,
|
|
155
160
|
body,
|
|
156
161
|
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
@@ -162,7 +167,7 @@ export class TypeSafeJudge implements Judge {
|
|
|
162
167
|
}
|
|
163
168
|
if (response.ok) return (await response.json()) as T;
|
|
164
169
|
const text = await response.text();
|
|
165
|
-
const error = new TypeSafeApiError(
|
|
170
|
+
const error = new TypeSafeApiError(`${this.label} API error (${response.status}): ${text}`, response.status, {
|
|
166
171
|
headers: response.headers,
|
|
167
172
|
});
|
|
168
173
|
const transient = response.status === 408 || response.status === 429 || response.status >= 500;
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
* very turn that rotated; it stays on its existing session-close path.
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
-
import { clearAnthropicFastModeFallback } from "./providers/anthropic";
|
|
41
|
+
import { clearAnthropicFastModeFallback } from "./providers/anthropic-state";
|
|
42
42
|
import { resetOpenAIResponsesAccountScopedState } from "./providers/openai-responses";
|
|
43
43
|
import type { ProviderSessionState } from "./types";
|
|
44
44
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { isOfficialAnthropicApiUrl } from "@oh-my-pi/pi-catalog/compat/anthropic";
|
|
2
|
+
import type { Model } from "../types";
|
|
3
|
+
import type { AnthropicMessagesClientLike } from "./anthropic-client";
|
|
4
|
+
import { normalizeAnthropicBaseUrl, resolveDirectAnthropicBaseUrl } from "./anthropic-state";
|
|
5
|
+
|
|
6
|
+
function isCompactionCapableModel(model: Model<"anthropic-messages">): boolean {
|
|
7
|
+
return (
|
|
8
|
+
model.compat.supportsServerCompaction === true &&
|
|
9
|
+
model.compat.supportsContextManagement !== false &&
|
|
10
|
+
model.remoteCompaction?.enabled !== false
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Whether the model's effective first-party route is the official Anthropic API. */
|
|
15
|
+
export function resolvesToOfficialAnthropicEndpoint(model: Model<"anthropic-messages">): boolean {
|
|
16
|
+
const baseUrl =
|
|
17
|
+
model.provider === "anthropic" ? resolveDirectAnthropicBaseUrl(model) : normalizeAnthropicBaseUrl(model.baseUrl);
|
|
18
|
+
return isOfficialAnthropicApiUrl(baseUrl);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Whether the model and effective endpoint support Anthropic native compaction. */
|
|
22
|
+
export function supportsAnthropicCompaction(model: Model<"anthropic-messages">, effectiveBaseUrl?: string): boolean {
|
|
23
|
+
if (!isCompactionCapableModel(model)) return false;
|
|
24
|
+
if (model.remoteCompaction?.enabled === true) return true;
|
|
25
|
+
if (
|
|
26
|
+
model.transport === "pi-native" &&
|
|
27
|
+
model.compat.firstPartyProvider === true &&
|
|
28
|
+
(effectiveBaseUrl === undefined || effectiveBaseUrl === normalizeAnthropicBaseUrl(model.baseUrl))
|
|
29
|
+
) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return (
|
|
33
|
+
model.compat.firstPartyProvider === true &&
|
|
34
|
+
(effectiveBaseUrl === undefined
|
|
35
|
+
? resolvesToOfficialAnthropicEndpoint(model)
|
|
36
|
+
: isOfficialAnthropicApiUrl(effectiveBaseUrl))
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Read a caller-owned client's endpoint for request and compaction routing. */
|
|
41
|
+
export function injectedClientBaseUrl(client: AnthropicMessagesClientLike): string | undefined {
|
|
42
|
+
const candidate = "baseURL" in client ? client.baseURL : undefined;
|
|
43
|
+
return typeof candidate === "string" && candidate.length > 0 ? candidate : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Whether a caller-owned Anthropic client targets a compaction-capable endpoint. */
|
|
47
|
+
export function supportsAnthropicCompactionOnClient(
|
|
48
|
+
model: Model<"anthropic-messages">,
|
|
49
|
+
client: AnthropicMessagesClientLike,
|
|
50
|
+
): boolean {
|
|
51
|
+
const baseUrl = injectedClientBaseUrl(client);
|
|
52
|
+
if (baseUrl !== undefined) return supportsAnthropicCompaction(model, baseUrl);
|
|
53
|
+
return isCompactionCapableModel(model) && model.remoteCompaction?.enabled === true;
|
|
54
|
+
}
|