@code-yeongyu/senpi 2026.11.3-1 → 2026.11.3-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/bundle/chunks/anthropic-messages-LJAKRKMX.js +11 -0
- package/dist/bundle/chunks/{args-O3WS4WCO.js → args-GQTXDYS3.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-YLBBQXI7.js → azure-openai-responses-HDEKTPMZ.js} +1 -1
- package/dist/bundle/chunks/chatgpt-subscription.js +10 -2
- package/dist/bundle/chunks/{chunk-ED7VJGL4.js → chunk-2PGSRQBF.js} +1 -1
- package/dist/bundle/chunks/{chunk-4BF4FLT6.js → chunk-3LDASCJN.js} +1 -1
- package/dist/bundle/chunks/{chunk-PIAY636U.js → chunk-3QPE75AR.js} +1 -1
- package/dist/bundle/chunks/{chunk-FQZZGSYH.js → chunk-4ZGQUOBV.js} +1 -1
- package/dist/bundle/chunks/{chunk-S2M5D5GS.js → chunk-5BMIET5O.js} +2 -2
- package/dist/bundle/chunks/{chunk-XLLQJALL.js → chunk-67M7HKXO.js} +1 -1
- package/dist/bundle/chunks/chunk-6ISOXWH2.js +2 -0
- package/dist/bundle/chunks/{chunk-7KLEFHSU.js → chunk-6NPK2O3C.js} +2 -2
- package/dist/bundle/chunks/{chunk-BNIZP4OS.js → chunk-7C4W5HMU.js} +1 -1
- package/dist/bundle/chunks/chunk-7KIHKE2T.js +2 -0
- package/dist/bundle/chunks/{chunk-WCRNJYHO.js → chunk-A4KOQUHT.js} +1 -1
- package/dist/bundle/chunks/{chunk-TOOB6OLG.js → chunk-A5PZGQCP.js} +1 -1
- package/dist/bundle/chunks/{chunk-TBARMJ75.js → chunk-AVP6DDBF.js} +1 -1
- package/dist/bundle/chunks/{chunk-FLM6GZPT.js → chunk-B3GTID4P.js} +1 -1
- package/dist/bundle/chunks/{chunk-2XBKE2BZ.js → chunk-BIWFWGUA.js} +1 -1
- package/dist/bundle/chunks/{chunk-6LDUM5CL.js → chunk-DCI4QOUX.js} +1 -1
- package/dist/bundle/chunks/{chunk-2YFPDNNP.js → chunk-DNZKKG4V.js} +1 -1
- package/dist/bundle/chunks/{chunk-W2V7RIBF.js → chunk-E7SA75PP.js} +1 -1
- package/dist/bundle/chunks/{chunk-YWD54RHU.js → chunk-EBZ6IP4V.js} +1 -1
- package/dist/bundle/chunks/{chunk-J2OBN7TI.js → chunk-G5XUGK4B.js} +1 -1
- package/dist/bundle/chunks/{chunk-PX7FF5LE.js → chunk-GKP34H3C.js} +2 -2
- package/dist/bundle/chunks/{chunk-PP52OWBB.js → chunk-GLTOBXZY.js} +1 -1
- package/dist/bundle/chunks/{chunk-ZNLCKKF6.js → chunk-IJGXAACA.js} +117 -35
- package/dist/bundle/chunks/{chunk-LGO25HPL.js → chunk-JCTODLUP.js} +1 -1
- package/dist/bundle/chunks/{chunk-3ESZE3DG.js → chunk-KHBCZ6TR.js} +1 -1
- package/dist/bundle/chunks/chunk-KJR3XXMI.js +2 -0
- package/dist/bundle/chunks/{chunk-FJ2UCBOA.js → chunk-KY4V3TSO.js} +2 -2
- package/dist/bundle/chunks/{chunk-4SENPCEA.js → chunk-MCXOJHN2.js} +1 -1
- package/dist/bundle/chunks/{chunk-DPBPO5OU.js → chunk-NKI7L5UM.js} +3 -3
- package/dist/bundle/chunks/{chunk-CJWSQ6CK.js → chunk-OD7RT7G7.js} +1 -1
- package/dist/bundle/chunks/{chunk-F4OHTL2K.js → chunk-OOU3BRUY.js} +1 -1
- package/dist/bundle/chunks/{chunk-LPHUMR6R.js → chunk-OT6ALXSJ.js} +1 -1
- package/dist/bundle/chunks/{chunk-XGBRBCIQ.js → chunk-Q5CKFBQT.js} +1 -1
- package/dist/bundle/chunks/{chunk-F7GBSQRV.js → chunk-QPYDQ4QK.js} +1 -1
- package/dist/bundle/chunks/{chunk-MOOFLKLI.js → chunk-QSMT5KVJ.js} +1 -1
- package/dist/bundle/chunks/{chunk-SAZ6E74O.js → chunk-RUOTFZQR.js} +1 -1
- package/dist/bundle/chunks/{chunk-5DZD623T.js → chunk-RWF6IJQL.js} +2 -2
- package/dist/bundle/chunks/{chunk-HBCNDC53.js → chunk-S5Y7NEHB.js} +1 -1
- package/dist/bundle/chunks/{chunk-DAPBNRN5.js → chunk-SCBTASM4.js} +1 -1
- package/dist/bundle/chunks/chunk-SJWTZ3Z2.js +2 -0
- package/dist/bundle/chunks/{chunk-ISZ4EWIS.js → chunk-SODMQEHO.js} +1 -1
- package/dist/bundle/chunks/{chunk-WEGTCUO3.js → chunk-SQAQEITV.js} +1 -1
- package/dist/bundle/chunks/{chunk-UFVZTQKN.js → chunk-TQQG5L4D.js} +1 -1
- package/dist/bundle/chunks/{chunk-VPDQ6MOL.js → chunk-U6A4X3FV.js} +1 -1
- package/dist/bundle/chunks/{chunk-ACIFXWJ6.js → chunk-UHUM55TR.js} +1 -1
- package/dist/bundle/chunks/{chunk-L5OFGHDA.js → chunk-USKWGPA6.js} +1 -1
- package/dist/bundle/chunks/{chunk-7ORS33CE.js → chunk-VDDJADPV.js} +1 -1
- package/dist/bundle/chunks/{chunk-R65P6LAT.js → chunk-VIHXG53S.js} +1 -1
- package/dist/bundle/chunks/{chunk-SGS76663.js → chunk-VPVD5G32.js} +1 -1
- package/dist/bundle/chunks/chunk-WO6I5PRF.js +3 -0
- package/dist/bundle/chunks/{chunk-ETCYVVVO.js → chunk-WVEIJ437.js} +1 -1
- package/dist/bundle/chunks/{chunk-7565XQAU.js → chunk-XKFG4N3Z.js} +1 -1
- package/dist/bundle/chunks/{chunk-57MRFI6E.js → chunk-XMWPCJU6.js} +1 -1
- package/dist/bundle/chunks/{chunk-GDPWQJDQ.js → chunk-XPSXUKSD.js} +1 -1
- package/dist/bundle/chunks/{chunk-FNBEICAA.js → chunk-YI6KRJDC.js} +1 -1
- package/dist/bundle/chunks/{chunk-UURGXV5B.js → chunk-YP47G4YP.js} +1 -1
- package/dist/bundle/chunks/{chunk-6BSMXZKQ.js → chunk-YRYDCKUI.js} +1 -1
- package/dist/bundle/chunks/{chunk-QTQ3ES2S.js → chunk-YTKZSGMQ.js} +1 -1
- package/dist/bundle/chunks/{chunk-V36CXIIB.js → chunk-YXL3PNTL.js} +1 -1
- package/dist/bundle/chunks/chunk-Z7HTHT6K.js +2 -0
- package/dist/bundle/chunks/{chunk-27XEDYQA.js → chunk-ZTPG27EJ.js} +1 -1
- package/dist/bundle/chunks/{cli-main-XM5FWY3Z.js → cli-main-RN7YB427.js} +1 -1
- package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-FC7QLRU2.js → cloudflare-workers-ai-system-one-N5KFNKK4.js} +1 -1
- package/dist/bundle/chunks/credential-accounts-BJX55MTT.js +2 -0
- package/dist/bundle/chunks/devin.js +10 -2
- package/dist/bundle/chunks/{dispatch-UY6LBJJE.js → dispatch-M7UAPGVY.js} +1 -1
- package/dist/bundle/chunks/{google-generative-ai-7PQ4QRHJ.js → google-generative-ai-2TZSCSIS.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-DY7EO4EJ.js → google-vertex-7NJJOI7V.js} +1 -1
- package/dist/bundle/chunks/{health-write-45CX64T2.js → health-write-YUSPVH7U.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-3EDGRKQM.js → help-fast-path-DWCA77QP.js} +1 -1
- package/dist/bundle/chunks/host-command-KJXUTSQ7.js +2 -0
- package/dist/bundle/chunks/{host-lifecycle-TGG7KGQH.js → host-lifecycle-QHIJ4G2K.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +1 -1
- package/dist/bundle/chunks/{host-session-command-OJFARE5Q.js → host-session-command-IPH3WXSG.js} +1 -1
- package/dist/bundle/chunks/{host-socket-request-F2R5P34M.js → host-socket-request-XPXJV3CS.js} +1 -1
- package/dist/bundle/chunks/interactive-mode-XUXMDYLA.js +2 -0
- package/dist/bundle/chunks/{list-tips-3D3A6GFK.js → list-tips-XJY7EL5S.js} +1 -1
- package/dist/bundle/chunks/{login-health-YOPTYRTU.js → login-health-226NO44U.js} +1 -1
- package/dist/bundle/chunks/main-P7PKMJUG.js +2 -0
- package/dist/bundle/chunks/{mistral-conversations-6F2RAIB2.js → mistral-conversations-QMHMHSUF.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-N2RQ7JSZ.js → multi-session-host-ZQY4AN46.js} +1 -1
- package/dist/bundle/chunks/{native-loader-NMDTMHMA.js → native-loader-A6DNMGCK.js} +1 -1
- package/dist/bundle/chunks/openai-chatgpt.js +11 -3
- package/dist/bundle/chunks/{openai-codex-responses-YTWLG4OL.js → openai-codex-responses-GUAHKQE2.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-TIFDYHZL.js → openai-completions-UPRTFM54.js} +1 -1
- package/dist/bundle/chunks/{openai-images-OAEMY4RG.js → openai-images-IKWNS6VB.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-L35NIG5Y.js → openai-responses-AXN5NUHH.js} +1 -1
- package/dist/bundle/chunks/openrouter.js +10 -2
- package/dist/bundle/chunks/{package-manager-cli-JPNA27E5.js → package-manager-cli-PP7L2Q5M.js} +1 -1
- package/dist/bundle/chunks/radius.js +10 -2
- package/dist/bundle/chunks/{rg-engine-JDUYBM6L.js → rg-engine-VRSXT6HG.js} +1 -1
- package/dist/bundle/chunks/{role-45Y7PQFC.js → role-L5J7ZPCF.js} +1 -1
- package/dist/bundle/chunks/rotation-stream-6INZXQFC.js +2 -0
- package/dist/bundle/chunks/rpc-mode-W4UATRAT.js +2 -0
- package/dist/bundle/chunks/{rpc-monitor-source-5TUR27NY.js → rpc-monitor-source-RMMUPBMS.js} +1 -1
- package/dist/bundle/chunks/{run-attached-tui-TI6MELEI.js → run-attached-tui-RLXFGCEP.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-OZ5ESMIT.js → schedule-command-ZNFEJNTT.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-5YKBYJOO.js → session-control-endpoint-TWTONJWF.js} +1 -1
- package/dist/bundle/chunks/{session-picker-CMRV72VA.js → session-picker-KRUVCHOV.js} +1 -1
- package/dist/bundle/chunks/session-slot-NJOZCOHQ.js +2 -0
- package/dist/bundle/chunks/session-worker.js +218 -135
- package/dist/bundle/chunks/{state-store-Q47AYMRI.js → state-store-ZAFZ4JOM.js} +1 -1
- package/dist/bundle/chunks/{typesafe-system-one-5JYDJK6S.js → typesafe-system-one-CICIRAYN.js} +1 -1
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/host-supervisor.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/agent-session.js +5 -3
- package/dist/core/auth-storage.d.ts +19 -0
- package/dist/core/auth-storage.js +99 -24
- package/dist/core/auth-store-load-error.d.ts +29 -0
- package/dist/core/auth-store-load-error.js +74 -0
- package/dist/core/brand.d.ts +2 -0
- package/dist/core/brand.js +1 -0
- package/dist/core/credential-pool/failover.d.ts +8 -0
- package/dist/core/credential-pool/failover.js +18 -5
- package/dist/core/credential-pool/rotation-stream.d.ts +1 -1
- package/dist/core/credential-pool/rotation-stream.js +2 -7
- package/dist/core/credential-pool/session-slot.d.ts +20 -0
- package/dist/core/credential-pool/session-slot.js +26 -0
- package/dist/core/credential-recovery-revision.d.ts +10 -0
- package/dist/core/credential-recovery-revision.js +19 -0
- package/dist/core/execution/executor-body.d.ts +22 -0
- package/dist/core/execution/executor-body.js +73 -0
- package/dist/core/execution/executor-freeze.d.ts +17 -0
- package/dist/core/execution/executor-freeze.js +53 -0
- package/dist/core/execution/executor-pipeline.d.ts +9 -0
- package/dist/core/execution/executor-pipeline.js +25 -0
- package/dist/core/execution/executor-types.d.ts +82 -0
- package/dist/core/execution/executor-types.js +2 -0
- package/dist/core/execution/executor.d.ts +8 -0
- package/dist/core/execution/executor.js +216 -0
- package/dist/core/extensions/builtin/anthropic-subscription/account-block-log.d.ts +15 -0
- package/dist/core/extensions/builtin/anthropic-subscription/account-block-log.js +18 -0
- package/dist/core/extensions/builtin/anthropic-subscription/affinity.d.ts +9 -1
- package/dist/core/extensions/builtin/anthropic-subscription/affinity.js +15 -4
- package/dist/core/extensions/builtin/anthropic-subscription/failover.d.ts +7 -1
- package/dist/core/extensions/builtin/anthropic-subscription/failover.js +19 -2
- package/dist/core/extensions/builtin/anthropic-subscription/guidance.d.ts +8 -2
- package/dist/core/extensions/builtin/anthropic-subscription/guidance.js +25 -1
- package/dist/core/extensions/builtin/anthropic-subscription/provider-diagnostic.d.ts +9 -0
- package/dist/core/extensions/builtin/anthropic-subscription/provider-diagnostic.js +59 -0
- package/dist/core/extensions/builtin/anthropic-subscription/session-registry-wiring.js +7 -0
- package/dist/core/extensions/builtin/anthropic-subscription/stream-guidance.js +5 -3
- package/dist/core/extensions/builtin/anthropic-subscription/stream.js +4 -0
- package/dist/core/extensions/builtin/btw/index.js +3 -1
- package/dist/core/extensions/builtin/cache-keepalive/index.js +3 -1
- package/dist/core/extensions/builtin/compaction/index.js +1 -1
- package/dist/core/extensions/builtin/compaction/openai-remote.d.ts +3 -1
- package/dist/core/extensions/builtin/compaction/openai-remote.js +1 -1
- package/dist/core/extensions/builtin/compaction/speculative.js +3 -2
- package/dist/core/extensions/builtin/cursor-cli-oauth/account-command.js +3 -1
- package/dist/core/extensions/builtin/cursor-cli-oauth/native-bootstrap.js +3 -1
- package/dist/core/extensions/builtin/look-at/runner.js +3 -1
- package/dist/core/extensions/builtin/mcp/auth/callback.js +3 -4
- package/dist/core/extensions/builtin/mcp/auth/oauth-provider.js +2 -1
- package/dist/core/model-registry.d.ts +10 -2
- package/dist/core/model-registry.js +12 -4
- package/dist/core/model-runtime.d.ts +8 -0
- package/dist/core/model-runtime.js +38 -0
- package/dist/core/retry-fallback/controller-types.d.ts +7 -0
- package/dist/core/retry-fallback/controller.d.ts +2 -0
- package/dist/core/retry-fallback/controller.js +13 -4
- package/dist/core/retry-fallback/credential-recovery.d.ts +18 -0
- package/dist/core/retry-fallback/credential-recovery.js +32 -0
- package/dist/core/retry-fallback/settings.d.ts +8 -1
- package/dist/core/retry-fallback/settings.js +12 -6
- package/dist/core/session-log.js +1 -1
- package/dist/core/settings-manager.js +4 -1
- package/dist/modes/rpc/connection-handler.js +3 -1
- package/dist/modes/rpc/effect-commands.d.ts +1 -0
- package/dist/modes/rpc/loopless-execute-grant.d.ts +20 -0
- package/dist/modes/rpc/loopless-execute-grant.js +26 -0
- package/dist/modes/rpc/loopless-runtime-settings.d.ts +30 -0
- package/dist/modes/rpc/loopless-runtime-settings.js +69 -0
- package/dist/modes/rpc/loopless-tool-call.d.ts +12 -0
- package/dist/modes/rpc/loopless-tool-call.js +22 -0
- package/dist/modes/rpc/rpc-read-response-types.d.ts +2 -1
- package/dist/modes/rpc/rpc-session-state-types.d.ts +10 -0
- package/dist/modes/rpc/rpc-types.d.ts +1 -1
- package/dist/modes/rpc/runtime-factory-loopless.d.ts +57 -0
- package/dist/modes/rpc/runtime-factory-loopless.js +131 -0
- package/docs/development.md +1 -1
- package/docs/rpc.md +2 -0
- package/package.json +6 -6
- package/dist/bundle/chunks/anthropic-messages-AFWQB3KN.js +0 -10
- package/dist/bundle/chunks/chunk-3WJ2DE2G.js +0 -3
- package/dist/bundle/chunks/chunk-ESQBKTNG.js +0 -2
- package/dist/bundle/chunks/chunk-MOH5HOHX.js +0 -2
- package/dist/bundle/chunks/credential-accounts-YZYSKA7X.js +0 -2
- package/dist/bundle/chunks/host-command-PZ4VYQIW.js +0 -2
- package/dist/bundle/chunks/interactive-mode-7JSJVVIS.js +0 -2
- package/dist/bundle/chunks/main-3YGJ2O4I.js +0 -2
- package/dist/bundle/chunks/rotation-stream-WXWJKJZN.js +0 -2
- package/dist/bundle/chunks/rpc-mode-Z7MN2ZWA.js +0 -2
|
@@ -11,7 +11,29 @@ export function noAccountGuidance(hasAnthropicCredential) {
|
|
|
11
11
|
lines.push(" Or set CLAUDE_CODE_OAUTH_TOKEN (and _2.._N for more accounts),", " or log in with the claude CLI for ambient auth.");
|
|
12
12
|
return lines.join("\n");
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
/** One line per pool member, naming the account that needs /login (senpi#4671). */
|
|
15
|
+
function accountStateLines(accounts) {
|
|
16
|
+
return accounts.map((account) => {
|
|
17
|
+
const until = account.until === undefined ? "re-login" : new Date(account.until).toISOString();
|
|
18
|
+
const login = account.reason === "auth_error" ? ` - run /login ${PROVIDER} for this account` : "";
|
|
19
|
+
return ` ${account.name}: ${account.reason} until ${until}${login}`;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The pool context of a failure that left no other account (senpi#4671): the failure stays the error, and this
|
|
24
|
+
* names the soonest retry (which goal mode reads) and every account's state. No refusal note: the pool did not
|
|
25
|
+
* refuse the request, the account failed it.
|
|
26
|
+
*/
|
|
27
|
+
export function exhaustedPoolGuidance(soonestUnblockAt, accounts) {
|
|
28
|
+
const eta = soonestUnblockAt !== undefined && Number.isFinite(soonestUnblockAt)
|
|
29
|
+
? new Date(soonestUnblockAt).toISOString()
|
|
30
|
+
: "after re-login";
|
|
31
|
+
return [
|
|
32
|
+
` No other Claude account is available. Soonest automatic retry: ${eta}.`,
|
|
33
|
+
...accountStateLines(accounts),
|
|
34
|
+
].join("\n");
|
|
35
|
+
}
|
|
36
|
+
export function allAccountsBlockedGuidance(soonestUnblockAt, blockReason, accounts = []) {
|
|
15
37
|
if (blockReason === "auth_error") {
|
|
16
38
|
// omo#8383: name the authentication failure; the recovery is a re-login. Only a pool whose every
|
|
17
39
|
// account is auth-blocked says so (senpi#2081), and the outer credential pool reads the refusal note
|
|
@@ -22,6 +44,7 @@ export function allAccountsBlockedGuidance(soonestUnblockAt, blockReason) {
|
|
|
22
44
|
return [
|
|
23
45
|
`All Claude accounts for ${PROVIDER} are currently blocked (authentication error).`,
|
|
24
46
|
...recheck,
|
|
47
|
+
...accountStateLines(accounts),
|
|
25
48
|
` /login ${PROVIDER} - re-authenticate to refresh the blocked account`,
|
|
26
49
|
` /claude-account list - inspect account states`,
|
|
27
50
|
ACCOUNT_POOL_REFUSAL_NOTE,
|
|
@@ -33,6 +56,7 @@ export function allAccountsBlockedGuidance(soonestUnblockAt, blockReason) {
|
|
|
33
56
|
return [
|
|
34
57
|
`All Claude accounts for ${PROVIDER} are currently blocked (rate limit or auth errors).`,
|
|
35
58
|
` Soonest automatic retry: ${eta}.`,
|
|
59
|
+
...accountStateLines(accounts),
|
|
36
60
|
` /claude-account list - inspect account states`,
|
|
37
61
|
` /login ${PROVIDER} - add another account`,
|
|
38
62
|
ACCOUNT_POOL_REFUSAL_NOTE,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ProviderDiagnostic } from "@earendil-works/pi-ai";
|
|
2
|
+
/**
|
|
3
|
+
* The providerDiagnostic for an Anthropic Subscription turn failure (senpi#4672), mirroring the
|
|
4
|
+
* chatgpt-subscription adapter (senpi#3771). A pool the extension refused locally is `auth` when every
|
|
5
|
+
* account waits on a new login, otherwise `rate_limit` with the soonest retry; a classified SDK failure maps
|
|
6
|
+
* to its provider token. The result is revalidated against the closed vocabulary, so nothing else escapes.
|
|
7
|
+
*/
|
|
8
|
+
export declare function anthropicSubscriptionDiagnostic(error: unknown): ProviderDiagnostic | undefined;
|
|
9
|
+
//# sourceMappingURL=provider-diagnostic.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { sanitizeProviderDiagnostic, } from "@earendil-works/pi-ai";
|
|
2
|
+
import { isThinkingBindingMismatchText } from "@earendil-works/pi-ai/api/anthropic-thinking-binding";
|
|
3
|
+
import { AllAccountsBlockedError } from "./affinity.js";
|
|
4
|
+
import { classifySdkError } from "./errors.js";
|
|
5
|
+
import { ClassifiedSdkError } from "./failover.js";
|
|
6
|
+
/** The allowlisted provider token each classified SDK failure proves; kinds that prove nothing map to none. */
|
|
7
|
+
const SDK_KIND_DIAGNOSTICS = {
|
|
8
|
+
auth_error: { category: "auth", code: "authentication_error" },
|
|
9
|
+
rate_limit: { category: "rate_limit", code: "rate_limit_error" },
|
|
10
|
+
overloaded: { category: "provider_unavailable", code: "overloaded_error" },
|
|
11
|
+
billing: { category: "unknown", code: "billing_error" },
|
|
12
|
+
org_not_allowed: { category: "unknown", code: "permission_error" },
|
|
13
|
+
};
|
|
14
|
+
const CAUSE_DEPTH = 8;
|
|
15
|
+
/** The pool refusal behind `error`: the error itself or one it carries as `cause`, so wrapping never hides it. */
|
|
16
|
+
function poolRefusal(error) {
|
|
17
|
+
let current = error;
|
|
18
|
+
for (let depth = 0; depth < CAUSE_DEPTH && current instanceof Error; depth++) {
|
|
19
|
+
if (current instanceof AllAccountsBlockedError)
|
|
20
|
+
return current;
|
|
21
|
+
current = current.cause;
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The providerDiagnostic for an Anthropic Subscription turn failure (senpi#4672), mirroring the
|
|
27
|
+
* chatgpt-subscription adapter (senpi#3771). A pool the extension refused locally is `auth` when every
|
|
28
|
+
* account waits on a new login, otherwise `rate_limit` with the soonest retry; a classified SDK failure maps
|
|
29
|
+
* to its provider token. The result is revalidated against the closed vocabulary, so nothing else escapes.
|
|
30
|
+
*/
|
|
31
|
+
export function anthropicSubscriptionDiagnostic(error) {
|
|
32
|
+
const pool = poolRefusal(error);
|
|
33
|
+
if (pool !== undefined) {
|
|
34
|
+
// A pin to an account that is no longer configured is a configuration error, not a limit: no diagnostic.
|
|
35
|
+
if (pool.unavailableAccount?.configured === false)
|
|
36
|
+
return undefined;
|
|
37
|
+
const auth = pool.blockReason === "auth_error";
|
|
38
|
+
return sanitizeProviderDiagnostic({
|
|
39
|
+
category: auth ? "auth" : "rate_limit",
|
|
40
|
+
code: auth ? "account_pool_auth_blocked" : "account_pool_blocked",
|
|
41
|
+
evidence: "structured_code",
|
|
42
|
+
...(auth || pool.soonestUnblockAt === undefined ? {} : { resetsAt: pool.soonestUnblockAt }),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const original = error instanceof ClassifiedSdkError ? error.original : error;
|
|
46
|
+
if (original instanceof Error && isThinkingBindingMismatchText(original.message)) {
|
|
47
|
+
// senpi#5587: the API refused a replayed thinking block bound to a different conversation.
|
|
48
|
+
return sanitizeProviderDiagnostic({
|
|
49
|
+
category: "invalid_request",
|
|
50
|
+
httpStatus: 400,
|
|
51
|
+
code: "invalid_request_error",
|
|
52
|
+
evidence: "structured_code",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const kind = error instanceof ClassifiedSdkError ? error.classification.kind : classifySdkError(error).kind;
|
|
56
|
+
const mapped = SDK_KIND_DIAGNOSTICS[kind];
|
|
57
|
+
return mapped === undefined ? undefined : sanitizeProviderDiagnostic({ ...mapped, evidence: "structured_code" });
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=provider-diagnostic.js.map
|
|
@@ -178,6 +178,13 @@ export function registerSessionRegistry(pi) {
|
|
|
178
178
|
await writeStoredBinding(sessionFile, stored);
|
|
179
179
|
});
|
|
180
180
|
pi.on("session_shutdown", (event, ctx) => {
|
|
181
|
+
// A config hot-reload keeps the session and this module's registry (session_start skips
|
|
182
|
+
// "reload" above), and the resident query captures no per-load state: its hooks and tool
|
|
183
|
+
// handlers are module constants. The next turn's fingerprint check restarts the process
|
|
184
|
+
// only when the reload changed the prompt, tools or options. Closing here re-spawned Claude
|
|
185
|
+
// Code on every reload, and on a real account re-attached the claude.ai connectors.
|
|
186
|
+
if (event.reason === "reload")
|
|
187
|
+
return;
|
|
181
188
|
closeSession(ctx.sessionManager.getSessionId(), event.reason);
|
|
182
189
|
});
|
|
183
190
|
pi.on("session_extensions_removed", async (_event, ctx) => {
|
|
@@ -2,7 +2,7 @@ import { AllAccountsBlockedError } from "./affinity.js";
|
|
|
2
2
|
import { errorAccountContext } from "./account-context.js";
|
|
3
3
|
import { classifySdkError } from "./errors.js";
|
|
4
4
|
import { ClassifiedSdkError } from "./failover.js";
|
|
5
|
-
import { allAccountsBlockedGuidance, claudeCodeVersionFloorGuidance, sdkErrorGuidance, singleAccountUnavailableGuidance, switchAccountLine, } from "./guidance.js";
|
|
5
|
+
import { allAccountsBlockedGuidance, claudeCodeVersionFloorGuidance, exhaustedPoolGuidance, sdkErrorGuidance, singleAccountUnavailableGuidance, switchAccountLine, } from "./guidance.js";
|
|
6
6
|
import { ClaudeSignInRequiredError } from "./sign-in-again.js";
|
|
7
7
|
export function withAuthGuidance(error, message, ran, now = Date.now()) {
|
|
8
8
|
// The sign-in-again prompt is the remedy; the generic auth_error hint ("token was rejected... refresh") contradicts it.
|
|
@@ -10,7 +10,7 @@ export function withAuthGuidance(error, message, ran, now = Date.now()) {
|
|
|
10
10
|
return message;
|
|
11
11
|
if (error instanceof AllAccountsBlockedError) {
|
|
12
12
|
const guidance = error.unavailableAccount === undefined
|
|
13
|
-
? allAccountsBlockedGuidance(error.soonestUnblockAt, error.blockReason)
|
|
13
|
+
? allAccountsBlockedGuidance(error.soonestUnblockAt, error.blockReason, error.accounts)
|
|
14
14
|
: singleAccountUnavailableGuidance(error.unavailableAccount, error.soonestUnblockAt, error.blockReason);
|
|
15
15
|
// The model-limit sentence is how the fallback chain learns the limit binds this model only (senpi#5007).
|
|
16
16
|
return error.limitedModel === undefined
|
|
@@ -28,7 +28,9 @@ export function withAuthGuidance(error, message, ran, now = Date.now()) {
|
|
|
28
28
|
guidance = `${selected} ${action}`;
|
|
29
29
|
}
|
|
30
30
|
const versionGuidance = claudeCodeVersionFloorGuidance(message, ran);
|
|
31
|
-
const
|
|
31
|
+
const pool = error instanceof ClassifiedSdkError ? error.poolState : undefined;
|
|
32
|
+
const poolGuidance = pool && exhaustedPoolGuidance(pool.soonestUnblockAt, pool.accounts);
|
|
33
|
+
const hints = [guidance, versionGuidance, poolGuidance].filter((hint) => hint !== undefined);
|
|
32
34
|
return hints.length > 0 ? `${message}\n${hints.join("\n")}` : message;
|
|
33
35
|
}
|
|
34
36
|
/**
|
|
@@ -13,6 +13,7 @@ import { refusalError } from "./refusal.js";
|
|
|
13
13
|
import { getSdkBoundary, loadClaudeAgentSdk } from "./sdk-boundary.js";
|
|
14
14
|
import { emitContinuityObservation } from "./session-observability.js";
|
|
15
15
|
import { forgetBinding } from "./session-reattach.js";
|
|
16
|
+
import { anthropicSubscriptionDiagnostic } from "./provider-diagnostic.js";
|
|
16
17
|
import { requireAnthropicSubscriptionCwd } from "./session-cwd.js";
|
|
17
18
|
import { residentSessionMessages } from "./session-stream.js";
|
|
18
19
|
import { loadAnthropicSubscriptionProviderSettingsFromDisk, resolveCompactionOwner, resumeModeSource, } from "./settings.js";
|
|
@@ -268,6 +269,9 @@ export function streamAnthropicSubscription(model, context, options) {
|
|
|
268
269
|
if (billed)
|
|
269
270
|
updateUsage(model, output, billed, "query");
|
|
270
271
|
output.errorMessage = withAuthGuidance(error, errorMessage(error), claudeCodeRun);
|
|
272
|
+
const diagnostic = output.stopReason === "error" ? anthropicSubscriptionDiagnostic(error) : undefined;
|
|
273
|
+
if (diagnostic !== undefined)
|
|
274
|
+
output.providerDiagnostic = diagnostic;
|
|
271
275
|
markColdSeedOverflow(output, model, coldSeedAttempt, coldSeedEstimate, options?.sessionId);
|
|
272
276
|
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
273
277
|
}
|
|
@@ -78,7 +78,9 @@ export default function btwExtension(pi) {
|
|
|
78
78
|
return undefined;
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model
|
|
81
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model, {
|
|
82
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
83
|
+
});
|
|
82
84
|
if (!auth.ok) {
|
|
83
85
|
if (active !== entry)
|
|
84
86
|
return;
|
|
@@ -136,7 +136,9 @@ export function createCacheKeepAliveExtension(dependencies = {}) {
|
|
|
136
136
|
.getAllTools()
|
|
137
137
|
.filter((tool) => activeToolNames.has(tool.name))
|
|
138
138
|
.map(({ name, description, parameters }) => ({ name, description, parameters }));
|
|
139
|
-
const auth = await current.modelRegistry.getApiKeyAndHeaders(current.model
|
|
139
|
+
const auth = await current.modelRegistry.getApiKeyAndHeaders(current.model, {
|
|
140
|
+
sessionId: current.sessionManager.getSessionId(),
|
|
141
|
+
});
|
|
140
142
|
if (pingGeneration !== generation) {
|
|
141
143
|
inFlight = false;
|
|
142
144
|
arm();
|
|
@@ -869,7 +869,7 @@ export default function compactionExtension(pi, remoteCompactionDependencies = {
|
|
|
869
869
|
const model = event.model ?? ctx.model;
|
|
870
870
|
if (!isOpenAiRemoteCompactionModel(model))
|
|
871
871
|
return undefined;
|
|
872
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
872
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model, { sessionId: ctx.sessionManager.getSessionId() });
|
|
873
873
|
if (!auth.ok)
|
|
874
874
|
return undefined;
|
|
875
875
|
const effectiveModel = auth.upstreamModelId || auth.baseUrl
|
|
@@ -36,7 +36,9 @@ type OpenAiRemoteCompactionContext = {
|
|
|
36
36
|
getSystemPrompt(): string;
|
|
37
37
|
model: Model<Api> | undefined;
|
|
38
38
|
modelRegistry: {
|
|
39
|
-
getApiKeyAndHeaders(model: Model<Api
|
|
39
|
+
getApiKeyAndHeaders(model: Model<Api>, options?: {
|
|
40
|
+
sessionId?: string;
|
|
41
|
+
}): Promise<{
|
|
40
42
|
ok: true;
|
|
41
43
|
apiKey?: string;
|
|
42
44
|
headers?: ProviderHeaders;
|
|
@@ -335,7 +335,7 @@ export async function runOpenAiRemoteCompaction(ctx, event, emit, dependencies =
|
|
|
335
335
|
});
|
|
336
336
|
return undefined;
|
|
337
337
|
}
|
|
338
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
338
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model, { sessionId: ctx.sessionManager.getSessionId() });
|
|
339
339
|
if (!auth.ok) {
|
|
340
340
|
emit?.({
|
|
341
341
|
version: 1,
|
|
@@ -146,15 +146,16 @@ export async function runExtensionCompaction(context, snapshot, signal, onProgre
|
|
|
146
146
|
// same provider resolution normal turns use; an unconfigured keyed provider still
|
|
147
147
|
// has none of these and is rejected before a request.
|
|
148
148
|
const authenticated = (resolved) => resolved?.ok === true && Boolean(resolved.apiKey || hasCredentialHeaders(resolved.headers) || resolved.ambient);
|
|
149
|
+
const session = { sessionId: context.sessionManager.getSessionId() };
|
|
149
150
|
let selectedModel = context.getCompactionModel?.() ?? snapshot.model;
|
|
150
|
-
let auth = await context.modelRegistry?.getApiKeyAndHeaders(selectedModel);
|
|
151
|
+
let auth = await context.modelRegistry?.getApiKeyAndHeaders(selectedModel, session);
|
|
151
152
|
if (signal?.aborted)
|
|
152
153
|
return undefined;
|
|
153
154
|
// A configured summary model whose credentials could not be confirmed earlier (#830) must not
|
|
154
155
|
// disable compaction: without credentials at request time, summarize on the session model.
|
|
155
156
|
if (selectedModel !== snapshot.model && !authenticated(auth)) {
|
|
156
157
|
selectedModel = snapshot.model;
|
|
157
|
-
auth = await context.modelRegistry?.getApiKeyAndHeaders(selectedModel);
|
|
158
|
+
auth = await context.modelRegistry?.getApiKeyAndHeaders(selectedModel, session);
|
|
158
159
|
if (signal?.aborted)
|
|
159
160
|
return undefined;
|
|
160
161
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stampLogin } from "@earendil-works/pi-ai/auth/pool/login-id";
|
|
1
2
|
import { accountLabel } from "@earendil-works/pi-ai/auth/pool/slots";
|
|
2
3
|
import { describeAccountBlock } from "../../../credential-account-status.js";
|
|
3
4
|
import { emitProviderAccountsChanged } from "../anthropic-subscription/account-events.js";
|
|
@@ -248,7 +249,8 @@ async function importAccount(ctx, deps, source) {
|
|
|
248
249
|
: await importCredential(before, importDeps);
|
|
249
250
|
const existing = new Set(listAccounts(before).map((account) => account.name));
|
|
250
251
|
importedName = listAccounts(updated).find((account) => !existing.has(account.name))?.name;
|
|
251
|
-
|
|
252
|
+
// An import adds a signed-in account, so it stamps a login id like a login (senpi#4099).
|
|
253
|
+
return stampLogin(updated);
|
|
252
254
|
});
|
|
253
255
|
}
|
|
254
256
|
catch (error) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stampLogin } from "@earendil-works/pi-ai/auth/pool/login-id";
|
|
1
2
|
import { emptyCredential, listAccounts, SENTINEL_OAUTH_FIELDS } from "./accounts.js";
|
|
2
3
|
import { CURSOR_CLI_OAUTH_PROVIDER_ID, importNativeCursorCredential } from "./oauth-login.js";
|
|
3
4
|
function asManagedCredential(credential) {
|
|
@@ -31,7 +32,8 @@ async function bootstrapNativeCredential(deps) {
|
|
|
31
32
|
return latest;
|
|
32
33
|
if (latest !== undefined && !latestManaged)
|
|
33
34
|
return latest;
|
|
34
|
-
|
|
35
|
+
// The first-run copy adds a signed-in account, so it stamps a login id like a login (senpi#4099).
|
|
36
|
+
return stampLogin(await importNativeCursorCredential(latestManaged ?? emptyCredential(), async () => native));
|
|
35
37
|
});
|
|
36
38
|
}
|
|
37
39
|
catch {
|
|
@@ -10,7 +10,9 @@ export async function runLookAt(args, signal, ctx, store, dependencies = {}) {
|
|
|
10
10
|
if (!resolved) {
|
|
11
11
|
throw new Error("No image-capable model is available. Configure a vision-capable provider and try look_at again.");
|
|
12
12
|
}
|
|
13
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(resolved.model
|
|
13
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(resolved.model, {
|
|
14
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
15
|
+
});
|
|
14
16
|
if (!auth.ok) {
|
|
15
17
|
throw new Error(`look_at cannot use ${resolved.model.provider}/${resolved.model.id}: ${auth.error}. ` +
|
|
16
18
|
`Configure credentials with /login ${resolved.model.provider} and try again.`);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
|
+
import { oauthErrorHtml, oauthSuccessHtml } from "@earendil-works/pi-ai/utils/oauth-page";
|
|
2
3
|
import { safeTimer } from "../wrap.js";
|
|
3
4
|
import { OAuthFlowError } from "./oauth-errors.js";
|
|
4
5
|
// Chooses the redirect channel for an authorization: a real loopback listener,
|
|
@@ -23,8 +24,6 @@ export async function openCallbackChannel(options) {
|
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
26
|
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
26
|
-
const SUCCESS_HTML = "<!doctype html><title>senpi</title><p>Authorization complete. You can close this tab.</p>";
|
|
27
|
-
const FAILURE_HTML = "<!doctype html><title>senpi</title><p>Authorization failed. Return to senpi and retry.</p>";
|
|
28
27
|
// Lazily-bound 127.0.0.1 listener that exists only for the duration of one
|
|
29
28
|
// OAuth authorization. The handle is unref'd so it never keeps the process
|
|
30
29
|
// alive, and it tears down on success, invalid state, or a 5-minute timeout.
|
|
@@ -105,11 +104,11 @@ export class LoopbackCallbackServer {
|
|
|
105
104
|
const state = url.searchParams.get("state") ?? undefined;
|
|
106
105
|
const error = url.searchParams.get("error");
|
|
107
106
|
if (error !== null || code === null || state === undefined || !this.#options.validateState(state)) {
|
|
108
|
-
res.writeHead(400, { "content-type": "text/html" }).end(
|
|
107
|
+
res.writeHead(400, { "content-type": "text/html" }).end(oauthErrorHtml("MCP server authorization failed. Return to the app and start the sign-in again."));
|
|
109
108
|
void this.#fail(new OAuthFlowError("state_mismatch", `MCP server ${this.#options.serverName} authorization state did not match (possible CSRF or a stale/replayed link); restart the auth flow.`, { serverName: this.#options.serverName }));
|
|
110
109
|
return;
|
|
111
110
|
}
|
|
112
|
-
res.writeHead(200, { "content-type": "text/html" }).end(
|
|
111
|
+
res.writeHead(200, { "content-type": "text/html" }).end(oauthSuccessHtml("MCP server authorization complete. You can close this tab."));
|
|
113
112
|
this.#succeed({ code, state, iss: url.searchParams.get("iss") ?? undefined });
|
|
114
113
|
}
|
|
115
114
|
#succeed(result) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { oauthBrandName } from "@earendil-works/pi-ai/utils/oauth-page";
|
|
2
3
|
import { fingerprintSecret } from "../log.js";
|
|
3
4
|
import { OAuthFlowError } from "./oauth-errors.js";
|
|
4
5
|
import { canonicalAuthorizationServerIssuer, credentialsMatchIssuer, validatedAuthorizationServerIssuer, } from "./oauth-issuer.js";
|
|
@@ -113,7 +114,7 @@ export class McpOAuthProvider {
|
|
|
113
114
|
grant_types: ["authorization_code", "refresh_token"],
|
|
114
115
|
response_types: ["code"],
|
|
115
116
|
token_endpoint_auth_method: "none",
|
|
116
|
-
client_name:
|
|
117
|
+
client_name: oauthBrandName(),
|
|
117
118
|
scope: this.#options.scopes?.join(" "),
|
|
118
119
|
};
|
|
119
120
|
}
|
|
@@ -43,12 +43,20 @@ export declare class ModelRegistry {
|
|
|
43
43
|
/**
|
|
44
44
|
* The live auth check reads the in-memory credentials. When the last read found the store
|
|
45
45
|
* locked they were never loaded, so re-read now (bounded by the sync lock budget) instead
|
|
46
|
-
* of answering from the empty fallback for the rest of the process.
|
|
46
|
+
* of answering from the empty fallback for the rest of the process. A store that could not be
|
|
47
|
+
* loaded is re-read the same way, so a repaired auth.json is seen without a restart (#4413).
|
|
47
48
|
*/
|
|
48
49
|
private retryBusyCredentialRead;
|
|
49
50
|
getUpstreamModelId(model: Model<Api>): string | undefined;
|
|
50
51
|
getServiceTier(model: Model<Api>): "auto" | "flex" | "priority" | "ultrafast" | undefined;
|
|
51
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Request auth for a direct provider call. A caller acting for a session passes its id so the
|
|
54
|
+
* key belongs to the account that session's turns use; without it the stored credential is
|
|
55
|
+
* resolved as for session-less callers.
|
|
56
|
+
*/
|
|
57
|
+
getApiKeyAndHeaders(model: Model<Api>, options?: {
|
|
58
|
+
sessionId?: string;
|
|
59
|
+
}): Promise<ResolvedRequestAuth>;
|
|
52
60
|
getProviderAuthStatus(provider: string): AuthStatus;
|
|
53
61
|
/** `getProviderAuthStatus` plus whether it is a non-stored sign-in, from one resolution (#3537). */
|
|
54
62
|
resolveProviderAuth(provider: string): {
|
|
@@ -59,10 +59,11 @@ export class ModelRegistry {
|
|
|
59
59
|
/**
|
|
60
60
|
* The live auth check reads the in-memory credentials. When the last read found the store
|
|
61
61
|
* locked they were never loaded, so re-read now (bounded by the sync lock budget) instead
|
|
62
|
-
* of answering from the empty fallback for the rest of the process.
|
|
62
|
+
* of answering from the empty fallback for the rest of the process. A store that could not be
|
|
63
|
+
* loaded is re-read the same way, so a repaired auth.json is seen without a restart (#4413).
|
|
63
64
|
*/
|
|
64
65
|
retryBusyCredentialRead() {
|
|
65
|
-
if (this.authStorage.isCredentialStoreBusy())
|
|
66
|
+
if (this.authStorage.isCredentialStoreBusy() || this.authStorage.getLoadError())
|
|
66
67
|
this.authStorage.reload();
|
|
67
68
|
}
|
|
68
69
|
getUpstreamModelId(model) {
|
|
@@ -71,9 +72,16 @@ export class ModelRegistry {
|
|
|
71
72
|
getServiceTier(model) {
|
|
72
73
|
return this.runtime.getCompatibilityRequestConfig(model).serviceTier;
|
|
73
74
|
}
|
|
74
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Request auth for a direct provider call. A caller acting for a session passes its id so the
|
|
77
|
+
* key belongs to the account that session's turns use; without it the stored credential is
|
|
78
|
+
* resolved as for session-less callers.
|
|
79
|
+
*/
|
|
80
|
+
async getApiKeyAndHeaders(model, options = {}) {
|
|
75
81
|
try {
|
|
76
|
-
const resolution =
|
|
82
|
+
const resolution = options.sessionId === undefined
|
|
83
|
+
? await this.runtime.getAuth(model)
|
|
84
|
+
: await this.runtime.getSessionAuth(model, options.sessionId);
|
|
77
85
|
const compatibility = this.runtime.getCompatibilityRequestConfig(model);
|
|
78
86
|
if (!resolution) {
|
|
79
87
|
if (compatibility.authHeader) {
|
|
@@ -168,6 +168,14 @@ export declare class ModelRuntime implements Models {
|
|
|
168
168
|
isFallbackEligible(providerId: string): boolean;
|
|
169
169
|
getAuth(providerId: string, overrides?: ModelRuntimeAuthOverrides): Promise<AuthResult | undefined>;
|
|
170
170
|
getAuth(model: AnyModel, overrides?: ModelRuntimeAuthOverrides): Promise<AuthResult | undefined>;
|
|
171
|
+
/**
|
|
172
|
+
* Auth for a request a session sends outside the rotation stream (compaction, /btw, look-at,
|
|
173
|
+
* cache keepalive). The key must belong to the account the session's own turns use, so a pooled
|
|
174
|
+
* provider resolves the slot the stream would pick for this session; without a pool, or when
|
|
175
|
+
* the stream would not rotate either, it is `getAuth(model)`.
|
|
176
|
+
*/
|
|
177
|
+
getSessionAuth(model: Model<Api>, sessionId: string): Promise<AuthResult | undefined>;
|
|
178
|
+
private sessionSlotAuth;
|
|
171
179
|
private enqueueCredentialOperation;
|
|
172
180
|
private synchronizeCredentialState;
|
|
173
181
|
setRuntimeApiKey(providerId: string, apiKey: string, options?: AuthOperationOptions): Promise<void>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dirname, join } from "node:path";
|
|
2
2
|
import { clampThinkingLevel, createModels, getCurrentTools, isModelType, lazyStream, ModelsError, normalizeContext, normalizeProviderId, providerNotConfiguredMessage, setWireIdentity, wrapStreamWithModelRecovery, } from "@earendil-works/pi-ai";
|
|
3
3
|
import * as builtinProviderCatalog from "@earendil-works/pi-ai/providers/all";
|
|
4
|
+
import { setOAuthPageBrand } from "@earendil-works/pi-ai/utils/oauth-page";
|
|
4
5
|
import { assertChatModel, assertClassifierModel, assertImageModel, classifierErrorResult, imageErrorResult, } from "@earendil-works/pi-ai/utils/model-operations";
|
|
5
6
|
import { APP_NAME, BRAND, getAgentDir } from "../config.js";
|
|
6
7
|
import { operationSignal, raceWithAbortSignal } from "../utils/abort.js";
|
|
@@ -23,6 +24,7 @@ import { createVirtualModel, findLatestResponse, isVirtualModel, withVirtualMode
|
|
|
23
24
|
// The product's identity must ride outgoing requests. This lives here because the AI package
|
|
24
25
|
// is already part of this module's graph; the CLI bootstrap deliberately does not import it.
|
|
25
26
|
setWireIdentity(BRAND?.userAgent ?? APP_NAME);
|
|
27
|
+
setOAuthPageBrand({ name: APP_NAME, ...(BRAND?.logo ? { logo: BRAND.logo } : {}) });
|
|
26
28
|
/** Availability re-reads after a busy credential store: 250, 500, 1000, 2000, 4000 ms. */
|
|
27
29
|
const BUSY_AVAILABILITY_RETRY_LIMIT = 5;
|
|
28
30
|
const BUSY_AVAILABILITY_RETRY_BASE_MS = 250;
|
|
@@ -550,6 +552,42 @@ export class ModelRuntime {
|
|
|
550
552
|
},
|
|
551
553
|
};
|
|
552
554
|
}
|
|
555
|
+
/**
|
|
556
|
+
* Auth for a request a session sends outside the rotation stream (compaction, /btw, look-at,
|
|
557
|
+
* cache keepalive). The key must belong to the account the session's own turns use, so a pooled
|
|
558
|
+
* provider resolves the slot the stream would pick for this session; without a pool, or when
|
|
559
|
+
* the stream would not rotate either, it is `getAuth(model)`.
|
|
560
|
+
*/
|
|
561
|
+
async getSessionAuth(model, sessionId) {
|
|
562
|
+
return this.getAuth(model, (await this.sessionSlotAuth(model, sessionId)) ?? {});
|
|
563
|
+
}
|
|
564
|
+
async sessionSlotAuth(model, sessionId) {
|
|
565
|
+
// The stream's own synchronous admission check, so a provider the stream would not rotate costs
|
|
566
|
+
// nothing beyond getAuth. Remote compaction's before_provider_request hook calls this on every
|
|
567
|
+
// request to such a model; there a pooled provider pays one extra credential read and two slot
|
|
568
|
+
// listings (each reads the pool's health state) before getAuth.
|
|
569
|
+
if (!this.couldRotateCredentials(model, undefined))
|
|
570
|
+
return undefined;
|
|
571
|
+
const sources = await this.credentialRotationSources(model, undefined);
|
|
572
|
+
if (!sources)
|
|
573
|
+
return undefined;
|
|
574
|
+
const { rotation } = await this.loadCredentialPool();
|
|
575
|
+
const { selectSessionSlot } = await import("./credential-pool/session-slot.js");
|
|
576
|
+
const slots = await rotation.listRotationSlots(sources, {
|
|
577
|
+
acquireLeases: false,
|
|
578
|
+
modelId: this.getCompatibilityRequestConfig(model).upstreamModelId ?? model.id,
|
|
579
|
+
});
|
|
580
|
+
const pooled = sources.credential;
|
|
581
|
+
const slot = selectSessionSlot(slots, {
|
|
582
|
+
affinityKey: sessionId,
|
|
583
|
+
useAffinity: sources.policy?.affinity !== false,
|
|
584
|
+
hasher: rotation.sha256SlotHasher,
|
|
585
|
+
...(pooled?.order === undefined ? {} : { order: pooled.order }),
|
|
586
|
+
}, Date.now());
|
|
587
|
+
if (!slot)
|
|
588
|
+
return undefined;
|
|
589
|
+
return slot.lane === "env" ? { apiKey: slot.envKey } : { slotName: slot.name };
|
|
590
|
+
}
|
|
553
591
|
enqueueCredentialOperation(providerId, signal, task) {
|
|
554
592
|
const previous = this.credentialOperations.get(providerId) ?? Promise.resolve();
|
|
555
593
|
let markStarted;
|
|
@@ -14,6 +14,8 @@ export interface ActiveFallbackState {
|
|
|
14
14
|
pinnedByBilling: boolean;
|
|
15
15
|
/** Derived OR of the two provenance flags - the only field consumers read. */
|
|
16
16
|
pinned: boolean;
|
|
17
|
+
/** The original provider's login id when the episode began; a sign-in moves it and releases a billing pin (senpi#4099). */
|
|
18
|
+
recoveryRevision?: string;
|
|
17
19
|
}
|
|
18
20
|
export type FallbackReason = "transient" | "refusal" | "hard-error" | "billing";
|
|
19
21
|
/** Absent: the original recovered after its cooldown. `fallback-unusable`: the fallback itself could not serve. */
|
|
@@ -64,5 +66,10 @@ export interface RetryFallbackControllerDeps {
|
|
|
64
66
|
thinkingLevel?: ThinkingLevel;
|
|
65
67
|
} | undefined;
|
|
66
68
|
isAuthAvailable(provider: string): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* The `loginId` the last sign-in stored for `provider` ("" when none did), never key material;
|
|
71
|
+
* `undefined` when it cannot be read. Only a sign-in writes a new id; a token refresh keeps it or drops it to "" (senpi#4099).
|
|
72
|
+
*/
|
|
73
|
+
recoveryRevision?(provider: string): Promise<string | undefined>;
|
|
67
74
|
}
|
|
68
75
|
//# sourceMappingURL=controller-types.d.ts.map
|
|
@@ -41,6 +41,8 @@ export declare class RetryFallbackController {
|
|
|
41
41
|
* original is not is never where the session rests (senpi#2376). An original that is
|
|
42
42
|
* itself still held (an account usage limit with the reset it named, which can outlast
|
|
43
43
|
* the circuit's 30-minute ceiling) is not restored before that hold ends (senpi#3347).
|
|
44
|
+
* At a turn boundary, a changed credential on the original's provider first releases
|
|
45
|
+
* its billing pin and parks (senpi#4099).
|
|
44
46
|
*/
|
|
45
47
|
maybeRestorePrimary(revertPolicy: "cooldown-expiry" | "never", boundary?: "turn" | "continuation"): Promise<boolean>;
|
|
46
48
|
private originalRecovered;
|
|
@@ -4,6 +4,7 @@ import { firstUsableCandidate } from "./candidates.js";
|
|
|
4
4
|
import { baseSelector, canonicalizeFallbackChains, formatSelector, parseFallbackSelector, resolveChainKey, rungSelector, } from "./chains.js";
|
|
5
5
|
import { subscribeProviderAccountEvents } from "../extensions/builtin/anthropic-subscription/account-events.js";
|
|
6
6
|
import { CircuitProbes } from "./circuit-probes.js";
|
|
7
|
+
import { releaseOnCredentialChange } from "./credential-recovery.js";
|
|
7
8
|
import { pinAfterSwitch } from "./pin.js";
|
|
8
9
|
import { isPureNetworkTransient } from "./transient-network.js";
|
|
9
10
|
import { UnusableEntries } from "./unusable.js";
|
|
@@ -81,6 +82,8 @@ export class RetryFallbackController {
|
|
|
81
82
|
* original is not is never where the session rests (senpi#2376). An original that is
|
|
82
83
|
* itself still held (an account usage limit with the reset it named, which can outlast
|
|
83
84
|
* the circuit's 30-minute ceiling) is not restored before that hold ends (senpi#3347).
|
|
85
|
+
* At a turn boundary, a changed credential on the original's provider first releases
|
|
86
|
+
* its billing pin and parks (senpi#4099).
|
|
84
87
|
*/
|
|
85
88
|
async maybeRestorePrimary(revertPolicy, boundary = "turn") {
|
|
86
89
|
const heldForNetworkBlip = this.heldForNetworkBlip && boundary === "continuation";
|
|
@@ -88,14 +91,16 @@ export class RetryFallbackController {
|
|
|
88
91
|
this.heldForNetworkBlip = false;
|
|
89
92
|
const state = this.state;
|
|
90
93
|
const current = this.deps.getCurrentSelector();
|
|
91
|
-
if (!state ||
|
|
94
|
+
if (!state || !current)
|
|
92
95
|
return false;
|
|
93
96
|
const selector = parseFallbackSelector(state.originalSelector, this.deps.registry);
|
|
94
|
-
|
|
95
|
-
return false;
|
|
96
|
-
const model = this.deps.registry.find(selector.provider, selector.id);
|
|
97
|
+
const model = selector && this.deps.registry.find(selector.provider, selector.id);
|
|
97
98
|
if (!model)
|
|
98
99
|
return false;
|
|
100
|
+
if (boundary === "turn")
|
|
101
|
+
await releaseOnCredentialChange(state, model, this.deps, this.unusable);
|
|
102
|
+
if (state.pinned || !this.deps.isAuthAvailable(model.provider))
|
|
103
|
+
return false;
|
|
99
104
|
const stranded = this.unusable.isUnusable(current.model) && !this.unusable.isUnusable(model);
|
|
100
105
|
if (!stranded &&
|
|
101
106
|
(heldForNetworkBlip || this.unusable.isUnusable(model) || !this.originalRecovered(state, revertPolicy)))
|
|
@@ -313,7 +318,11 @@ export class RetryFallbackController {
|
|
|
313
318
|
const from = formatSelector(current.model);
|
|
314
319
|
const to = formatSelector(candidate.model);
|
|
315
320
|
const prior = this.state;
|
|
321
|
+
const recoveryRevision = prior
|
|
322
|
+
? prior.recoveryRevision
|
|
323
|
+
: await this.deps.recoveryRevision?.(current.model.provider);
|
|
316
324
|
this.state = {
|
|
325
|
+
...(recoveryRevision === undefined ? {} : { recoveryRevision }),
|
|
317
326
|
chainKey: candidate.chainKey,
|
|
318
327
|
originalThinkingLevel: prior?.originalThinkingLevel ?? current.thinkingLevel,
|
|
319
328
|
lastAppliedThinkingLevel: thinking,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ActiveFallbackState, RetryFallbackControllerDeps } from "./controller-types.ts";
|
|
3
|
+
import type { UnusableEntries } from "./unusable.ts";
|
|
4
|
+
type RecoveryDeps = Pick<RetryFallbackControllerDeps, "recoveryRevision" | "cooldowns" | "circuits" | "logger">;
|
|
5
|
+
/**
|
|
6
|
+
* A billing pin assumes the original's account cannot recover by retrying it. A sign-in changes
|
|
7
|
+
* that account, so when the original provider's login id moved since the episode began, the
|
|
8
|
+
* billing pin and the original's cooldown, unusable mark and circuit are released and the
|
|
9
|
+
* existing revert path tries it once. A still-dead original falls back again and pins with the
|
|
10
|
+
* fresh id. This step reads the store at a turn boundary only while a billing pin holds; the
|
|
11
|
+
* other read is `applyCandidate` (controller.ts), once when an episode begins at its first
|
|
12
|
+
* fallback switch, to record the starting id. A token refresh keeps the id or drops it to "", and
|
|
13
|
+
* an id that is gone ("") is no sign-in, so neither releases. Refusal pins hold: a new credential
|
|
14
|
+
* does not change the context that was refused (senpi#4099).
|
|
15
|
+
*/
|
|
16
|
+
export declare function releaseOnCredentialChange(state: ActiveFallbackState, original: Model<Api>, deps: RecoveryDeps, unusable: UnusableEntries): Promise<void>;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=credential-recovery.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { formatSelector } from "./chains.js";
|
|
2
|
+
/**
|
|
3
|
+
* A billing pin assumes the original's account cannot recover by retrying it. A sign-in changes
|
|
4
|
+
* that account, so when the original provider's login id moved since the episode began, the
|
|
5
|
+
* billing pin and the original's cooldown, unusable mark and circuit are released and the
|
|
6
|
+
* existing revert path tries it once. A still-dead original falls back again and pins with the
|
|
7
|
+
* fresh id. This step reads the store at a turn boundary only while a billing pin holds; the
|
|
8
|
+
* other read is `applyCandidate` (controller.ts), once when an episode begins at its first
|
|
9
|
+
* fallback switch, to record the starting id. A token refresh keeps the id or drops it to "", and
|
|
10
|
+
* an id that is gone ("") is no sign-in, so neither releases. Refusal pins hold: a new credential
|
|
11
|
+
* does not change the context that was refused (senpi#4099).
|
|
12
|
+
*/
|
|
13
|
+
export async function releaseOnCredentialChange(state, original, deps, unusable) {
|
|
14
|
+
if (!state.pinnedByBilling || state.recoveryRevision === undefined || !deps.recoveryRevision)
|
|
15
|
+
return;
|
|
16
|
+
const revision = await deps.recoveryRevision(original.provider);
|
|
17
|
+
if (!revision || revision === state.recoveryRevision)
|
|
18
|
+
return;
|
|
19
|
+
state.recoveryRevision = revision;
|
|
20
|
+
state.pinnedByBilling = false;
|
|
21
|
+
state.pinned = state.pinnedByRefusal;
|
|
22
|
+
const selector = formatSelector(original);
|
|
23
|
+
deps.cooldowns.clearModel(selector);
|
|
24
|
+
unusable.clear(original);
|
|
25
|
+
deps.circuits?.close(selector);
|
|
26
|
+
deps.logger.info("credential_recovery_released", {
|
|
27
|
+
chainKey: state.chainKey,
|
|
28
|
+
originalSelector: state.originalSelector,
|
|
29
|
+
trigger: "sign-in",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=credential-recovery.js.map
|