@cortexkit/opencode-openai-auth 0.11.1 → 1.0.0
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/README.md +15 -26
- package/dist/index-766sfrh5.js +1 -0
- package/dist/index-8kj7sa1p.js +12 -0
- package/dist/index-8rqfbhfw.js +221 -0
- package/dist/index-b3q3j39x.js +2 -0
- package/dist/index-bmd7x5aq.js +2 -0
- package/dist/index-eqh53k8v.js +2 -0
- package/dist/{index-sdt2d2ra.js → index-hy9030jh.js} +1 -1
- package/dist/index-jyr8y8k4.js +7 -0
- package/dist/index-rv08jmd4.js +2 -0
- package/dist/index.d.ts +578 -145
- package/dist/index.js +1 -110
- package/dist/pool-lifecycle-DbDmgabd.d.ts +2430 -0
- package/dist/port-file-Do3lQIEk.d.ts +29 -0
- package/dist/protocol-BOBdkqqm.d.ts +198 -0
- package/dist/rpc/port-file.d.ts +4 -17
- package/dist/rpc/port-file.js +1 -1
- package/dist/rpc/protocol.d.ts +2 -2
- package/dist/rpc/protocol.js +1 -0
- package/dist/rpc/rpc-client.d.ts +5 -4
- package/dist/rpc/rpc-client.js +1 -1
- package/dist/rpc/rpc-dir.d.ts +2 -0
- package/dist/rpc/rpc-dir.js +1 -1
- package/dist/sidebar-state-D6cUKgkm.d.ts +385 -0
- package/dist/sidebar-state.d.ts +2 -2
- package/dist/sidebar-state.js +1 -1
- package/dist/token-util-wdza526f.js +1 -0
- package/dist/{token-9823y0pc.js → token-ypk61xj0.js} +1 -1
- package/dist/tui-preferences.js +5 -2
- package/dist/tui.d.ts +9 -4
- package/dist/v2/server.d.ts +54 -0
- package/dist/v2/server.js +4 -0
- package/package.json +21 -9
- package/src/tui/entry.mjs +26 -25
- package/src/tui-compiled/raw/logger.js +24 -0
- package/src/tui-compiled/raw/rpc/protocol.js +3 -0
- package/src/tui-compiled/raw/rpc/rpc-client.js +7 -0
- package/src/tui-compiled/raw/rpc/rpc-dir.js +27 -0
- package/src/tui-compiled/raw/selector.js +21 -0
- package/src/tui-compiled/raw/shared/0332701351b3eeec-format.js +138 -0
- package/src/tui-compiled/raw/shared/04a6357627de5ed0-errors.js +38 -0
- package/src/tui-compiled/raw/shared/053cc17b5419261e-codec.js +12 -0
- package/src/tui-compiled/raw/shared/0766d78c6f481f18-watcher.js +157 -0
- package/src/tui-compiled/raw/shared/08eef45626a94992-reserved.js +13 -0
- package/src/tui-compiled/raw/shared/39c3cb05738fcf6d-projection.js +134 -0
- package/src/tui-compiled/raw/shared/3c2d4169d00f907e-tui-support.js +5 -0
- package/src/tui-compiled/raw/shared/3e7d45a77c6ad4d5-index.js +4 -0
- package/src/tui-compiled/raw/shared/415792e85d80819e-capture-sink.js +13 -0
- package/src/tui-compiled/raw/shared/42ae93ab554fcec2-open-url.js +28 -0
- package/src/tui-compiled/raw/shared/43fc9e6b994519c7-refresh-lock.js +136 -0
- package/src/tui-compiled/raw/shared/458b934e92490331-mutate.js +500 -0
- package/src/tui-compiled/raw/shared/45bf5343bbbf9add-admission.js +142 -0
- package/src/tui-compiled/raw/shared/46f3dff1266160e8-client.js +2 -0
- package/src/tui-compiled/raw/shared/4825e03209d6ae3e-with-lock.js +48 -0
- package/src/tui-compiled/raw/shared/4f36d333d081d040-merge.js +162 -0
- package/src/tui-compiled/raw/shared/54eba94fd64c8c3c-index.js +7 -0
- package/src/tui-compiled/raw/shared/7211cfec06a298bc-atomic-write.js +80 -0
- package/src/tui-compiled/raw/shared/754d6017f8e419cf-builtins.js +534 -0
- package/src/tui-compiled/raw/shared/75bd8c3bd63b315f-identity.js +79 -0
- package/src/tui-compiled/raw/shared/7a91bb630e6f4804-index.js +1 -0
- package/src/tui-compiled/raw/shared/826342a4b6a8b89b-model.js +16 -0
- package/src/tui-compiled/raw/shared/83068c0dbe8a6f13-publication.js +304 -0
- package/src/tui-compiled/raw/shared/836fc615de6afbd9-settings.js +120 -0
- package/src/tui-compiled/raw/shared/83bc3ba9d0fe2761-runtime.js +68 -0
- package/src/tui-compiled/raw/shared/8a18abe4fa2977c8-menu.js +300 -0
- package/src/tui-compiled/raw/shared/901e149c5a0dbbff-sticky.js +339 -0
- package/src/tui-compiled/raw/shared/9020c407ce64f672-ordered.js +58 -0
- package/src/tui-compiled/raw/shared/905bffc0220f43d0-map.js +105 -0
- package/src/tui-compiled/raw/shared/94820e324f2b4c09-seam.js +301 -0
- package/src/tui-compiled/raw/shared/97484d9bb98c705b-provider-state.js +226 -0
- package/src/tui-compiled/raw/shared/9968e3cade6969a1-index.js +2 -0
- package/src/tui-compiled/raw/shared/9b4da614c45f9cd0-lock-constants.js +14 -0
- package/src/tui-compiled/raw/shared/9d3fc9e37456504e-refresh.js +225 -0
- package/src/tui-compiled/raw/shared/9deae3c4da4fd63b-hooks.js +34 -0
- package/src/tui-compiled/raw/shared/a09b6a533c11858f-tui-preferences.js +77 -0
- package/src/tui-compiled/raw/shared/a62a27a204266acd-attribution.js +60 -0
- package/src/tui-compiled/raw/shared/a910277ac43a8626-pull.js +143 -0
- package/src/tui-compiled/raw/shared/b85c5301fddcb98a-paths.js +5 -0
- package/src/tui-compiled/raw/shared/c14988621dc60a75-port-file.js +217 -0
- package/src/tui-compiled/raw/shared/c64d7afbc14ca125-sidebar-file.js +157 -0
- package/src/tui-compiled/raw/shared/c6b176708c446d6d-pins.js +34 -0
- package/src/tui-compiled/raw/shared/c7d252bedee01600-pi.js +206 -0
- package/src/tui-compiled/raw/shared/c852a15cfe489d12-pool.js +120 -0
- package/src/tui-compiled/raw/shared/cc9e0702a752cea7-index.js +3 -0
- package/src/tui-compiled/raw/shared/cd94316a888b0a8e-schema.js +754 -0
- package/src/tui-compiled/raw/shared/d319517e3ea6ad70-redact.js +83 -0
- package/src/tui-compiled/raw/shared/d5a8f1061ee2f114-index.js +5 -0
- package/src/tui-compiled/raw/shared/d669d72c40e9e116-logger.js +19 -0
- package/src/tui-compiled/raw/shared/d7dcba96fea1685c-engine.js +246 -0
- package/src/tui-compiled/raw/shared/dbef697d1df1cd84-index.js +5 -0
- package/src/tui-compiled/raw/shared/e4acc4cb713f300c-protocol.js +3 -0
- package/src/tui-compiled/raw/shared/e8cd4929507508d8-refresh-file-lock.js +501 -0
- package/src/tui-compiled/raw/shared/ec971142548df90e-index.js +6 -0
- package/src/tui-compiled/raw/shared/f37fcefc7aedbf25-error.js +11 -0
- package/src/tui-compiled/raw/shared/fbcdcd0f925e74c0-rpc-client.js +253 -0
- package/src/tui-compiled/raw/shared/fd45e82f2f9c0a0d-torn.js +294 -0
- package/src/tui-compiled/raw/shared/fe02abeadc535f77-rows.js +1258 -0
- package/src/tui-compiled/raw/sidebar-state.js +960 -0
- package/src/tui-compiled/raw/tui/command-dialogs.tsx +424 -0
- package/src/tui-compiled/raw/tui-preferences.js +135 -0
- package/src/tui-compiled/raw/tui.tsx +135 -143
- package/src/tui-compiled/runtime/logger.js +24 -0
- package/src/tui-compiled/runtime/rpc/protocol.js +3 -0
- package/src/tui-compiled/runtime/rpc/rpc-client.js +7 -0
- package/src/tui-compiled/runtime/rpc/rpc-dir.js +27 -0
- package/src/tui-compiled/runtime/selector.js +21 -0
- package/src/tui-compiled/runtime/shared/0332701351b3eeec-format.js +138 -0
- package/src/tui-compiled/runtime/shared/04a6357627de5ed0-errors.js +38 -0
- package/src/tui-compiled/runtime/shared/053cc17b5419261e-codec.js +12 -0
- package/src/tui-compiled/runtime/shared/0766d78c6f481f18-watcher.js +157 -0
- package/src/tui-compiled/runtime/shared/08eef45626a94992-reserved.js +13 -0
- package/src/tui-compiled/runtime/shared/39c3cb05738fcf6d-projection.js +134 -0
- package/src/tui-compiled/runtime/shared/3c2d4169d00f907e-tui-support.js +5 -0
- package/src/tui-compiled/runtime/shared/3e7d45a77c6ad4d5-index.js +4 -0
- package/src/tui-compiled/runtime/shared/415792e85d80819e-capture-sink.js +13 -0
- package/src/tui-compiled/runtime/shared/42ae93ab554fcec2-open-url.js +28 -0
- package/src/tui-compiled/runtime/shared/43fc9e6b994519c7-refresh-lock.js +136 -0
- package/src/tui-compiled/runtime/shared/458b934e92490331-mutate.js +500 -0
- package/src/tui-compiled/runtime/shared/45bf5343bbbf9add-admission.js +142 -0
- package/src/tui-compiled/runtime/shared/46f3dff1266160e8-client.js +2 -0
- package/src/tui-compiled/runtime/shared/4825e03209d6ae3e-with-lock.js +48 -0
- package/src/tui-compiled/runtime/shared/4f36d333d081d040-merge.js +162 -0
- package/src/tui-compiled/runtime/shared/54eba94fd64c8c3c-index.js +7 -0
- package/src/tui-compiled/runtime/shared/7211cfec06a298bc-atomic-write.js +80 -0
- package/src/tui-compiled/runtime/shared/754d6017f8e419cf-builtins.js +534 -0
- package/src/tui-compiled/runtime/shared/75bd8c3bd63b315f-identity.js +79 -0
- package/src/tui-compiled/runtime/shared/7a91bb630e6f4804-index.js +1 -0
- package/src/tui-compiled/runtime/shared/826342a4b6a8b89b-model.js +16 -0
- package/src/tui-compiled/runtime/shared/83068c0dbe8a6f13-publication.js +304 -0
- package/src/tui-compiled/runtime/shared/836fc615de6afbd9-settings.js +120 -0
- package/src/tui-compiled/runtime/shared/83bc3ba9d0fe2761-runtime.js +68 -0
- package/src/tui-compiled/runtime/shared/8a18abe4fa2977c8-menu.js +300 -0
- package/src/tui-compiled/runtime/shared/901e149c5a0dbbff-sticky.js +339 -0
- package/src/tui-compiled/runtime/shared/9020c407ce64f672-ordered.js +58 -0
- package/src/tui-compiled/runtime/shared/905bffc0220f43d0-map.js +105 -0
- package/src/tui-compiled/runtime/shared/94820e324f2b4c09-seam.js +301 -0
- package/src/tui-compiled/runtime/shared/97484d9bb98c705b-provider-state.js +226 -0
- package/src/tui-compiled/runtime/shared/9968e3cade6969a1-index.js +2 -0
- package/src/tui-compiled/runtime/shared/9b4da614c45f9cd0-lock-constants.js +14 -0
- package/src/tui-compiled/runtime/shared/9d3fc9e37456504e-refresh.js +225 -0
- package/src/tui-compiled/runtime/shared/9deae3c4da4fd63b-hooks.js +34 -0
- package/src/tui-compiled/runtime/shared/a09b6a533c11858f-tui-preferences.js +77 -0
- package/src/tui-compiled/runtime/shared/a62a27a204266acd-attribution.js +60 -0
- package/src/tui-compiled/runtime/shared/a910277ac43a8626-pull.js +143 -0
- package/src/tui-compiled/runtime/shared/b85c5301fddcb98a-paths.js +5 -0
- package/src/tui-compiled/runtime/shared/c14988621dc60a75-port-file.js +217 -0
- package/src/tui-compiled/runtime/shared/c64d7afbc14ca125-sidebar-file.js +157 -0
- package/src/tui-compiled/runtime/shared/c6b176708c446d6d-pins.js +34 -0
- package/src/tui-compiled/runtime/shared/c7d252bedee01600-pi.js +206 -0
- package/src/tui-compiled/runtime/shared/c852a15cfe489d12-pool.js +120 -0
- package/src/tui-compiled/runtime/shared/cc9e0702a752cea7-index.js +3 -0
- package/src/tui-compiled/runtime/shared/cd94316a888b0a8e-schema.js +754 -0
- package/src/tui-compiled/runtime/shared/d319517e3ea6ad70-redact.js +83 -0
- package/src/tui-compiled/runtime/shared/d5a8f1061ee2f114-index.js +5 -0
- package/src/tui-compiled/runtime/shared/d669d72c40e9e116-logger.js +19 -0
- package/src/tui-compiled/runtime/shared/d7dcba96fea1685c-engine.js +246 -0
- package/src/tui-compiled/runtime/shared/dbef697d1df1cd84-index.js +5 -0
- package/src/tui-compiled/runtime/shared/e4acc4cb713f300c-protocol.js +3 -0
- package/src/tui-compiled/runtime/shared/e8cd4929507508d8-refresh-file-lock.js +501 -0
- package/src/tui-compiled/runtime/shared/ec971142548df90e-index.js +6 -0
- package/src/tui-compiled/runtime/shared/f37fcefc7aedbf25-error.js +11 -0
- package/src/tui-compiled/runtime/shared/fbcdcd0f925e74c0-rpc-client.js +253 -0
- package/src/tui-compiled/runtime/shared/fd45e82f2f9c0a0d-torn.js +294 -0
- package/src/tui-compiled/runtime/shared/fe02abeadc535f77-rows.js +1258 -0
- package/src/tui-compiled/runtime/sidebar-state.js +960 -0
- package/src/tui-compiled/runtime/tui/command-dialogs.js +378 -0
- package/src/tui-compiled/runtime/tui-preferences.js +135 -0
- package/src/tui-compiled/runtime/{tui.tsx → tui.js} +288 -316
- package/dist/index-4effjmr6.js +0 -2
- package/dist/index-9ehhb051.js +0 -2
- package/dist/index-g3hsdjh4.js +0 -2
- package/dist/index-hh52v8sb.js +0 -2
- package/dist/index-jwn4r3sw.js +0 -1
- package/dist/index-sq36b120.js +0 -211
- package/dist/internal-BTndPmW2.d.ts +0 -914
- package/dist/quota-normalize-08hpp51v.js +0 -1
- package/dist/sidebar-state-CoKYKcLn.d.ts +0 -220
- package/dist/token-util-ypgz7rge.js +0 -1
- package/src/tui-compiled/raw/core-account-paths.ts +0 -114
- package/src/tui-compiled/raw/core-custody-state.ts +0 -243
- package/src/tui-compiled/raw/logger.ts +0 -53
- package/src/tui-compiled/raw/rpc-port-file.ts +0 -177
- package/src/tui-compiled/raw/rpc-protocol.ts +0 -12
- package/src/tui-compiled/raw/rpc-rpc-client.ts +0 -82
- package/src/tui-compiled/raw/rpc-rpc-dir.ts +0 -46
- package/src/tui-compiled/raw/sidebar-state.ts +0 -1659
- package/src/tui-compiled/raw/tui-command-dialogs.tsx +0 -1096
- package/src/tui-compiled/raw/tui-preferences.ts +0 -328
- package/src/tui-compiled/runtime/core-account-paths.ts +0 -114
- package/src/tui-compiled/runtime/core-custody-state.ts +0 -243
- package/src/tui-compiled/runtime/logger.ts +0 -53
- package/src/tui-compiled/runtime/rpc-port-file.ts +0 -177
- package/src/tui-compiled/runtime/rpc-protocol.ts +0 -12
- package/src/tui-compiled/runtime/rpc-rpc-client.ts +0 -82
- package/src/tui-compiled/runtime/rpc-rpc-dir.ts +0 -46
- package/src/tui-compiled/runtime/sidebar-state.ts +0 -1659
- package/src/tui-compiled/runtime/tui-command-dialogs.tsx +0 -855
- package/src/tui-compiled/runtime/tui-preferences.ts +0 -328
- package/src/tui-compiled/shared/internal.ts +0 -6
- package/src/tui-compiled/shared/logger.ts +0 -264
- package/src/tui-compiled/shared/paths.ts +0 -33
- package/src/tui-compiled/shared/refresh-file-lock.ts +0 -384
- package/src/tui-compiled/shared/util/error.ts +0 -9
- package/src/tui-compiled/shared/util/open-url.ts +0 -30
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// One slash command per plugin: the menu a plugin creates once and serves to
|
|
2
|
+
// every invocation. It keeps no per-invocation state; each `open` and `apply`
|
|
3
|
+
// builds its sections afresh from the store and gets its own copy of the
|
|
4
|
+
// caller's context.
|
|
5
|
+
import { createLogger } from "./cc9e0702a752cea7-index.js";
|
|
6
|
+
import { builtinSections, } from "./754d6017f8e419cf-builtins.js";
|
|
7
|
+
import { applyResult, confirmationOf, createTextRedactor, dialogPayload, projectFailure, } from "./94820e324f2b4c09-seam.js";
|
|
8
|
+
const BUILTIN_IDS = new Set([
|
|
9
|
+
'accounts',
|
|
10
|
+
'quota',
|
|
11
|
+
'routing',
|
|
12
|
+
'limits',
|
|
13
|
+
'cache',
|
|
14
|
+
'diagnostics',
|
|
15
|
+
]);
|
|
16
|
+
const STORE_SLOTS = [
|
|
17
|
+
'accounts',
|
|
18
|
+
'quota',
|
|
19
|
+
'routing',
|
|
20
|
+
'limits',
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* A private copy of the caller's context, taken before the first await. Work
|
|
24
|
+
* an invocation leaves running reports through this copy, so a host that
|
|
25
|
+
* reuses and rebinds one context object for the next session cannot pull an
|
|
26
|
+
* earlier invocation's feedback over to it.
|
|
27
|
+
*/
|
|
28
|
+
function ownInvocation(invocation, redact) {
|
|
29
|
+
const { sessionId } = invocation;
|
|
30
|
+
const notify = invocation.notify.bind(invocation);
|
|
31
|
+
// A notification leaves the process just as a payload does, so its text
|
|
32
|
+
// goes through the same redactor.
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
...(sessionId !== undefined ? { sessionId } : {}),
|
|
35
|
+
notify: (message, kind) => kind === undefined
|
|
36
|
+
? notify(redact(String(message)))
|
|
37
|
+
: notify(redact(String(message)), kind),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function coerceOne(knob, raw) {
|
|
41
|
+
switch (knob.kind) {
|
|
42
|
+
case 'choice': {
|
|
43
|
+
const value = raw ?? knob.value;
|
|
44
|
+
if (typeof value === 'string' &&
|
|
45
|
+
knob.choices.some((choice) => choice.value === value))
|
|
46
|
+
return { value };
|
|
47
|
+
return { problem: `Choose one of the options for ${knob.label}.` };
|
|
48
|
+
}
|
|
49
|
+
case 'toggle': {
|
|
50
|
+
const value = raw ?? knob.value;
|
|
51
|
+
return typeof value === 'boolean'
|
|
52
|
+
? { value }
|
|
53
|
+
: { problem: `${knob.label} must be on or off.` };
|
|
54
|
+
}
|
|
55
|
+
case 'number': {
|
|
56
|
+
const value = raw === undefined ? (knob.value ?? null) : raw;
|
|
57
|
+
if (value === null || value === '') {
|
|
58
|
+
return knob.required
|
|
59
|
+
? { problem: `${knob.label} needs a number.` }
|
|
60
|
+
: { value: null };
|
|
61
|
+
}
|
|
62
|
+
const number = typeof value === 'number' ? value : Number(value);
|
|
63
|
+
if (typeof value === 'boolean' || !Number.isFinite(number))
|
|
64
|
+
return { problem: `${knob.label} needs a number.` };
|
|
65
|
+
if (knob.min !== undefined && number < knob.min)
|
|
66
|
+
return { problem: `${knob.label} must be at least ${knob.min}.` };
|
|
67
|
+
if (knob.max !== undefined && number > knob.max)
|
|
68
|
+
return { problem: `${knob.label} must be at most ${knob.max}.` };
|
|
69
|
+
return { value: number };
|
|
70
|
+
}
|
|
71
|
+
case 'text': {
|
|
72
|
+
const value = raw === undefined ? (knob.value ?? null) : raw;
|
|
73
|
+
if (value === null || value === '')
|
|
74
|
+
return knob.required
|
|
75
|
+
? { problem: `${knob.label} cannot be empty.` }
|
|
76
|
+
: { value: null };
|
|
77
|
+
return typeof value === 'string'
|
|
78
|
+
? { value }
|
|
79
|
+
: { problem: `${knob.label} must be text.` };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** The action's inputs, checked against its knobs; unknown names are ignored. */
|
|
84
|
+
function coerceValues(knobs, raw) {
|
|
85
|
+
const values = {};
|
|
86
|
+
for (const knob of knobs) {
|
|
87
|
+
const given = raw && Object.hasOwn(raw, knob.id) ? raw[knob.id] : undefined;
|
|
88
|
+
const result = coerceOne(knob, given);
|
|
89
|
+
if ('problem' in result)
|
|
90
|
+
return result;
|
|
91
|
+
values[knob.id] = result.value;
|
|
92
|
+
}
|
|
93
|
+
return { values };
|
|
94
|
+
}
|
|
95
|
+
function findAction(sections, request) {
|
|
96
|
+
const section = sections.find((entry) => entry.id === request.sectionId);
|
|
97
|
+
if (!section)
|
|
98
|
+
return undefined;
|
|
99
|
+
const actions = request.itemId === undefined
|
|
100
|
+
? section.content.actions
|
|
101
|
+
: section.content.items?.find((item) => item.id === request.itemId)
|
|
102
|
+
?.actions;
|
|
103
|
+
return actions?.find((action) => action.id === request.actionId);
|
|
104
|
+
}
|
|
105
|
+
export function createCommandMenu(options) {
|
|
106
|
+
const logger = options.logger ?? createLogger('commands');
|
|
107
|
+
const redact = createTextRedactor(options.redaction);
|
|
108
|
+
const seam = { logger, redact };
|
|
109
|
+
const now = options.now ?? Date.now;
|
|
110
|
+
const extras = options.extras ?? [];
|
|
111
|
+
const replaced = options.replace ?? {};
|
|
112
|
+
const builtSlots = STORE_SLOTS.filter((slot) => !replaced[slot]);
|
|
113
|
+
for (const slot of STORE_SLOTS) {
|
|
114
|
+
if (replaced[slot] && options[slot] !== undefined)
|
|
115
|
+
throw new Error(`the ${slot} section is replaced; its built-in options cannot also be given`);
|
|
116
|
+
}
|
|
117
|
+
const store = options.store;
|
|
118
|
+
if (builtSlots.length > 0 && !store)
|
|
119
|
+
throw new Error(`a store is required for the built-in ${builtSlots.join(', ')} section${builtSlots.length > 1 ? 's' : ''}`);
|
|
120
|
+
const seen = new Set();
|
|
121
|
+
for (const extra of extras) {
|
|
122
|
+
if (BUILTIN_IDS.has(extra.id) || seen.has(extra.id))
|
|
123
|
+
throw new Error(`extra section id ${extra.id} is taken`);
|
|
124
|
+
seen.add(extra.id);
|
|
125
|
+
}
|
|
126
|
+
async function plugin(id, slot, section, invocation) {
|
|
127
|
+
try {
|
|
128
|
+
return {
|
|
129
|
+
id,
|
|
130
|
+
slot,
|
|
131
|
+
title: section.title,
|
|
132
|
+
content: await section.build(invocation),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
// One broken plugin section must not take the whole menu down.
|
|
137
|
+
logger.warn('command menu section failed to build', {
|
|
138
|
+
command: options.command,
|
|
139
|
+
section: id,
|
|
140
|
+
error: redact(error instanceof Error ? error.message : String(error)),
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
id,
|
|
144
|
+
slot,
|
|
145
|
+
title: section.title,
|
|
146
|
+
content: { lines: ['This section could not be loaded.'] },
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** Every section, in the fixed slot order. */
|
|
151
|
+
async function sections(invocation) {
|
|
152
|
+
// Built-in sections read the store once; a fully replaced menu never
|
|
153
|
+
// touches it.
|
|
154
|
+
const built = store && builtSlots.length > 0
|
|
155
|
+
? await builtinSections({
|
|
156
|
+
store,
|
|
157
|
+
now,
|
|
158
|
+
...(options.extraLocks ? { extraLocks: options.extraLocks } : {}),
|
|
159
|
+
...(options.accounts ? { accounts: options.accounts } : {}),
|
|
160
|
+
...(options.quota ? { quota: options.quota } : {}),
|
|
161
|
+
...(options.routing ? { routing: options.routing } : {}),
|
|
162
|
+
...(options.limits ? { limits: options.limits } : {}),
|
|
163
|
+
})
|
|
164
|
+
: [];
|
|
165
|
+
const out = [];
|
|
166
|
+
for (const slot of STORE_SLOTS) {
|
|
167
|
+
const replacement = replaced[slot];
|
|
168
|
+
if (replacement)
|
|
169
|
+
out.push(await plugin(slot, slot, replacement, invocation));
|
|
170
|
+
else {
|
|
171
|
+
const section = built.find((entry) => entry.slot === slot);
|
|
172
|
+
if (section)
|
|
173
|
+
out.push(section);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (options.cache)
|
|
177
|
+
out.push(await plugin('cache', 'cache', options.cache, invocation));
|
|
178
|
+
if (options.diagnostics)
|
|
179
|
+
out.push(await plugin('diagnostics', 'diagnostics', options.diagnostics, invocation));
|
|
180
|
+
for (const extra of extras)
|
|
181
|
+
out.push(await plugin(extra.id, 'extra', extra, invocation));
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
command: options.command,
|
|
186
|
+
async open(invocation) {
|
|
187
|
+
const own = ownInvocation(invocation, redact);
|
|
188
|
+
return dialogPayload(options.command, options.title, await sections(own), seam);
|
|
189
|
+
},
|
|
190
|
+
async apply(request, invocation) {
|
|
191
|
+
const own = ownInvocation(invocation, redact);
|
|
192
|
+
const finish = async (outcome) => applyResult(options.command, options.title, await sections(own), outcome, seam);
|
|
193
|
+
const action = request.command === options.command
|
|
194
|
+
? findAction(await sections(own), request)
|
|
195
|
+
: undefined;
|
|
196
|
+
if (!action)
|
|
197
|
+
return finish({
|
|
198
|
+
ok: false,
|
|
199
|
+
code: 'unavailable',
|
|
200
|
+
text: 'That action is no longer available.',
|
|
201
|
+
});
|
|
202
|
+
const confirmation = confirmationOf(action);
|
|
203
|
+
if (confirmation && request.confirmed !== true)
|
|
204
|
+
return finish({
|
|
205
|
+
ok: false,
|
|
206
|
+
code: 'needs-confirmation',
|
|
207
|
+
text: confirmation.message,
|
|
208
|
+
needsConfirmation: true,
|
|
209
|
+
});
|
|
210
|
+
const coerced = coerceValues(action.knobs ?? [], request.values);
|
|
211
|
+
if ('problem' in coerced)
|
|
212
|
+
return finish({
|
|
213
|
+
ok: false,
|
|
214
|
+
code: 'invalid-input',
|
|
215
|
+
text: coerced.problem,
|
|
216
|
+
});
|
|
217
|
+
let outcome;
|
|
218
|
+
try {
|
|
219
|
+
const result = await action.run({
|
|
220
|
+
values: coerced.values,
|
|
221
|
+
...(request.itemId !== undefined ? { itemId: request.itemId } : {}),
|
|
222
|
+
invocation: own,
|
|
223
|
+
});
|
|
224
|
+
outcome =
|
|
225
|
+
typeof result === 'string'
|
|
226
|
+
? { ok: true, text: result }
|
|
227
|
+
: {
|
|
228
|
+
ok: result.ok,
|
|
229
|
+
text: result.text,
|
|
230
|
+
...(result.ok ? {} : { code: result.code ?? 'refused' }),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
// The raw text goes only to the log (redacted); the user sees the
|
|
235
|
+
// projected code and message, never the exception's own words.
|
|
236
|
+
const failure = projectFailure(error);
|
|
237
|
+
logger.warn('command menu action failed', {
|
|
238
|
+
command: options.command,
|
|
239
|
+
section: request.sectionId,
|
|
240
|
+
action: request.actionId,
|
|
241
|
+
code: failure.code,
|
|
242
|
+
error: redact(error instanceof Error ? error.message : String(error)),
|
|
243
|
+
});
|
|
244
|
+
outcome = { ok: false, ...failure };
|
|
245
|
+
}
|
|
246
|
+
return finish(outcome);
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function isKnobValue(value) {
|
|
251
|
+
return (value === null ||
|
|
252
|
+
typeof value === 'string' ||
|
|
253
|
+
typeof value === 'boolean' ||
|
|
254
|
+
(typeof value === 'number' && Number.isFinite(value)));
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Checks an apply request that arrived over the loopback RPC; undefined when
|
|
258
|
+
* it is not one. Only the request's own fields are kept.
|
|
259
|
+
*/
|
|
260
|
+
export function parseApplyRequest(value) {
|
|
261
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
262
|
+
return undefined;
|
|
263
|
+
const raw = value;
|
|
264
|
+
if (typeof raw.command !== 'string' ||
|
|
265
|
+
typeof raw.sectionId !== 'string' ||
|
|
266
|
+
typeof raw.actionId !== 'string')
|
|
267
|
+
return undefined;
|
|
268
|
+
if (raw.itemId !== undefined && typeof raw.itemId !== 'string')
|
|
269
|
+
return undefined;
|
|
270
|
+
if (raw.sessionId !== undefined && typeof raw.sessionId !== 'string')
|
|
271
|
+
return undefined;
|
|
272
|
+
if (raw.confirmed !== undefined && typeof raw.confirmed !== 'boolean')
|
|
273
|
+
return undefined;
|
|
274
|
+
const values = {};
|
|
275
|
+
if (raw.values !== undefined) {
|
|
276
|
+
if (raw.values === null ||
|
|
277
|
+
typeof raw.values !== 'object' ||
|
|
278
|
+
Array.isArray(raw.values))
|
|
279
|
+
return undefined;
|
|
280
|
+
for (const [name, entry] of Object.entries(raw.values)) {
|
|
281
|
+
if (!isKnobValue(entry))
|
|
282
|
+
return undefined;
|
|
283
|
+
Object.defineProperty(values, name, {
|
|
284
|
+
value: entry,
|
|
285
|
+
enumerable: true,
|
|
286
|
+
writable: true,
|
|
287
|
+
configurable: true,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
command: raw.command,
|
|
293
|
+
sectionId: raw.sectionId,
|
|
294
|
+
actionId: raw.actionId,
|
|
295
|
+
...(typeof raw.itemId === 'string' ? { itemId: raw.itemId } : {}),
|
|
296
|
+
...(raw.values !== undefined ? { values } : {}),
|
|
297
|
+
...(raw.confirmed === true ? { confirmed: true } : {}),
|
|
298
|
+
...(typeof raw.sessionId === 'string' ? { sessionId: raw.sessionId } : {}),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
// `sticky-balanced` routing: openai-auth's quota-weighted session placement
|
|
2
|
+
// and pin-break classification, judged over the quota projection rather than
|
|
3
|
+
// openai-auth's fixed primary/secondary snapshot.
|
|
4
|
+
import { budgetExhaustedResetAt, projectQuota, } from "./39c3cb05738fcf6d-projection.js";
|
|
5
|
+
import { admit, } from "./45bf5343bbbf9add-admission.js";
|
|
6
|
+
import { isPinValid } from "./c6b176708c446d6d-pins.js";
|
|
7
|
+
export const QUOTA_STALENESS_MS = 15 * 60_000;
|
|
8
|
+
export const MIN_RESET_HOURS = 1 / 60;
|
|
9
|
+
export const MIN_WEIGHT = 1e-6;
|
|
10
|
+
/** The projection's time, else the caller's cache-entry time. */
|
|
11
|
+
export function snapshotCheckedAt(quota, entryCheckedAt) {
|
|
12
|
+
for (const checkedAt of [quota?.checkedAt, entryCheckedAt]) {
|
|
13
|
+
if (typeof checkedAt === 'number' && Number.isFinite(checkedAt)) {
|
|
14
|
+
return checkedAt;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The status rule used when the adapter supplies none, carried from
|
|
21
|
+
* openai-auth: 401 and 403 are permanent; no response, 0, a non-finite
|
|
22
|
+
* status, 429 and 5xx are transient; anything else leaves the row healthy.
|
|
23
|
+
* A provider whose 403 can mean an organisation or model policy rather than
|
|
24
|
+
* a dead account supplies its own classifier and can fall back to this one
|
|
25
|
+
* for the statuses it does not single out.
|
|
26
|
+
*/
|
|
27
|
+
export function defaultStickyStatusClass(status) {
|
|
28
|
+
if (status === 401 || status === 403)
|
|
29
|
+
return 'permanent';
|
|
30
|
+
if (status === undefined ||
|
|
31
|
+
status === 0 ||
|
|
32
|
+
!Number.isFinite(status) ||
|
|
33
|
+
(status >= 500 && status <= 599) ||
|
|
34
|
+
status === 429) {
|
|
35
|
+
return 'transient';
|
|
36
|
+
}
|
|
37
|
+
return 'healthy';
|
|
38
|
+
}
|
|
39
|
+
function windowReadings(quota) {
|
|
40
|
+
// Every reading is an independent constraint, so all of them are judged:
|
|
41
|
+
// a third window (for example a short window beside two weekly ones) can
|
|
42
|
+
// be the one that is nearly spent. Longest known window first, unknown
|
|
43
|
+
// lengths last, as openai-auth sorts, so a break decision with several
|
|
44
|
+
// spent windows names the longest. Tombstones and absence records carry no
|
|
45
|
+
// capacity figure and are left out.
|
|
46
|
+
return quota.limits
|
|
47
|
+
.filter((limit) => limit.kind === 'reading')
|
|
48
|
+
.sort((left, right) => {
|
|
49
|
+
const leftKnown = left.windowMinutes !== undefined;
|
|
50
|
+
const rightKnown = right.windowMinutes !== undefined;
|
|
51
|
+
if (leftKnown !== rightKnown)
|
|
52
|
+
return leftKnown ? -1 : 1;
|
|
53
|
+
if (leftKnown && rightKnown) {
|
|
54
|
+
return (right.windowMinutes ?? 0) - (left.windowMinutes ?? 0);
|
|
55
|
+
}
|
|
56
|
+
return 0;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** Classifies whether a pinned session should leave its row after a failure. */
|
|
60
|
+
export function decideStickyBreak(input) {
|
|
61
|
+
const statusClass = (input.classifyStatus ?? defaultStickyStatusClass)(input.status);
|
|
62
|
+
if (statusClass === 'permanent') {
|
|
63
|
+
return { action: 'migrate', reason: 'permanent' };
|
|
64
|
+
}
|
|
65
|
+
if (!input.quota)
|
|
66
|
+
return { action: 'retain', reason: 'unknown' };
|
|
67
|
+
const checkedAt = snapshotCheckedAt(input.quota, input.quotaCheckedAt);
|
|
68
|
+
if (checkedAt === undefined ||
|
|
69
|
+
!Number.isFinite(checkedAt) ||
|
|
70
|
+
input.now - checkedAt > QUOTA_STALENESS_MS) {
|
|
71
|
+
return { action: 'retain', reason: 'stale' };
|
|
72
|
+
}
|
|
73
|
+
// After the stale check, so a stale snapshot never judges the account on a
|
|
74
|
+
// reading the killswitch would consider below its floor.
|
|
75
|
+
if (input.killswitchPasses === false) {
|
|
76
|
+
return { action: 'migrate', reason: 'killswitch' };
|
|
77
|
+
}
|
|
78
|
+
for (const limit of windowReadings(input.quota)) {
|
|
79
|
+
const remaining = limit.remainingPercent;
|
|
80
|
+
if (typeof remaining === 'number' &&
|
|
81
|
+
Number.isFinite(remaining) &&
|
|
82
|
+
remaining <= 0) {
|
|
83
|
+
return {
|
|
84
|
+
action: 'migrate',
|
|
85
|
+
reason: 'exhausted',
|
|
86
|
+
window: { scope: limit.scope, label: limit.label },
|
|
87
|
+
...(typeof limit.resetsAt === 'string'
|
|
88
|
+
? { resetsAt: limit.resetsAt }
|
|
89
|
+
: {}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// A reached credit budget is exhaustion on its own axis, judged by the same
|
|
94
|
+
// signal admission uses so the two never disagree on what "spent" means.
|
|
95
|
+
const budgetReset = budgetExhaustedResetAt(input.quota, input.now);
|
|
96
|
+
if (budgetReset) {
|
|
97
|
+
return {
|
|
98
|
+
action: 'migrate',
|
|
99
|
+
reason: 'exhausted',
|
|
100
|
+
resetsAt: budgetReset.resetsAt,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return { action: 'retain', reason: statusClass };
|
|
104
|
+
}
|
|
105
|
+
export function sustainableWindowWeight(window, reservePercent, now) {
|
|
106
|
+
const spendable = Math.max(0, window.remainingPercent - reservePercent);
|
|
107
|
+
if (spendable <= 0)
|
|
108
|
+
return 0;
|
|
109
|
+
if (!window.resetsAt)
|
|
110
|
+
return spendable;
|
|
111
|
+
const resetMs = Date.parse(window.resetsAt);
|
|
112
|
+
// A lapsed reset cannot yield a spend rate: the divisor would clamp to
|
|
113
|
+
// MIN_RESET_HOURS and inflate the weight about sixty-fold on stale
|
|
114
|
+
// information, so the un-rate-adjusted spendable capacity is used instead.
|
|
115
|
+
if (!Number.isFinite(resetMs) || resetMs <= now)
|
|
116
|
+
return spendable;
|
|
117
|
+
const hours = Math.max((resetMs - now) / 3_600_000, MIN_RESET_HOURS);
|
|
118
|
+
return spendable / hours;
|
|
119
|
+
}
|
|
120
|
+
function compareAccountIds(left, right) {
|
|
121
|
+
if (left < right)
|
|
122
|
+
return -1;
|
|
123
|
+
if (left > right)
|
|
124
|
+
return 1;
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
function candidateWeight(candidate, now) {
|
|
128
|
+
if (!candidate.quota)
|
|
129
|
+
return undefined;
|
|
130
|
+
const quotaCheckedAt = snapshotCheckedAt(candidate.quota, candidate.quotaCheckedAt);
|
|
131
|
+
if (quotaCheckedAt === undefined ||
|
|
132
|
+
now - quotaCheckedAt > QUOTA_STALENESS_MS) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
// Missing reserve data must leave a window usable rather than silently
|
|
136
|
+
// excluding its account.
|
|
137
|
+
// The account is as constrained as its tightest window, whichever it is.
|
|
138
|
+
const weights = windowReadings(candidate.quota).map((limit) => sustainableWindowWeight({
|
|
139
|
+
remainingPercent: limit.remainingPercent ?? Number.NaN,
|
|
140
|
+
...(limit.resetsAt === undefined ? {} : { resetsAt: limit.resetsAt }),
|
|
141
|
+
}, candidate.reservePercent[limit.label] ?? 0, now));
|
|
142
|
+
// The credit budget is a third pressure axis on its own reset clock. It has
|
|
143
|
+
// no configured reserve, and a malformed reading is ignored rather than
|
|
144
|
+
// allowed to zero the account's weight.
|
|
145
|
+
const budget = candidate.quota.budget;
|
|
146
|
+
if (budget &&
|
|
147
|
+
typeof budget.remainingPercent === 'number' &&
|
|
148
|
+
Number.isFinite(budget.remainingPercent)) {
|
|
149
|
+
weights.push(sustainableWindowWeight({
|
|
150
|
+
remainingPercent: budget.remainingPercent,
|
|
151
|
+
...(budget.resetsAt === undefined
|
|
152
|
+
? {}
|
|
153
|
+
: { resetsAt: budget.resetsAt }),
|
|
154
|
+
}, 0, now));
|
|
155
|
+
}
|
|
156
|
+
const weight = weights.length > 0 ? Math.min(...weights) : 0;
|
|
157
|
+
return weight > 0 ? { candidate, quotaCheckedAt, weight } : undefined;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Places a session: the lowest projected pressure among candidates with a
|
|
161
|
+
* fresh positive weight, else (`mode-fallback`) the first candidate in
|
|
162
|
+
* configured order, preferring one with an applicable reset credit.
|
|
163
|
+
*/
|
|
164
|
+
export function selectStickyCandidate(input) {
|
|
165
|
+
// A candidate killed by the killswitch is excluded from BOTH weighted
|
|
166
|
+
// placement and the fallback branch, which must never become a way to
|
|
167
|
+
// spend on a killed account.
|
|
168
|
+
const eligibleCandidates = input.candidates.filter((candidate) => candidate.killswitchPasses !== false);
|
|
169
|
+
if (input.candidates.length === 0) {
|
|
170
|
+
throw new Error('Cannot select a sticky candidate: input.candidates is empty');
|
|
171
|
+
}
|
|
172
|
+
if (eligibleCandidates.length === 0)
|
|
173
|
+
return undefined;
|
|
174
|
+
const weighted = eligibleCandidates
|
|
175
|
+
.map((candidate) => candidateWeight(candidate, input.now))
|
|
176
|
+
.filter((candidate) => candidate !== undefined);
|
|
177
|
+
if (weighted.length > 0) {
|
|
178
|
+
weighted.sort((left, right) => {
|
|
179
|
+
// MIN_WEIGHT only guards the division; every weight here is positive.
|
|
180
|
+
const leftScore = ((input.pendingBytes.get(left.candidate.accountId) ?? 0) +
|
|
181
|
+
input.requestBytes) /
|
|
182
|
+
Math.max(left.weight, MIN_WEIGHT);
|
|
183
|
+
const rightScore = ((input.pendingBytes.get(right.candidate.accountId) ?? 0) +
|
|
184
|
+
input.requestBytes) /
|
|
185
|
+
Math.max(right.weight, MIN_WEIGHT);
|
|
186
|
+
return (leftScore - rightScore ||
|
|
187
|
+
left.candidate.configuredOrder - right.candidate.configuredOrder ||
|
|
188
|
+
compareAccountIds(left.candidate.accountId, right.candidate.accountId));
|
|
189
|
+
});
|
|
190
|
+
const selected = weighted[0];
|
|
191
|
+
if (selected) {
|
|
192
|
+
return {
|
|
193
|
+
accountId: selected.candidate.accountId,
|
|
194
|
+
quotaCheckedAt: selected.quotaCheckedAt,
|
|
195
|
+
source: 'weighted',
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
input.onEmptyWeightedSet?.();
|
|
200
|
+
const fallback = [...eligibleCandidates].sort((left, right) => {
|
|
201
|
+
const leftHasCredits = (left.resetCreditsApplicable ?? 0) > 0 ? 1 : 0;
|
|
202
|
+
const rightHasCredits = (right.resetCreditsApplicable ?? 0) > 0 ? 1 : 0;
|
|
203
|
+
return (rightHasCredits - leftHasCredits ||
|
|
204
|
+
left.configuredOrder - right.configuredOrder ||
|
|
205
|
+
compareAccountIds(left.accountId, right.accountId));
|
|
206
|
+
})[0];
|
|
207
|
+
if (!fallback) {
|
|
208
|
+
throw new Error('Cannot select a sticky candidate: input.candidates is empty');
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
accountId: fallback.accountId,
|
|
212
|
+
quotaCheckedAt: snapshotCheckedAt(fallback.quota, fallback.quotaCheckedAt),
|
|
213
|
+
source: 'mode-fallback',
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Routes one request in `sticky-balanced` mode. A valid pin whose row is
|
|
218
|
+
* admitted, not excluded and not killed is dispatched as is. Otherwise
|
|
219
|
+
* selection runs over the non-excluded rows; each selected row admission
|
|
220
|
+
* refused is removed and selection re-runs, so the loop ends either on an
|
|
221
|
+
* admitted row or with no admissible account.
|
|
222
|
+
*/
|
|
223
|
+
export function routeSticky(input) {
|
|
224
|
+
const admission = admit(input);
|
|
225
|
+
const admitted = new Map(admission.admitted.map((row) => [row.id, row]));
|
|
226
|
+
const refusals = new Map(admission.refused.map((r) => [r.id, r]));
|
|
227
|
+
const excluded = new Set(admission.excluded.map((row) => row.id));
|
|
228
|
+
const validIds = new Set(input.rows.map((row) => row.id));
|
|
229
|
+
const pinValid = input.pin !== undefined &&
|
|
230
|
+
isPinValid(input.pin, validIds, input.identities?.get(input.pin.accountId));
|
|
231
|
+
if (pinValid && input.pin) {
|
|
232
|
+
const pinned = admitted.get(input.pin.accountId);
|
|
233
|
+
if (pinned && input.killswitch?.get(pinned.id) !== false) {
|
|
234
|
+
return {
|
|
235
|
+
outcome: 'dispatch',
|
|
236
|
+
accountId: pinned.id,
|
|
237
|
+
source: 'pin',
|
|
238
|
+
...(pinned.projection?.checkedAt === undefined
|
|
239
|
+
? {}
|
|
240
|
+
: { quotaCheckedAt: pinned.projection.checkedAt }),
|
|
241
|
+
pin: { action: 'retain' },
|
|
242
|
+
refusedSelections: [],
|
|
243
|
+
admission,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// A spent window with a future reset, a spent credit budget and a killswitch
|
|
248
|
+
// verdict all say the pinned row will not serve until some known later time,
|
|
249
|
+
// so the pin may move. A row refused for want of a usable reading, or
|
|
250
|
+
// excluded by a short rate-limit mark or refresh backoff, may serve again on
|
|
251
|
+
// the next reading, so its pin stays.
|
|
252
|
+
const pinRefusal = input.pin ? refusals.get(input.pin.accountId) : undefined;
|
|
253
|
+
const pinMoves = pinValid &&
|
|
254
|
+
input.pin !== undefined &&
|
|
255
|
+
input.refusedPinPolicy === 'move-on-confirmed-exhaustion' &&
|
|
256
|
+
(input.killswitch?.get(input.pin.accountId) === false ||
|
|
257
|
+
pinRefusal?.reason === 'exhausted' ||
|
|
258
|
+
pinRefusal?.reason === 'budget-spent');
|
|
259
|
+
const reserveFor = (row) => {
|
|
260
|
+
const perRow = typeof input.rowReservePercent === 'function'
|
|
261
|
+
? input.rowReservePercent(row)
|
|
262
|
+
: input.rowReservePercent?.get(row.id);
|
|
263
|
+
return perRow ?? input.reservePercent ?? {};
|
|
264
|
+
};
|
|
265
|
+
const scope = input.scope;
|
|
266
|
+
let candidates = input.rows
|
|
267
|
+
.map((row, configuredOrder) => ({ row, configuredOrder }))
|
|
268
|
+
.filter(({ row }) => !excluded.has(row.id))
|
|
269
|
+
.map(({ row, configuredOrder }) => {
|
|
270
|
+
const killswitchPasses = input.killswitch?.get(row.id);
|
|
271
|
+
const credits = input.resetCreditsApplicable?.get(row.id);
|
|
272
|
+
return {
|
|
273
|
+
accountId: row.id,
|
|
274
|
+
quota: row.kind === 'api-key'
|
|
275
|
+
? undefined
|
|
276
|
+
: (admitted.get(row.id)?.projection ??
|
|
277
|
+
projectQuota(row.quota, scope)),
|
|
278
|
+
reservePercent: reserveFor(row),
|
|
279
|
+
configuredOrder,
|
|
280
|
+
...(credits === undefined ? {} : { resetCreditsApplicable: credits }),
|
|
281
|
+
...(killswitchPasses === undefined ? {} : { killswitchPasses }),
|
|
282
|
+
};
|
|
283
|
+
});
|
|
284
|
+
const refusedSelections = [];
|
|
285
|
+
const unplaced = pinValid
|
|
286
|
+
? { action: 'retain' }
|
|
287
|
+
: input.pin
|
|
288
|
+
? { action: 'clear' }
|
|
289
|
+
: { action: 'none' };
|
|
290
|
+
while (candidates.length > 0) {
|
|
291
|
+
const selection = selectStickyCandidate({
|
|
292
|
+
candidates,
|
|
293
|
+
pendingBytes: input.pendingBytes ?? new Map(),
|
|
294
|
+
requestBytes: input.requestBytes,
|
|
295
|
+
now: input.now,
|
|
296
|
+
...(input.onEmptyWeightedSet
|
|
297
|
+
? { onEmptyWeightedSet: input.onEmptyWeightedSet }
|
|
298
|
+
: {}),
|
|
299
|
+
});
|
|
300
|
+
if (!selection)
|
|
301
|
+
break;
|
|
302
|
+
const refusal = refusals.get(selection.accountId);
|
|
303
|
+
if (refusal) {
|
|
304
|
+
refusedSelections.push(refusal);
|
|
305
|
+
candidates = candidates.filter((candidate) => candidate.accountId !== selection.accountId);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const identity = input.identities?.get(selection.accountId);
|
|
309
|
+
return {
|
|
310
|
+
outcome: 'dispatch',
|
|
311
|
+
accountId: selection.accountId,
|
|
312
|
+
source: selection.source,
|
|
313
|
+
...(selection.quotaCheckedAt === undefined
|
|
314
|
+
? {}
|
|
315
|
+
: { quotaCheckedAt: selection.quotaCheckedAt }),
|
|
316
|
+
pin: pinValid && !pinMoves
|
|
317
|
+
? { action: 'retain' }
|
|
318
|
+
: {
|
|
319
|
+
action: 'assign',
|
|
320
|
+
pin: {
|
|
321
|
+
accountId: selection.accountId,
|
|
322
|
+
inputBytes: input.requestBytes,
|
|
323
|
+
...(identity === undefined ? {} : { wireIdentity: identity }),
|
|
324
|
+
...(selection.quotaCheckedAt === undefined
|
|
325
|
+
? {}
|
|
326
|
+
: { quotaCheckedAt: selection.quotaCheckedAt }),
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
refusedSelections,
|
|
330
|
+
admission,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
outcome: 'no-admissible-account',
|
|
335
|
+
pin: unplaced,
|
|
336
|
+
refusedSelections,
|
|
337
|
+
admission,
|
|
338
|
+
};
|
|
339
|
+
}
|