@oh-my-pi/pi-ai 18.2.7 → 18.2.9
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 +49 -18
- package/dist/types/auth-gateway/dispatch.d.ts +80 -0
- package/dist/types/auth-gateway/http.d.ts +5 -6
- package/dist/types/auth-gateway/index.d.ts +1 -0
- package/dist/types/auth-gateway/routes/embeddings.d.ts +3 -0
- package/dist/types/auth-gateway/routes/images.d.ts +3 -0
- package/dist/types/auth-gateway/routes/rerank.d.ts +3 -0
- package/dist/types/auth-gateway/routes/speech.d.ts +2 -0
- package/dist/types/auth-gateway/routes/systemone.d.ts +2 -0
- package/dist/types/auth-gateway/routes/transcriptions.d.ts +3 -0
- package/dist/types/auth-gateway/routes/video.d.ts +7 -0
- package/dist/types/auth-gateway/server.d.ts +10 -16
- package/dist/types/auth-gateway/types.d.ts +5 -0
- package/dist/types/auth-storage.d.ts +35 -32
- package/dist/types/embeddings/index.d.ts +7 -0
- package/dist/types/embeddings/openai-embeddings.d.ts +15 -0
- package/dist/types/embeddings/types.d.ts +15 -0
- package/dist/types/images/google-antigravity.d.ts +9 -0
- package/dist/types/images/google-generative-ai.d.ts +3 -0
- package/dist/types/images/index.d.ts +14 -0
- package/dist/types/images/openai-hosted.d.ts +3 -0
- package/dist/types/images/openai-images.d.ts +5 -0
- package/dist/types/images/openrouter-images.d.ts +3 -0
- package/dist/types/images/shared.d.ts +34 -0
- package/dist/types/images/types.d.ts +31 -0
- package/dist/types/index.d.ts +8 -1
- package/dist/types/judgment/typesafe.d.ts +2 -0
- package/dist/types/providers/amazon-bedrock.d.ts +7 -0
- package/dist/types/providers/claude-code-fingerprint.d.ts +22 -3
- package/dist/types/providers/embeddings-server.d.ts +32 -0
- package/dist/types/providers/google-gemini-cli.d.ts +0 -2
- package/dist/types/providers/images-server.d.ts +22 -0
- package/dist/types/providers/openai-chat-server-schema.d.ts +2 -2
- package/dist/types/providers/rerank-server.d.ts +35 -0
- package/dist/types/providers/speech-server.d.ts +8 -0
- package/dist/types/providers/systemone-server.d.ts +26 -0
- package/dist/types/providers/transcriptions-server.d.ts +32 -0
- package/dist/types/providers/video-server.d.ts +39 -0
- package/dist/types/rerank/index.d.ts +7 -0
- package/dist/types/rerank/openrouter-rerank.d.ts +15 -0
- package/dist/types/rerank/types.d.ts +17 -0
- package/dist/types/speech/index.d.ts +13 -0
- package/dist/types/speech/openai-speech.d.ts +3 -0
- package/dist/types/speech/transport.d.ts +7 -0
- package/dist/types/speech/types.d.ts +24 -0
- package/dist/types/speech/xai-tts.d.ts +7 -0
- package/dist/types/transcription/index.d.ts +7 -0
- package/dist/types/transcription/openai-transcriptions.d.ts +15 -0
- package/dist/types/transcription/types.d.ts +41 -0
- package/dist/types/usage/claude-api.d.ts +22 -0
- package/dist/types/usage/claude-reset.d.ts +44 -0
- package/dist/types/usage.d.ts +111 -5
- package/dist/types/utils/schema/json-schema-validator.d.ts +5 -2
- package/dist/types/utils/tool-call-loop-guard.d.ts +1 -1
- package/dist/types/video/index.d.ts +11 -0
- package/dist/types/video/openrouter-video.d.ts +19 -0
- package/dist/types/video/types.d.ts +62 -0
- package/package.json +30 -6
- package/src/auth/sqlite-credential-store.ts +44 -1
- package/src/auth-broker/remote-store.ts +6 -6
- package/src/auth-broker/wire-schemas.ts +14 -0
- package/src/auth-gateway/dispatch.ts +273 -0
- package/src/auth-gateway/http.ts +6 -7
- package/src/auth-gateway/index.ts +1 -0
- package/src/auth-gateway/routes/embeddings.ts +98 -0
- package/src/auth-gateway/routes/images.ts +131 -0
- package/src/auth-gateway/routes/rerank.ts +87 -0
- package/src/auth-gateway/routes/speech.ts +101 -0
- package/src/auth-gateway/routes/systemone.ts +116 -0
- package/src/auth-gateway/routes/transcriptions.ts +98 -0
- package/src/auth-gateway/routes/video.ts +243 -0
- package/src/auth-gateway/server.ts +127 -260
- package/src/auth-gateway/types.ts +5 -0
- package/src/auth-storage.ts +263 -140
- package/src/embeddings/index.ts +17 -0
- package/src/embeddings/openai-embeddings.ts +141 -0
- package/src/embeddings/types.ts +14 -0
- package/src/error/flags.ts +10 -0
- package/src/error/rate-limit.ts +1 -1
- package/src/images/google-antigravity.ts +180 -0
- package/src/images/google-generative-ai.ts +92 -0
- package/src/images/index.ts +59 -0
- package/src/images/openai-hosted.ts +185 -0
- package/src/images/openai-images.ts +110 -0
- package/src/images/openrouter-images.ts +33 -0
- package/src/images/shared.ts +193 -0
- package/src/images/types.ts +36 -0
- package/src/index.ts +8 -1
- package/src/judgment/typesafe.ts +5 -0
- package/src/providers/amazon-bedrock.ts +55 -5
- package/src/providers/anthropic.ts +45 -11
- package/src/providers/aws-credentials.ts +124 -11
- package/src/providers/claude-code-fingerprint.ts +55 -3
- package/src/providers/embeddings-server.ts +151 -0
- package/src/providers/gitlab-duo.ts +20 -4
- package/src/providers/google-gemini-cli.ts +0 -8
- package/src/providers/google-shared.ts +1 -18
- package/src/providers/images-server.ts +159 -0
- package/src/providers/openai-chat-server-schema.ts +1 -1
- package/src/providers/openai-chat-server.ts +3 -1
- package/src/providers/openai-codex-responses.ts +27 -5
- package/src/providers/openai-completions.ts +122 -19
- package/src/providers/pi-native-server.ts +1 -0
- package/src/providers/rerank-server.ts +166 -0
- package/src/providers/speech-server.ts +53 -0
- package/src/providers/systemone-server.ts +73 -0
- package/src/providers/transcriptions-server.ts +243 -0
- package/src/providers/video-server.ts +286 -0
- package/src/registry/oauth/anthropic.ts +2 -3
- package/src/rerank/index.ts +13 -0
- package/src/rerank/openrouter-rerank.ts +136 -0
- package/src/rerank/types.ts +20 -0
- package/src/speech/index.ts +35 -0
- package/src/speech/openai-speech.ts +26 -0
- package/src/speech/transport.ts +66 -0
- package/src/speech/types.ts +37 -0
- package/src/speech/xai-tts.ts +41 -0
- package/src/stream.ts +13 -4
- package/src/transcription/index.ts +17 -0
- package/src/transcription/openai-transcriptions.ts +133 -0
- package/src/transcription/types.ts +46 -0
- package/src/usage/alibaba-token-plan.ts +7 -1
- package/src/usage/claude-api.ts +66 -0
- package/src/usage/claude-reset.ts +638 -0
- package/src/usage/claude.ts +37 -59
- package/src/usage/kimi.ts +32 -1
- package/src/usage.ts +52 -5
- package/src/utils/schema/json-schema-validator.ts +23 -10
- package/src/utils/tool-call-loop-guard.ts +2 -2
- package/src/utils/validation.ts +145 -50
- package/src/video/index.ts +34 -0
- package/src/video/openrouter-video.ts +210 -0
- package/src/video/types.ts +72 -0
package/src/auth-storage.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* - re-exported `SqliteAuthCredentialStore`: concrete SQLite-backed implementation
|
|
9
9
|
*/
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
|
+
import { authPolicyFor } from "@oh-my-pi/pi-catalog/compat/auth";
|
|
11
12
|
import { planRequirementFor } from "@oh-my-pi/pi-catalog/compat/behavior";
|
|
12
13
|
import { $env, $envExact, getAgentDbPath, logger, untilAborted } from "@oh-my-pi/pi-utils";
|
|
13
14
|
import {
|
|
@@ -50,11 +51,14 @@ import type {
|
|
|
50
51
|
UsageLogger,
|
|
51
52
|
UsageProvider,
|
|
52
53
|
UsageReport,
|
|
54
|
+
UsageResetCredit,
|
|
55
|
+
UsageResetCredits,
|
|
53
56
|
} from "./usage";
|
|
54
57
|
import { resolveUsedFraction } from "./usage";
|
|
55
58
|
import { alibabaTokenPlanRankingStrategy, alibabaTokenPlanUsageProvider } from "./usage/alibaba-token-plan";
|
|
56
59
|
import { charmHyperUsageProvider } from "./usage/charm-hyper";
|
|
57
60
|
import { claudeRankingStrategy, claudeUsageProvider } from "./usage/claude";
|
|
61
|
+
import { consumeClaudeResetCredit, listClaudeResetCredits } from "./usage/claude-reset";
|
|
58
62
|
import { clinePassUsageProvider } from "./usage/cline-pass";
|
|
59
63
|
import { cursorUsageProvider } from "./usage/cursor";
|
|
60
64
|
import { devinUsageProvider } from "./usage/devin";
|
|
@@ -66,13 +70,7 @@ import { museCodeUsageProvider } from "./usage/muse-code";
|
|
|
66
70
|
import { minimaxCodeUsageProvider } from "./usage/minimax-code";
|
|
67
71
|
import { ollamaCloudUsageProvider, ollamaUsageProvider } from "./usage/ollama";
|
|
68
72
|
import { codexRankingStrategy, openaiCodexUsageProvider } from "./usage/openai-codex";
|
|
69
|
-
import {
|
|
70
|
-
type CodexResetConsumeCode,
|
|
71
|
-
type CodexResetCredit,
|
|
72
|
-
consumeCodexResetCredit,
|
|
73
|
-
listCodexResetCredits,
|
|
74
|
-
pickSoonestExpiringCredit,
|
|
75
|
-
} from "./usage/openai-codex-reset";
|
|
73
|
+
import { consumeCodexResetCredit, listCodexResetCredits, pickSoonestExpiringCredit } from "./usage/openai-codex-reset";
|
|
76
74
|
import { opencodeGoRankingStrategy, opencodeGoUsageProvider } from "./usage/opencode-go";
|
|
77
75
|
import { syntheticUsageProvider } from "./usage/synthetic";
|
|
78
76
|
import { umansUsageProvider } from "./usage/umans";
|
|
@@ -989,14 +987,15 @@ export interface StoredOAuthRefreshResult<T extends OAuthCredential = OAuthCrede
|
|
|
989
987
|
removed: boolean;
|
|
990
988
|
}
|
|
991
989
|
|
|
992
|
-
/**
|
|
993
|
-
* Identifies which stored account to redeem a saved rate-limit reset for.
|
|
994
|
-
* Any one field is enough; `credentialId` is the most precise.
|
|
995
|
-
*/
|
|
990
|
+
/** A saved-reset option bound to one provider and durable stored credential. */
|
|
996
991
|
export interface ResetCreditTarget {
|
|
997
|
-
|
|
992
|
+
provider: string;
|
|
993
|
+
credentialId: number;
|
|
994
|
+
/** Grant selected by the caller; a changed offer must be confirmed again. */
|
|
995
|
+
creditId?: string;
|
|
998
996
|
accountId?: string;
|
|
999
997
|
email?: string;
|
|
998
|
+
orgId?: string;
|
|
1000
999
|
}
|
|
1001
1000
|
|
|
1002
1001
|
/** Outcome of {@link AuthStorage.redeemResetCredit}. */
|
|
@@ -1011,21 +1010,28 @@ export interface ResetCreditRedeemOutcome {
|
|
|
1011
1010
|
* retryable, unlike a genuine `no_credit`), `http_<status>` (unexpected
|
|
1012
1011
|
* HTTP).
|
|
1013
1012
|
*/
|
|
1014
|
-
code:
|
|
1013
|
+
code: string;
|
|
1014
|
+
provider?: string;
|
|
1015
1015
|
accountId?: string;
|
|
1016
1016
|
email?: string;
|
|
1017
|
+
orgId?: string;
|
|
1018
|
+
/** Provider explanation for an unavailable or refused reset. */
|
|
1019
|
+
reason?: string;
|
|
1020
|
+
/** Normalized usage limit IDs the provider confirmed it cleared. */
|
|
1021
|
+
cleared?: string[];
|
|
1017
1022
|
/** The credit that was spent (when one was). */
|
|
1018
1023
|
creditId?: string;
|
|
1019
1024
|
}
|
|
1020
1025
|
|
|
1021
1026
|
/** One stored account's live saved-reset status, from {@link AuthStorage.listResetCredits}. */
|
|
1022
|
-
export interface ResetCreditAccountStatus {
|
|
1023
|
-
|
|
1027
|
+
export interface ResetCreditAccountStatus extends UsageResetCredits {
|
|
1028
|
+
provider: string;
|
|
1029
|
+
credentialId: number;
|
|
1024
1030
|
accountId?: string;
|
|
1025
1031
|
email?: string;
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
credits:
|
|
1032
|
+
orgId?: string;
|
|
1033
|
+
orgName?: string;
|
|
1034
|
+
credits: UsageResetCredit[];
|
|
1029
1035
|
/** Whether this is the given session's active account. */
|
|
1030
1036
|
active: boolean;
|
|
1031
1037
|
/** Set when the account's token refresh or list call failed. */
|
|
@@ -1398,6 +1404,13 @@ export class AuthStorage {
|
|
|
1398
1404
|
#usageHeaderIngestAt: Map<string, number> = new Map();
|
|
1399
1405
|
#usageReportsInFlight: Map<string, Promise<UsageReport[] | null>> = new Map();
|
|
1400
1406
|
#usageFetch: typeof fetch;
|
|
1407
|
+
/** Manual and automatic attempts on one stored account share a mutation. */
|
|
1408
|
+
#resetInFlight = new Map<string, { creditId?: string; promise: Promise<ResetCreditRedeemOutcome> }>();
|
|
1409
|
+
/** Ambiguous Claude claims retain their idempotency key until reconciled. */
|
|
1410
|
+
#pendingClaudeResets = new Map<
|
|
1411
|
+
string,
|
|
1412
|
+
{ creditId: string; requestId: string; program?: string; remainingCount?: number; startedAt: number }
|
|
1413
|
+
>();
|
|
1401
1414
|
#usageRequestTimeoutMs: number;
|
|
1402
1415
|
#usageLogger?: UsageLogger;
|
|
1403
1416
|
#fallbackResolver?: (provider: string) => string | undefined;
|
|
@@ -2952,16 +2965,57 @@ export class AuthStorage {
|
|
|
2952
2965
|
return this.#getCredentialsForProvider(provider).length > 0;
|
|
2953
2966
|
}
|
|
2954
2967
|
|
|
2968
|
+
/**
|
|
2969
|
+
* True when a stored credential is the provider's KDL `empty-fallback`
|
|
2970
|
+
* keyless-mode marker — what an empty paste at an "Optional: paste API key"
|
|
2971
|
+
* login prompt stores (e.g. `lm-studio-local` for lm-studio). The wire layer
|
|
2972
|
+
* never sends these as a bearer (`isDiscoveryBearerApiKey` strips them), so
|
|
2973
|
+
* auth-status surfaces must not count them either; otherwise the model hub
|
|
2974
|
+
* and `/login` report the provider as authenticated while every request
|
|
2975
|
+
* goes out bare (issue #12281). The credential itself stays stored: `/logout`
|
|
2976
|
+
* can still remove it, and availability treats the provider as keyless.
|
|
2977
|
+
*/
|
|
2978
|
+
#isKeylessFallbackCredential(provider: string, credential: AuthCredential): boolean {
|
|
2979
|
+
if (credential.type !== "api_key") return false;
|
|
2980
|
+
const login = authPolicyFor(provider)?.login;
|
|
2981
|
+
if (login?.kind !== "api-key") return false;
|
|
2982
|
+
const fallback = login.emptyFallback;
|
|
2983
|
+
return fallback !== undefined && fallback !== "" && credential.key === fallback;
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
/** Stored credentials that carry real auth — keyless-fallback markers excluded. */
|
|
2987
|
+
#getAuthBearingCredentials(provider: string): AuthCredential[] {
|
|
2988
|
+
return this.#getCredentialsForProvider(provider).filter(
|
|
2989
|
+
credential => !this.#isKeylessFallbackCredential(provider, credential),
|
|
2990
|
+
);
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
/**
|
|
2994
|
+
* True when the provider has stored credentials but none of them carries
|
|
2995
|
+
* auth — i.e. its only credential is the KDL `empty-fallback` keyless-mode
|
|
2996
|
+
* marker (an empty paste at an optional-key login prompt). Such a provider
|
|
2997
|
+
* is configured-but-keyless: model availability treats it like an
|
|
2998
|
+
* `auth: none` endpoint instead of locking it out (issue #12281).
|
|
2999
|
+
*/
|
|
3000
|
+
hasKeylessPlaceholder(provider: string): boolean {
|
|
3001
|
+
const stored = this.#getCredentialsForProvider(provider);
|
|
3002
|
+
return stored.length > 0 && stored.every(credential => this.#isKeylessFallbackCredential(provider, credential));
|
|
3003
|
+
}
|
|
3004
|
+
|
|
2955
3005
|
/**
|
|
2956
3006
|
* Dedicated auth for default-model availability (picker / `getAvailable`).
|
|
2957
3007
|
* Unlike {@link getApiKey}, this does not refresh OAuth tokens, and unlike
|
|
2958
3008
|
* {@link hasResolvableAuth} it ignores cross-provider env aliases so
|
|
2959
3009
|
* `XAI_API_KEY` does not auto-select SuperGrok (`xai-oauth`).
|
|
3010
|
+
*
|
|
3011
|
+
* A stored keyless-fallback marker (empty paste at an optional-key login)
|
|
3012
|
+
* does not count: it never reaches the wire as a bearer, so treating it as
|
|
3013
|
+
* auth would present the provider as signed-in while requests go out bare.
|
|
2960
3014
|
*/
|
|
2961
3015
|
hasAuth(provider: string): boolean {
|
|
2962
3016
|
if (this.#runtimeOverrides.has(provider)) return true;
|
|
2963
3017
|
if (this.#configOverrides.has(provider)) return true;
|
|
2964
|
-
if (this.#
|
|
3018
|
+
if (this.#getAuthBearingCredentials(provider).length > 0) return true;
|
|
2965
3019
|
if (this.#hasDedicatedEnvAuth(provider)) return true;
|
|
2966
3020
|
if (this.#fallbackResolver?.(provider)) return true;
|
|
2967
3021
|
return false;
|
|
@@ -2981,7 +3035,7 @@ export class AuthStorage {
|
|
|
2981
3035
|
hasConcreteAuth(provider: string): boolean {
|
|
2982
3036
|
if (this.#runtimeOverrides.has(provider)) return true;
|
|
2983
3037
|
if (this.#configOverrides.has(provider)) return true;
|
|
2984
|
-
if (this.#
|
|
3038
|
+
if (this.#getAuthBearingCredentials(provider).length > 0) return true;
|
|
2985
3039
|
if ((provider === "amazon-bedrock" || provider === "bedrock-mantle") && $env.AWS_BEARER_TOKEN_BEDROCK?.trim()) {
|
|
2986
3040
|
return true;
|
|
2987
3041
|
}
|
|
@@ -3021,7 +3075,7 @@ export class AuthStorage {
|
|
|
3021
3075
|
hasNonEnvCredential(provider: string): boolean {
|
|
3022
3076
|
if (this.#runtimeOverrides.has(provider)) return true;
|
|
3023
3077
|
if (this.#configOverrides.has(provider)) return true;
|
|
3024
|
-
if (this.#
|
|
3078
|
+
if (this.#getAuthBearingCredentials(provider).length > 0) return true;
|
|
3025
3079
|
if (this.#fallbackResolver?.(provider)) return true;
|
|
3026
3080
|
return false;
|
|
3027
3081
|
}
|
|
@@ -3053,7 +3107,7 @@ export class AuthStorage {
|
|
|
3053
3107
|
getCredentialOrigin(provider: string): CredentialOrigin | undefined {
|
|
3054
3108
|
if (this.#runtimeOverrides.has(provider)) return { kind: "runtime" };
|
|
3055
3109
|
if (this.#configOverrides.has(provider)) return { kind: "config" };
|
|
3056
|
-
const stored = this.#
|
|
3110
|
+
const stored = this.#getAuthBearingCredentials(provider);
|
|
3057
3111
|
if (stored.some(credential => credential.type === "oauth")) return { kind: "oauth" };
|
|
3058
3112
|
if (stored.some(credential => credential.type === "api_key" && credential.source === "login")) {
|
|
3059
3113
|
return { kind: "api_key" };
|
|
@@ -5994,7 +6048,10 @@ export class AuthStorage {
|
|
|
5994
6048
|
provider,
|
|
5995
6049
|
"api_key",
|
|
5996
6050
|
undefined,
|
|
5997
|
-
credential =>
|
|
6051
|
+
credential =>
|
|
6052
|
+
credential.type === "api_key" &&
|
|
6053
|
+
credential.source === "login" &&
|
|
6054
|
+
!this.#isKeylessFallbackCredential(provider, credential),
|
|
5998
6055
|
);
|
|
5999
6056
|
if (loginApiKeySelection) {
|
|
6000
6057
|
return this.#configValueResolver(loginApiKeySelection.credential.key);
|
|
@@ -6049,7 +6106,7 @@ export class AuthStorage {
|
|
|
6049
6106
|
provider,
|
|
6050
6107
|
sessionId,
|
|
6051
6108
|
options,
|
|
6052
|
-
credential => credential.source === "login",
|
|
6109
|
+
credential => credential.source === "login" && !this.#isKeylessFallbackCredential(provider, credential),
|
|
6053
6110
|
);
|
|
6054
6111
|
if (loginApiKeySelection) {
|
|
6055
6112
|
this.#recordSessionCredential(provider, sessionId, "api_key", loginApiKeySelection.index);
|
|
@@ -6346,16 +6403,7 @@ export class AuthStorage {
|
|
|
6346
6403
|
return this.#resolveStoredOAuthAccess(provider, selection, providerKey, options);
|
|
6347
6404
|
}
|
|
6348
6405
|
|
|
6349
|
-
/**
|
|
6350
|
-
* List saved rate-limit resets for every stored OAuth account of `provider`
|
|
6351
|
-
* (Codex), fetched LIVE from the dedicated `rate-limit-reset-credits` route.
|
|
6352
|
-
*
|
|
6353
|
-
* This deliberately bypasses the usage-report cache: `/wham/usage` is
|
|
6354
|
-
* IP-rate-limited and may serve stale (or pre-feature) snapshots when many
|
|
6355
|
-
* accounts are polled, which would hide redeemable credits. One entry per
|
|
6356
|
-
* account, with the session's active account flagged and unreachable
|
|
6357
|
-
* accounts carrying an `error`.
|
|
6358
|
-
*/
|
|
6406
|
+
/** List live saved-reset balances and eligibility for one provider's stored OAuth accounts. */
|
|
6359
6407
|
async listResetCredits(options?: {
|
|
6360
6408
|
provider?: string;
|
|
6361
6409
|
sessionId?: string;
|
|
@@ -6363,36 +6411,25 @@ export class AuthStorage {
|
|
|
6363
6411
|
signal?: AbortSignal;
|
|
6364
6412
|
}): Promise<ResetCreditAccountStatus[]> {
|
|
6365
6413
|
const provider = options?.provider ?? "openai-codex";
|
|
6366
|
-
|
|
6367
|
-
|
|
6414
|
+
if (provider !== "openai-codex" && provider !== "anthropic") return [];
|
|
6415
|
+
const accounts = this.listOAuthAccounts(provider, options?.sessionId);
|
|
6368
6416
|
const baseUrl = options?.baseUrlResolver?.(provider);
|
|
6369
|
-
const activeId = this.getOAuthAccountIdentity(provider, options?.sessionId);
|
|
6370
6417
|
return Promise.all(
|
|
6371
|
-
|
|
6372
|
-
const
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
credentialId: access.credentialId,
|
|
6378
|
-
accountId: access.accountId,
|
|
6379
|
-
email: access.email,
|
|
6380
|
-
active,
|
|
6381
|
-
};
|
|
6382
|
-
if (!access.ok)
|
|
6418
|
+
accounts.map(async (account): Promise<ResetCreditAccountStatus> => {
|
|
6419
|
+
const base = { ...account, provider };
|
|
6420
|
+
const access = await this.getOAuthAccessByCredentialId(provider, account.credentialId, {
|
|
6421
|
+
signal: options?.signal,
|
|
6422
|
+
});
|
|
6423
|
+
if (!access?.ok)
|
|
6383
6424
|
return {
|
|
6384
6425
|
...base,
|
|
6385
6426
|
availableCount: 0,
|
|
6386
6427
|
credits: [],
|
|
6387
|
-
error: access
|
|
6428
|
+
error: access?.error ?? "Account no longer available",
|
|
6388
6429
|
};
|
|
6389
|
-
const
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
baseUrl,
|
|
6393
|
-
fetch: this.#usageFetch,
|
|
6394
|
-
signal: options?.signal,
|
|
6395
|
-
});
|
|
6430
|
+
const auth = { ...access, baseUrl, fetch: this.#usageFetch, signal: options?.signal };
|
|
6431
|
+
const list =
|
|
6432
|
+
provider === "anthropic" ? await listClaudeResetCredits(auth) : await listCodexResetCredits(auth);
|
|
6396
6433
|
if (!list)
|
|
6397
6434
|
return {
|
|
6398
6435
|
...base,
|
|
@@ -6400,115 +6437,195 @@ export class AuthStorage {
|
|
|
6400
6437
|
credits: [],
|
|
6401
6438
|
error: "Failed to load saved resets",
|
|
6402
6439
|
};
|
|
6403
|
-
return {
|
|
6404
|
-
...base,
|
|
6405
|
-
availableCount: list.availableCount,
|
|
6406
|
-
credits: list.credits,
|
|
6407
|
-
};
|
|
6440
|
+
return { ...base, ...list };
|
|
6408
6441
|
}),
|
|
6409
6442
|
);
|
|
6410
6443
|
}
|
|
6411
6444
|
|
|
6412
6445
|
/**
|
|
6413
|
-
* Redeem
|
|
6414
|
-
*
|
|
6415
|
-
*
|
|
6416
|
-
* Resolves a fresh access token for the target account, picks an available
|
|
6417
|
-
* credit (the given `creditId`, else the first redeemable one), spends it,
|
|
6418
|
-
* and invalidates the cached usage report so the next `/usage` reflects the
|
|
6419
|
-
* reset. Never throws for business outcomes — inspect the returned `code`.
|
|
6446
|
+
* Redeem a stored account's saved reset after checking its live offer.
|
|
6447
|
+
* Business refusals return a code; transport errors may throw without losing Claude's request ID.
|
|
6420
6448
|
*/
|
|
6421
6449
|
async redeemResetCredit(options: {
|
|
6422
6450
|
target: ResetCreditTarget;
|
|
6423
|
-
provider?: string;
|
|
6424
|
-
creditId?: string;
|
|
6425
6451
|
baseUrlResolver?: (provider: string) => string | undefined;
|
|
6426
6452
|
signal?: AbortSignal;
|
|
6427
6453
|
}): Promise<ResetCreditRedeemOutcome> {
|
|
6428
|
-
const provider = options.provider ?? "openai-codex";
|
|
6429
|
-
const baseUrl = options.baseUrlResolver?.(provider);
|
|
6430
6454
|
const { target } = options;
|
|
6431
|
-
const
|
|
6432
|
-
const
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
(!!target.accountId && access.accountId === target.accountId) ||
|
|
6436
|
-
(!!target.email && access.email === target.email),
|
|
6437
|
-
);
|
|
6438
|
-
if (!match)
|
|
6439
|
-
return {
|
|
6440
|
-
ok: false,
|
|
6441
|
-
code: "no_account",
|
|
6442
|
-
accountId: target.accountId,
|
|
6443
|
-
email: target.email,
|
|
6444
|
-
};
|
|
6445
|
-
if (!match.ok) {
|
|
6446
|
-
return {
|
|
6447
|
-
ok: false,
|
|
6448
|
-
code: "account_unavailable",
|
|
6449
|
-
accountId: match.accountId,
|
|
6450
|
-
email: match.email,
|
|
6451
|
-
};
|
|
6455
|
+
const { provider, creditId } = target;
|
|
6456
|
+
const identity = { provider, accountId: target.accountId, email: target.email, orgId: target.orgId };
|
|
6457
|
+
if (provider !== "openai-codex" && provider !== "anthropic") {
|
|
6458
|
+
return { ...identity, ok: false, code: "unsupported_provider" };
|
|
6452
6459
|
}
|
|
6460
|
+
const baseUrl = options.baseUrlResolver?.(provider);
|
|
6461
|
+
const match = await this.getOAuthAccessByCredentialId(provider, target.credentialId, { signal: options.signal });
|
|
6462
|
+
if (!match) return { ...identity, ok: false, code: "no_account" };
|
|
6463
|
+
const resolvedIdentity = {
|
|
6464
|
+
provider,
|
|
6465
|
+
accountId: match.accountId,
|
|
6466
|
+
email: match.email,
|
|
6467
|
+
orgId: match.orgId,
|
|
6468
|
+
};
|
|
6469
|
+
if (!match.ok) return { ...resolvedIdentity, ok: false, code: "account_unavailable" };
|
|
6470
|
+
const accountKey = JSON.stringify([provider, baseUrl, target.credentialId]);
|
|
6471
|
+
const inFlight = this.#resetInFlight.get(accountKey);
|
|
6472
|
+
if (inFlight) {
|
|
6473
|
+
if (inFlight.creditId !== creditId) return { ...resolvedIdentity, ok: false, code: "reset_in_progress" };
|
|
6474
|
+
return inFlight.promise;
|
|
6475
|
+
}
|
|
6476
|
+
const promise = this.#redeemAccountReset(provider, match, accountKey, {
|
|
6477
|
+
creditId,
|
|
6478
|
+
baseUrl,
|
|
6479
|
+
signal: options.signal,
|
|
6480
|
+
}).finally(() => this.#resetInFlight.delete(accountKey));
|
|
6481
|
+
this.#resetInFlight.set(accountKey, { creditId, promise });
|
|
6482
|
+
return promise;
|
|
6483
|
+
}
|
|
6453
6484
|
|
|
6485
|
+
async #redeemAccountReset(
|
|
6486
|
+
provider: string,
|
|
6487
|
+
access: OAuthAccess,
|
|
6488
|
+
accountKey: string,
|
|
6489
|
+
options: { creditId?: string; baseUrl?: string; signal?: AbortSignal },
|
|
6490
|
+
): Promise<ResetCreditRedeemOutcome> {
|
|
6491
|
+
const identity = { provider, accountId: access.accountId, email: access.email, orgId: access.orgId };
|
|
6492
|
+
const auth = { ...access, baseUrl: options.baseUrl, fetch: this.#usageFetch, signal: options.signal };
|
|
6454
6493
|
let creditId = options.creditId;
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
// Transport/auth failure is NOT "no credits": callers treat `no_credit`
|
|
6464
|
-
// as terminal for the episode, so conflating them would bury a live
|
|
6465
|
-
// credit behind one flaky request.
|
|
6466
|
-
if (!list) {
|
|
6467
|
-
return {
|
|
6468
|
-
ok: false,
|
|
6469
|
-
code: "credit_list_failed",
|
|
6470
|
-
accountId: match.accountId,
|
|
6471
|
-
email: match.email,
|
|
6472
|
-
};
|
|
6494
|
+
let result: ResetCreditRedeemOutcome;
|
|
6495
|
+
let report: UsageReport | null = null;
|
|
6496
|
+
if (provider === "anthropic") {
|
|
6497
|
+
const list = await listClaudeResetCredits(auth);
|
|
6498
|
+
if (!list) return { ...identity, ok: false, code: "credit_list_failed" };
|
|
6499
|
+
const selected = list.credits.find(credit => credit.id === list.nextCreditId);
|
|
6500
|
+
if (creditId && creditId !== list.nextCreditId) {
|
|
6501
|
+
return { ...identity, ok: false, code: "offer_changed", creditId };
|
|
6473
6502
|
}
|
|
6474
|
-
|
|
6475
|
-
if (!credit)
|
|
6503
|
+
if (!list.eligible || !selected || !selected.usable || (list.redeemableCount ?? 0) < 1) {
|
|
6476
6504
|
return {
|
|
6505
|
+
...identity,
|
|
6477
6506
|
ok: false,
|
|
6478
|
-
code: "no_credit",
|
|
6479
|
-
|
|
6480
|
-
email: match.email,
|
|
6507
|
+
code: list.availableCount > 0 ? "ineligible" : "no_credit",
|
|
6508
|
+
reason: list.reason,
|
|
6481
6509
|
};
|
|
6482
|
-
|
|
6510
|
+
}
|
|
6511
|
+
creditId = selected.id;
|
|
6512
|
+
let pending = this.#pendingClaudeResets.get(accountKey);
|
|
6513
|
+
if (pending && Date.now() - pending.startedAt >= 10 * 60_000) {
|
|
6514
|
+
this.#pendingClaudeResets.delete(accountKey);
|
|
6515
|
+
pending = undefined;
|
|
6516
|
+
}
|
|
6517
|
+
if (pending) {
|
|
6518
|
+
if (pending.creditId !== creditId) return { ...identity, ok: false, code: "reset_unconfirmed", creditId };
|
|
6519
|
+
if (
|
|
6520
|
+
pending.remainingCount !== undefined &&
|
|
6521
|
+
selected.remainingCount !== undefined &&
|
|
6522
|
+
selected.remainingCount < pending.remainingCount
|
|
6523
|
+
) {
|
|
6524
|
+
this.#pendingClaudeResets.delete(accountKey);
|
|
6525
|
+
this.#invalidateUsageReportCache(provider, options.baseUrl);
|
|
6526
|
+
return { ...identity, ok: false, code: "already_redeemed", creditId };
|
|
6527
|
+
}
|
|
6528
|
+
if (pending.program === "juniper_tide") {
|
|
6529
|
+
return { ...identity, ok: false, code: "reset_unconfirmed", creditId };
|
|
6530
|
+
}
|
|
6531
|
+
}
|
|
6532
|
+
const credential = this.#getStoredCredentials(provider).find(entry => entry.id === access.credentialId);
|
|
6533
|
+
if (credential?.credential.type === "oauth") {
|
|
6534
|
+
report = await this.#getUsageReport(provider, credential.credential, {
|
|
6535
|
+
baseUrl: options.baseUrl,
|
|
6536
|
+
signal: options.signal,
|
|
6537
|
+
});
|
|
6538
|
+
}
|
|
6539
|
+
const requestId = pending?.requestId ?? crypto.randomUUID();
|
|
6540
|
+
options.signal?.throwIfAborted();
|
|
6541
|
+
this.#pendingClaudeResets.set(accountKey, {
|
|
6542
|
+
creditId,
|
|
6543
|
+
requestId,
|
|
6544
|
+
program: selected.program,
|
|
6545
|
+
remainingCount: selected.remainingCount,
|
|
6546
|
+
startedAt: pending?.startedAt ?? Date.now(),
|
|
6547
|
+
});
|
|
6548
|
+
const consumed = await consumeClaudeResetCredit({
|
|
6549
|
+
...auth,
|
|
6550
|
+
baseUrl: list.baseUrl ?? auth.baseUrl,
|
|
6551
|
+
orgId: list.orgId ?? access.orgId,
|
|
6552
|
+
credit: selected,
|
|
6553
|
+
redeemRequestId: requestId,
|
|
6554
|
+
});
|
|
6555
|
+
if (
|
|
6556
|
+
consumed.ok ||
|
|
6557
|
+
consumed.code === "already_redeemed" ||
|
|
6558
|
+
consumed.code === "nothing_to_reset" ||
|
|
6559
|
+
consumed.code === "ineligible" ||
|
|
6560
|
+
(!pending &&
|
|
6561
|
+
(consumed.code === "cooldown" ||
|
|
6562
|
+
consumed.status === 401 ||
|
|
6563
|
+
consumed.status === 403 ||
|
|
6564
|
+
consumed.status === 429))
|
|
6565
|
+
) {
|
|
6566
|
+
this.#pendingClaudeResets.delete(accountKey);
|
|
6567
|
+
}
|
|
6568
|
+
result = {
|
|
6569
|
+
...identity,
|
|
6570
|
+
ok: consumed.ok,
|
|
6571
|
+
code: consumed.code,
|
|
6572
|
+
reason: consumed.reason,
|
|
6573
|
+
cleared: consumed.cleared,
|
|
6574
|
+
creditId,
|
|
6575
|
+
};
|
|
6576
|
+
} else {
|
|
6577
|
+
if (!creditId) {
|
|
6578
|
+
const list = await listCodexResetCredits(auth);
|
|
6579
|
+
if (!list) return { ...identity, ok: false, code: "credit_list_failed" };
|
|
6580
|
+
const credit = pickSoonestExpiringCredit(list.credits);
|
|
6581
|
+
if (!credit) return { ...identity, ok: false, code: "no_credit" };
|
|
6582
|
+
creditId = credit.id;
|
|
6583
|
+
}
|
|
6584
|
+
const consumed = await consumeCodexResetCredit({ ...auth, creditId });
|
|
6585
|
+
result = { ...identity, ok: consumed.ok, code: consumed.code, creditId };
|
|
6483
6586
|
}
|
|
6484
|
-
|
|
6485
|
-
const result = await consumeCodexResetCredit({
|
|
6486
|
-
creditId,
|
|
6487
|
-
accessToken: match.accessToken,
|
|
6488
|
-
accountId: match.accountId,
|
|
6489
|
-
baseUrl,
|
|
6490
|
-
fetch: this.#usageFetch,
|
|
6491
|
-
signal: options.signal,
|
|
6492
|
-
});
|
|
6493
6587
|
if (result.ok) {
|
|
6494
|
-
this.#invalidateUsageReportCache(provider, baseUrl);
|
|
6588
|
+
this.#invalidateUsageReportCache(provider, options.baseUrl);
|
|
6495
6589
|
if (this.#store.invalidateUsageCache) {
|
|
6496
6590
|
await this.#store.invalidateUsageCache(options.signal).catch(err => {
|
|
6497
6591
|
logger.debug("Failed to notify store of stale usage", { err });
|
|
6498
6592
|
});
|
|
6499
6593
|
}
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6594
|
+
if (access.credentialId !== undefined) {
|
|
6595
|
+
if (provider === "anthropic")
|
|
6596
|
+
this.#clearClaudeResetBlocks(access.credentialId, result.cleared ?? [], report);
|
|
6597
|
+
else this.#clearCredentialBlocks(provider, access.credentialId);
|
|
6598
|
+
}
|
|
6599
|
+
}
|
|
6600
|
+
return result;
|
|
6601
|
+
}
|
|
6602
|
+
|
|
6603
|
+
/** Partial Claude resets must leave blocks for uncovered weekly/model limits intact. */
|
|
6604
|
+
#clearClaudeResetBlocks(credentialId: number, cleared: readonly string[], report: UsageReport | null): void {
|
|
6605
|
+
if (!report || Date.now() - report.fetchedAt > USAGE_REPORT_TTL_MS || cleared.length === 0) return;
|
|
6606
|
+
const provider = "anthropic";
|
|
6607
|
+
const index = this.#getStoredCredentials(provider).findIndex(entry => entry.id === credentialId);
|
|
6608
|
+
if (index < 0) return;
|
|
6609
|
+
const shared = report.limits.filter(limit => limit.scope.shared);
|
|
6610
|
+
if (!["anthropic:5h", "anthropic:7d"].every(id => shared.some(limit => limit.id === id))) return;
|
|
6611
|
+
const unscoped = report.limits.filter(
|
|
6612
|
+
limit => limit.scope.shared || limit.scope.tier === "opus" || limit.scope.tier === "sonnet",
|
|
6613
|
+
);
|
|
6614
|
+
const scopes = [
|
|
6615
|
+
{ blockScope: undefined, limits: unscoped },
|
|
6616
|
+
...(claudeRankingStrategy.healableBlockScopes?.(report) ?? []),
|
|
6617
|
+
];
|
|
6618
|
+
for (const scope of scopes) {
|
|
6619
|
+
if (!scope.limits.some(limit => cleared.includes(limit.id))) continue;
|
|
6620
|
+
if (this.#isUsageLimitReached(scope.limits.filter(limit => !cleared.includes(limit.id)))) continue;
|
|
6621
|
+
this.#clearCredentialBlockScope(
|
|
6622
|
+
provider,
|
|
6623
|
+
credentialId,
|
|
6624
|
+
index,
|
|
6625
|
+
this.#getProviderTypeKey(provider, "oauth"),
|
|
6626
|
+
scope.blockScope,
|
|
6627
|
+
);
|
|
6504
6628
|
}
|
|
6505
|
-
return {
|
|
6506
|
-
ok: result.ok,
|
|
6507
|
-
code: result.code,
|
|
6508
|
-
accountId: match.accountId,
|
|
6509
|
-
email: match.email,
|
|
6510
|
-
creditId,
|
|
6511
|
-
};
|
|
6512
6629
|
}
|
|
6513
6630
|
|
|
6514
6631
|
/**
|
|
@@ -7305,6 +7422,9 @@ export class AuthStorage {
|
|
|
7305
7422
|
throw error;
|
|
7306
7423
|
}
|
|
7307
7424
|
const updated: OAuthCredential = {
|
|
7425
|
+
// Preserve credential-subtype metadata, such as MCP token endpoints,
|
|
7426
|
+
// that the provider's bare OAuth response cannot reproduce.
|
|
7427
|
+
...attempted,
|
|
7308
7428
|
type: "oauth",
|
|
7309
7429
|
access: refreshed.access,
|
|
7310
7430
|
refresh: refreshed.refresh,
|
|
@@ -7493,7 +7613,10 @@ export class AuthStorage {
|
|
|
7493
7613
|
if (oauthSource) return oauthSource;
|
|
7494
7614
|
const loginApiKeySource = describeStored(
|
|
7495
7615
|
"api_key",
|
|
7496
|
-
credential =>
|
|
7616
|
+
credential =>
|
|
7617
|
+
credential.type === "api_key" &&
|
|
7618
|
+
credential.source === "login" &&
|
|
7619
|
+
!this.#isKeylessFallbackCredential(provider, credential),
|
|
7497
7620
|
);
|
|
7498
7621
|
if (loginApiKeySource) return loginApiKeySource;
|
|
7499
7622
|
if (getEnvApiKey(provider)) return `env (over ${baseLabel})`;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Api, Model } from "@oh-my-pi/pi-catalog/types";
|
|
2
|
+
import * as AIError from "../error";
|
|
3
|
+
import { embedOpenAI, type EmbeddingOptions } from "./openai-embeddings";
|
|
4
|
+
import type { EmbeddingRequest, EmbeddingResult } from "./types";
|
|
5
|
+
|
|
6
|
+
export * from "./openai-embeddings";
|
|
7
|
+
export * from "./types";
|
|
8
|
+
|
|
9
|
+
/** Dispatch an embedding request through the transport selected by the catalog model. */
|
|
10
|
+
export function embed(
|
|
11
|
+
model: Model<Api>,
|
|
12
|
+
request: EmbeddingRequest,
|
|
13
|
+
options: EmbeddingOptions,
|
|
14
|
+
): Promise<EmbeddingResult> {
|
|
15
|
+
if (model.api === "openai-embeddings") return embedOpenAI(model, request, options);
|
|
16
|
+
throw new AIError.ConfigurationError(`Unsupported embeddings API: ${model.api}`);
|
|
17
|
+
}
|