@thelioo/opencode-balancer 0.1.8 → 0.2.1
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/INSTALL.txt +53 -25
- package/README.md +95 -51
- package/dist/core/accounts.ts +404 -0
- package/dist/core/database.ts +67 -0
- package/dist/core/events.ts +75 -0
- package/dist/core/native-auth-suppression.ts +36 -0
- package/dist/core/native-connect.ts +31 -0
- package/dist/core/path.ts +34 -0
- package/dist/core/pending.ts +351 -0
- package/dist/core/priority.ts +193 -0
- package/dist/core/schema.ts +439 -0
- package/dist/core/time.ts +3 -0
- package/dist/core/types.ts +72 -0
- package/dist/core/usage/index.ts +23 -0
- package/dist/core/usage/providers/copilot.ts +243 -0
- package/dist/core/usage/providers/openai.ts +179 -0
- package/dist/core/usage/redact.ts +80 -0
- package/dist/core/usage/store.ts +66 -0
- package/dist/core/usage/types.ts +24 -0
- package/dist/index.js +173 -4
- package/dist/index.js.map +1 -1
- package/dist/server/auth-watcher.ts +318 -0
- package/dist/server/commands.ts +58 -0
- package/dist/server/fetch-patch.ts +162 -0
- package/dist/server/index.ts +134 -0
- package/dist/server/native.ts +49 -0
- package/dist/server/request-balancer.ts +67 -0
- package/dist/tui/actions.ts +176 -112
- package/dist/tui/balancer-bar-sync.ts +55 -45
- package/dist/tui/components/alias-dialog.tsx +71 -56
- package/dist/tui/components/dashboard.tsx +530 -358
- package/dist/tui/components/priority-screen.tsx +389 -267
- package/dist/tui/components/provider-model-dialog.tsx +71 -64
- package/dist/tui/components/rename-dialog.tsx +35 -28
- package/dist/tui/components/sidebar.tsx +103 -79
- package/dist/tui/components/status-indicator.tsx +78 -59
- package/dist/tui/components/usage-bar.tsx +18 -7
- package/dist/tui/components/usage-display.tsx +32 -16
- package/dist/tui/connect.ts +104 -73
- package/dist/tui/dashboard-keys.ts +53 -41
- package/dist/tui/native-model-apply.ts +45 -36
- package/dist/tui/priority-keys.ts +44 -36
- package/dist/tui/provider-models.ts +32 -25
- package/dist/tui/responsive.ts +10 -7
- package/dist/tui/selected-account-bar-sync.ts +23 -23
- package/dist/tui/selection-colors.ts +38 -30
- package/dist/tui/state.ts +61 -44
- package/dist/tui/status-format.ts +24 -20
- package/dist/tui/tui.js +165 -153
- package/dist/tui/tui.js.map +1 -1
- package/dist/tui/tui.tsx +194 -144
- package/dist/tui/usage-auto-refresh.ts +52 -45
- package/dist/tui/usage-format.ts +9 -9
- package/package.json +61 -52
- package/dist/core/accounts.d.ts +0 -14
- package/dist/core/accounts.js +0 -260
- package/dist/core/accounts.js.map +0 -1
- package/dist/core/database.d.ts +0 -4
- package/dist/core/database.js +0 -69
- package/dist/core/database.js.map +0 -1
- package/dist/core/events.d.ts +0 -18
- package/dist/core/events.js +0 -39
- package/dist/core/events.js.map +0 -1
- package/dist/core/native-auth-suppression.d.ts +0 -3
- package/dist/core/native-auth-suppression.js +0 -19
- package/dist/core/native-auth-suppression.js.map +0 -1
- package/dist/core/native-connect.d.ts +0 -4
- package/dist/core/native-connect.js +0 -19
- package/dist/core/native-connect.js.map +0 -1
- package/dist/core/path.d.ts +0 -4
- package/dist/core/path.js +0 -26
- package/dist/core/path.js.map +0 -1
- package/dist/core/pending.d.ts +0 -9
- package/dist/core/pending.js +0 -237
- package/dist/core/pending.js.map +0 -1
- package/dist/core/priority.d.ts +0 -20
- package/dist/core/priority.js +0 -120
- package/dist/core/priority.js.map +0 -1
- package/dist/core/schema.d.ts +0 -2
- package/dist/core/schema.js +0 -265
- package/dist/core/schema.js.map +0 -1
- package/dist/core/time.d.ts +0 -1
- package/dist/core/time.js +0 -4
- package/dist/core/time.js.map +0 -1
- package/dist/core/types.d.ts +0 -59
- package/dist/core/types.js +0 -2
- package/dist/core/types.js.map +0 -1
- package/dist/core/usage/index.d.ts +0 -4
- package/dist/core/usage/index.js +0 -16
- package/dist/core/usage/index.js.map +0 -1
- package/dist/core/usage/providers/copilot.d.ts +0 -2
- package/dist/core/usage/providers/copilot.js +0 -169
- package/dist/core/usage/providers/copilot.js.map +0 -1
- package/dist/core/usage/providers/openai.d.ts +0 -2
- package/dist/core/usage/providers/openai.js +0 -133
- package/dist/core/usage/providers/openai.js.map +0 -1
- package/dist/core/usage/redact.d.ts +0 -3
- package/dist/core/usage/redact.js +0 -67
- package/dist/core/usage/redact.js.map +0 -1
- package/dist/core/usage/store.d.ts +0 -4
- package/dist/core/usage/store.js +0 -31
- package/dist/core/usage/store.js.map +0 -1
- package/dist/core/usage/types.d.ts +0 -21
- package/dist/core/usage/types.js +0 -2
- package/dist/core/usage/types.js.map +0 -1
- package/dist/index.d.ts +0 -5
- package/dist/server/auth-watcher.d.ts +0 -32
- package/dist/server/auth-watcher.js +0 -227
- package/dist/server/auth-watcher.js.map +0 -1
- package/dist/server/commands.d.ts +0 -2
- package/dist/server/commands.js +0 -46
- package/dist/server/commands.js.map +0 -1
- package/dist/server/fetch-patch.d.ts +0 -3
- package/dist/server/fetch-patch.js +0 -118
- package/dist/server/fetch-patch.js.map +0 -1
- package/dist/server/index.d.ts +0 -8
- package/dist/server/index.js +0 -94
- package/dist/server/index.js.map +0 -1
- package/dist/server/native.d.ts +0 -6
- package/dist/server/native.js +0 -35
- package/dist/server/native.js.map +0 -1
- package/dist/server/request-balancer.d.ts +0 -16
- package/dist/server/request-balancer.js +0 -43
- package/dist/server/request-balancer.js.map +0 -1
- package/dist/tui/actions.d.ts +0 -41
- package/dist/tui/actions.js +0 -92
- package/dist/tui/actions.js.map +0 -1
- package/dist/tui/balancer-bar-sync.d.ts +0 -19
- package/dist/tui/balancer-bar-sync.js +0 -45
- package/dist/tui/balancer-bar-sync.js.map +0 -1
- package/dist/tui/components/alias-dialog.d.ts +0 -4
- package/dist/tui/components/dashboard.d.ts +0 -12
- package/dist/tui/components/priority-screen.d.ts +0 -9
- package/dist/tui/components/provider-model-dialog.d.ts +0 -14
- package/dist/tui/components/rename-dialog.d.ts +0 -4
- package/dist/tui/components/sidebar.d.ts +0 -10
- package/dist/tui/components/status-indicator.d.ts +0 -9
- package/dist/tui/components/usage-bar.d.ts +0 -8
- package/dist/tui/components/usage-display.d.ts +0 -10
- package/dist/tui/connect.d.ts +0 -30
- package/dist/tui/connect.js +0 -75
- package/dist/tui/connect.js.map +0 -1
- package/dist/tui/dashboard-keys.d.ts +0 -45
- package/dist/tui/dashboard-keys.js +0 -44
- package/dist/tui/dashboard-keys.js.map +0 -1
- package/dist/tui/native-model-apply.d.ts +0 -21
- package/dist/tui/native-model-apply.js +0 -53
- package/dist/tui/native-model-apply.js.map +0 -1
- package/dist/tui/priority-keys.d.ts +0 -40
- package/dist/tui/priority-keys.js +0 -38
- package/dist/tui/priority-keys.js.map +0 -1
- package/dist/tui/provider-models.d.ts +0 -19
- package/dist/tui/provider-models.js +0 -17
- package/dist/tui/provider-models.js.map +0 -1
- package/dist/tui/responsive.d.ts +0 -9
- package/dist/tui/responsive.js +0 -13
- package/dist/tui/responsive.js.map +0 -1
- package/dist/tui/selected-account-bar-sync.d.ts +0 -10
- package/dist/tui/selected-account-bar-sync.js +0 -26
- package/dist/tui/selected-account-bar-sync.js.map +0 -1
- package/dist/tui/selection-colors.d.ts +0 -10
- package/dist/tui/selection-colors.js +0 -38
- package/dist/tui/selection-colors.js.map +0 -1
- package/dist/tui/state.d.ts +0 -14
- package/dist/tui/state.js +0 -46
- package/dist/tui/state.js.map +0 -1
- package/dist/tui/status-format.d.ts +0 -15
- package/dist/tui/status-format.js +0 -17
- package/dist/tui/status-format.js.map +0 -1
- package/dist/tui/tui.d.ts +0 -7
- package/dist/tui/usage-auto-refresh.d.ts +0 -16
- package/dist/tui/usage-auto-refresh.js +0 -46
- package/dist/tui/usage-auto-refresh.js.map +0 -1
- package/dist/tui/usage-format.d.ts +0 -2
- package/dist/tui/usage-format.js +0 -17
- package/dist/tui/usage-format.js.map +0 -1
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { saveAccount } from "./accounts";
|
|
4
|
+
import { now } from "./time";
|
|
5
|
+
import type { AuthInfo, PendingConnection } from "./types";
|
|
6
|
+
|
|
7
|
+
type PendingConnectionRow = {
|
|
8
|
+
id: string;
|
|
9
|
+
provider_id: string;
|
|
10
|
+
auth_json: string;
|
|
11
|
+
auth_type: string;
|
|
12
|
+
source: string;
|
|
13
|
+
captured_at: number;
|
|
14
|
+
prompt_status: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type PendingIdentity = {
|
|
18
|
+
providerID: string;
|
|
19
|
+
auth: AuthInfo;
|
|
20
|
+
source: PendingConnection["source"];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const pendingSources = ["auth-file", "http", "oauth-callback"] as const;
|
|
24
|
+
const promptStatuses = ["new", "prompted", "dismissed"] as const;
|
|
25
|
+
const authTypes = ["api", "oauth", "wellknown"] as const;
|
|
26
|
+
|
|
27
|
+
function isAuthType(value: string): value is AuthInfo["type"] {
|
|
28
|
+
return authTypes.includes(value as AuthInfo["type"]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parsePendingAuth(row: PendingConnectionRow): AuthInfo {
|
|
32
|
+
let auth: unknown;
|
|
33
|
+
try {
|
|
34
|
+
auth = JSON.parse(row.auth_json);
|
|
35
|
+
} catch {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Invalid pending connection row ${row.id}: auth_json is invalid`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (
|
|
42
|
+
!auth ||
|
|
43
|
+
typeof auth !== "object" ||
|
|
44
|
+
!("type" in auth) ||
|
|
45
|
+
typeof auth.type !== "string"
|
|
46
|
+
) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Invalid pending connection row ${row.id}: auth_json type is invalid`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (!isAuthType(auth.type)) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Invalid pending connection row ${row.id}: auth_json type ${auth.type} is invalid`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return auth as AuthInfo;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function validatePendingAuthType(row: PendingConnectionRow): AuthInfo["type"] {
|
|
60
|
+
if (!isAuthType(row.auth_type)) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Invalid pending connection row ${row.id}: auth_type ${row.auth_type} is invalid`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return row.auth_type;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function validatePendingSource(
|
|
69
|
+
row: PendingConnectionRow,
|
|
70
|
+
): PendingConnection["source"] {
|
|
71
|
+
if (!pendingSources.includes(row.source as PendingConnection["source"])) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Invalid pending connection row ${row.id}: source ${row.source} is invalid`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return row.source as PendingConnection["source"];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function validatePromptStatus(
|
|
80
|
+
row: PendingConnectionRow,
|
|
81
|
+
): PendingConnection["promptStatus"] {
|
|
82
|
+
if (
|
|
83
|
+
!promptStatuses.includes(
|
|
84
|
+
row.prompt_status as PendingConnection["promptStatus"],
|
|
85
|
+
)
|
|
86
|
+
) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Invalid pending connection row ${row.id}: prompt_status ${row.prompt_status} is invalid`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return row.prompt_status as PendingConnection["promptStatus"];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function pendingConnectionFromRow(
|
|
95
|
+
row: PendingConnectionRow,
|
|
96
|
+
): PendingConnection {
|
|
97
|
+
const auth = parsePendingAuth(row);
|
|
98
|
+
const authType = validatePendingAuthType(row);
|
|
99
|
+
if (auth.type !== authType) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Invalid pending connection row ${row.id}: auth_type ${authType} does not match auth_json type ${auth.type}`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
auth,
|
|
107
|
+
authType,
|
|
108
|
+
capturedAt: row.captured_at,
|
|
109
|
+
id: row.id,
|
|
110
|
+
promptStatus: validatePromptStatus(row),
|
|
111
|
+
providerID: row.provider_id,
|
|
112
|
+
source: validatePendingSource(row),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function authIdentityKey(auth: AuthInfo) {
|
|
117
|
+
if (auth.type === "api") return `api\0${auth.key}`;
|
|
118
|
+
if (auth.type === "wellknown") return `wellknown\0${auth.key}\0${auth.token}`;
|
|
119
|
+
const accountID = oauthAccountID(auth);
|
|
120
|
+
return accountID
|
|
121
|
+
? `oauth-account\0${accountID}`
|
|
122
|
+
: `oauth-refresh\0${auth.refresh}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function decodeJwtPayload(token: string): Record<string, unknown> | undefined {
|
|
126
|
+
const payload = token.split(".")[1];
|
|
127
|
+
if (!payload) return undefined;
|
|
128
|
+
try {
|
|
129
|
+
const decoded = JSON.parse(
|
|
130
|
+
Buffer.from(payload, "base64url").toString("utf8"),
|
|
131
|
+
) as unknown;
|
|
132
|
+
return decoded && typeof decoded === "object" && !Array.isArray(decoded)
|
|
133
|
+
? (decoded as Record<string, unknown>)
|
|
134
|
+
: undefined;
|
|
135
|
+
} catch {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function oauthAccountID(auth: Extract<AuthInfo, { type: "oauth" }>) {
|
|
141
|
+
if (auth.accountId) return auth.accountId;
|
|
142
|
+
const claim = decodeJwtPayload(auth.access)?.["https://api.openai.com/auth"];
|
|
143
|
+
if (!claim || typeof claim !== "object" || Array.isArray(claim))
|
|
144
|
+
return undefined;
|
|
145
|
+
const accountID = (claim as Record<string, unknown>).chatgpt_account_id;
|
|
146
|
+
return typeof accountID === "string" && accountID.length > 0
|
|
147
|
+
? accountID
|
|
148
|
+
: undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function samePendingIdentity(auth: AuthInfo, other: AuthInfo) {
|
|
152
|
+
return authIdentityKey(auth) === authIdentityKey(other);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function pendingID(
|
|
156
|
+
providerID: string,
|
|
157
|
+
auth: AuthInfo,
|
|
158
|
+
source: PendingConnection["source"],
|
|
159
|
+
) {
|
|
160
|
+
const hash = createHash("sha256")
|
|
161
|
+
.update(`${providerID}\0${source}\0${authIdentityKey(auth)}`)
|
|
162
|
+
.digest("hex")
|
|
163
|
+
.slice(0, 32);
|
|
164
|
+
return `pending-${hash}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function pendingPromptGroupID(db: Database, id: string) {
|
|
168
|
+
const row = selectPendingRow(db, id);
|
|
169
|
+
if (!row) return undefined;
|
|
170
|
+
const identity = pendingIdentity(row);
|
|
171
|
+
return `${identity.providerID}\0${identity.source}\0${authIdentityKey(identity.auth)}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function selectPendingRow(db: Database, id: string) {
|
|
175
|
+
return db
|
|
176
|
+
.query<PendingConnectionRow, [string]>(
|
|
177
|
+
`SELECT id, provider_id, auth_json, auth_type, source, captured_at, prompt_status
|
|
178
|
+
FROM pending_connections
|
|
179
|
+
WHERE id = ?`,
|
|
180
|
+
)
|
|
181
|
+
.get(id);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function pendingIdentity(row: PendingConnectionRow): PendingIdentity {
|
|
185
|
+
return {
|
|
186
|
+
auth: parsePendingAuth(row),
|
|
187
|
+
providerID: row.provider_id,
|
|
188
|
+
source: validatePendingSource(row),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function equivalentPendingRows(db: Database, identity: PendingIdentity) {
|
|
193
|
+
return db
|
|
194
|
+
.query<PendingConnectionRow, [string, string, string]>(
|
|
195
|
+
`SELECT id, provider_id, auth_json, auth_type, source, captured_at, prompt_status
|
|
196
|
+
FROM pending_connections
|
|
197
|
+
WHERE provider_id = ? AND source = ? AND auth_type = ?`,
|
|
198
|
+
)
|
|
199
|
+
.all(identity.providerID, identity.source, identity.auth.type)
|
|
200
|
+
.filter((row) => samePendingIdentity(parsePendingAuth(row), identity.auth));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function createPendingConnection(
|
|
204
|
+
db: Database,
|
|
205
|
+
providerID: string,
|
|
206
|
+
auth: AuthInfo,
|
|
207
|
+
source: PendingConnection["source"],
|
|
208
|
+
): PendingConnection {
|
|
209
|
+
const authJSON = JSON.stringify(auth);
|
|
210
|
+
const identity = { auth, providerID, source } satisfies PendingIdentity;
|
|
211
|
+
const existing = equivalentPendingRows(db, identity)[0];
|
|
212
|
+
if (existing) return pendingConnectionFromRow(existing);
|
|
213
|
+
|
|
214
|
+
const pending: PendingConnection = {
|
|
215
|
+
auth,
|
|
216
|
+
authType: auth.type,
|
|
217
|
+
capturedAt: now(),
|
|
218
|
+
id: pendingID(providerID, auth, source),
|
|
219
|
+
promptStatus: "new",
|
|
220
|
+
providerID,
|
|
221
|
+
source,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
db.query<unknown, [string, string, string, string, string, number, string]>(
|
|
225
|
+
`INSERT INTO pending_connections (
|
|
226
|
+
id,
|
|
227
|
+
provider_id,
|
|
228
|
+
auth_json,
|
|
229
|
+
auth_type,
|
|
230
|
+
source,
|
|
231
|
+
captured_at,
|
|
232
|
+
prompt_status
|
|
233
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
234
|
+
ON CONFLICT(id) DO NOTHING`,
|
|
235
|
+
).run(
|
|
236
|
+
pending.id,
|
|
237
|
+
pending.providerID,
|
|
238
|
+
authJSON,
|
|
239
|
+
pending.authType,
|
|
240
|
+
pending.source,
|
|
241
|
+
pending.capturedAt,
|
|
242
|
+
pending.promptStatus,
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const row = selectPendingRow(db, pending.id);
|
|
246
|
+
return row ? pendingConnectionFromRow(row) : pending;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function claimPendingPrompt(db: Database, id: string) {
|
|
250
|
+
const claim = db.transaction(() => {
|
|
251
|
+
const row = selectPendingRow(db, id);
|
|
252
|
+
if (!row) return undefined;
|
|
253
|
+
const identity = pendingIdentity(row);
|
|
254
|
+
const equivalent = equivalentPendingRows(db, identity);
|
|
255
|
+
const ids = equivalent.map((pending) => pending.id);
|
|
256
|
+
const result = db
|
|
257
|
+
.query<unknown, string[]>(
|
|
258
|
+
`UPDATE pending_connections
|
|
259
|
+
SET prompt_status = 'prompted'
|
|
260
|
+
WHERE id IN (${ids.map(() => "?").join(", ")})
|
|
261
|
+
AND prompt_status = 'new'`,
|
|
262
|
+
)
|
|
263
|
+
.run(...ids);
|
|
264
|
+
if (result.changes === 0) return undefined;
|
|
265
|
+
return pendingConnectionFromRow({ ...row, prompt_status: "prompted" });
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
return claim();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function releasePendingPrompt(db: Database, id: string) {
|
|
272
|
+
const release = db.transaction(() => {
|
|
273
|
+
const row = selectPendingRow(db, id);
|
|
274
|
+
if (!row) return false;
|
|
275
|
+
const identity = pendingIdentity(row);
|
|
276
|
+
const ids = equivalentPendingRows(db, identity).map(
|
|
277
|
+
(pending) => pending.id,
|
|
278
|
+
);
|
|
279
|
+
if (ids.length === 0) return false;
|
|
280
|
+
const result = db
|
|
281
|
+
.query<unknown, string[]>(
|
|
282
|
+
`UPDATE pending_connections
|
|
283
|
+
SET prompt_status = 'new'
|
|
284
|
+
WHERE id IN (${ids.map(() => "?").join(", ")})
|
|
285
|
+
AND prompt_status = 'prompted'`,
|
|
286
|
+
)
|
|
287
|
+
.run(...ids);
|
|
288
|
+
return result.changes > 0;
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
return release();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function listPendingConnections(db: Database): PendingConnection[] {
|
|
295
|
+
const rows = db
|
|
296
|
+
.query<PendingConnectionRow, []>(
|
|
297
|
+
`SELECT id, provider_id, auth_json, auth_type, source, captured_at, prompt_status
|
|
298
|
+
FROM pending_connections
|
|
299
|
+
ORDER BY captured_at DESC, id DESC`,
|
|
300
|
+
)
|
|
301
|
+
.all();
|
|
302
|
+
return rows.map(pendingConnectionFromRow);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function completePendingConnection(
|
|
306
|
+
db: Database,
|
|
307
|
+
id: string,
|
|
308
|
+
alias: string,
|
|
309
|
+
) {
|
|
310
|
+
const complete = db.transaction(() => {
|
|
311
|
+
const row = selectPendingRow(db, id);
|
|
312
|
+
if (!row) throw new Error(`Pending connection not found: ${id}`);
|
|
313
|
+
|
|
314
|
+
const pending = pendingConnectionFromRow(row);
|
|
315
|
+
const account = saveAccount(db, pending.providerID, alias, pending.auth);
|
|
316
|
+
const identity = pendingIdentity(row);
|
|
317
|
+
const ids = equivalentPendingRows(db, identity).map(
|
|
318
|
+
(pending) => pending.id,
|
|
319
|
+
);
|
|
320
|
+
db.query<unknown, string[]>(
|
|
321
|
+
`DELETE FROM pending_connections
|
|
322
|
+
WHERE id IN (${ids.map(() => "?").join(", ")})`,
|
|
323
|
+
).run(...ids);
|
|
324
|
+
return account;
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
return complete();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function removePendingConnection(db: Database, id: string) {
|
|
331
|
+
const remove = db.transaction(() => {
|
|
332
|
+
const row = selectPendingRow(db, id);
|
|
333
|
+
if (!row) return false;
|
|
334
|
+
const identity = pendingIdentity(row);
|
|
335
|
+
const ids = equivalentPendingRows(db, identity).map(
|
|
336
|
+
(pending) => pending.id,
|
|
337
|
+
);
|
|
338
|
+
if (ids.length === 0) return false;
|
|
339
|
+
const result = db
|
|
340
|
+
.query<unknown, string[]>(
|
|
341
|
+
`UPDATE pending_connections
|
|
342
|
+
SET prompt_status = 'dismissed'
|
|
343
|
+
WHERE id IN (${ids.map(() => "?").join(", ")})
|
|
344
|
+
AND prompt_status != 'dismissed'`,
|
|
345
|
+
)
|
|
346
|
+
.run(...ids);
|
|
347
|
+
return result.changes > 0;
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
return remove();
|
|
351
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { getActiveAccount, listAccounts } from "./accounts";
|
|
3
|
+
import { now } from "./time";
|
|
4
|
+
import type { Account } from "./types";
|
|
5
|
+
|
|
6
|
+
export type PriorityEntry = {
|
|
7
|
+
providerID: string;
|
|
8
|
+
position: number;
|
|
9
|
+
modelID?: string;
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type ActiveSelection = {
|
|
14
|
+
providerID: string;
|
|
15
|
+
modelID: string;
|
|
16
|
+
account: Account;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type PriorityRow = {
|
|
20
|
+
provider_id: string;
|
|
21
|
+
position: number;
|
|
22
|
+
model_id: string | null;
|
|
23
|
+
enabled: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function storedRows(db: Database): Map<string, PriorityRow> {
|
|
27
|
+
const rows = db
|
|
28
|
+
.query<PriorityRow, []>(
|
|
29
|
+
"SELECT provider_id, position, model_id, enabled FROM provider_priority",
|
|
30
|
+
)
|
|
31
|
+
.all();
|
|
32
|
+
return new Map(rows.map((row) => [row.provider_id, row]));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function providersWithAccounts(db: Database): string[] {
|
|
36
|
+
return [...new Set(listAccounts(db).map((account) => account.providerID))];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function nextPosition(stored: Map<string, PriorityRow>): number {
|
|
40
|
+
let max = -1;
|
|
41
|
+
for (const row of stored.values()) max = Math.max(max, row.position);
|
|
42
|
+
return max + 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function upsert(
|
|
46
|
+
db: Database,
|
|
47
|
+
providerID: string,
|
|
48
|
+
patch: { position?: number; modelID?: string | null; enabled?: boolean },
|
|
49
|
+
) {
|
|
50
|
+
const stored = storedRows(db);
|
|
51
|
+
const existing = stored.get(providerID);
|
|
52
|
+
const position = patch.position ?? existing?.position ?? nextPosition(stored);
|
|
53
|
+
const modelID =
|
|
54
|
+
patch.modelID !== undefined ? patch.modelID : (existing?.model_id ?? null);
|
|
55
|
+
const enabled =
|
|
56
|
+
patch.enabled !== undefined
|
|
57
|
+
? patch.enabled
|
|
58
|
+
? 1
|
|
59
|
+
: 0
|
|
60
|
+
: (existing?.enabled ?? 1);
|
|
61
|
+
|
|
62
|
+
db.query<unknown, [string, number, string | null, number, number]>(
|
|
63
|
+
`INSERT INTO provider_priority (provider_id, position, model_id, enabled, updated_at)
|
|
64
|
+
VALUES (?, ?, ?, ?, ?)
|
|
65
|
+
ON CONFLICT(provider_id) DO UPDATE SET
|
|
66
|
+
position = excluded.position,
|
|
67
|
+
model_id = excluded.model_id,
|
|
68
|
+
enabled = excluded.enabled,
|
|
69
|
+
updated_at = excluded.updated_at`,
|
|
70
|
+
).run(providerID, position, modelID, enabled, now());
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function listProviderPriority(db: Database): PriorityEntry[] {
|
|
74
|
+
const stored = storedRows(db);
|
|
75
|
+
const providers = providersWithAccounts(db);
|
|
76
|
+
|
|
77
|
+
const ordered = providers.slice().sort((a, b) => {
|
|
78
|
+
const ra = stored.get(a);
|
|
79
|
+
const rb = stored.get(b);
|
|
80
|
+
if (ra && rb) return ra.position - rb.position || a.localeCompare(b);
|
|
81
|
+
if (ra) return -1;
|
|
82
|
+
if (rb) return 1;
|
|
83
|
+
return a.localeCompare(b);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return ordered.map((providerID, index) => {
|
|
87
|
+
const row = stored.get(providerID);
|
|
88
|
+
return {
|
|
89
|
+
enabled: row ? row.enabled === 1 : true,
|
|
90
|
+
modelID: row?.model_id ?? undefined,
|
|
91
|
+
position: index,
|
|
92
|
+
providerID,
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function setProviderModel(
|
|
98
|
+
db: Database,
|
|
99
|
+
providerID: string,
|
|
100
|
+
modelID: string,
|
|
101
|
+
) {
|
|
102
|
+
upsert(db, providerID, { modelID });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function setProviderEnabled(
|
|
106
|
+
db: Database,
|
|
107
|
+
providerID: string,
|
|
108
|
+
enabled: boolean,
|
|
109
|
+
) {
|
|
110
|
+
upsert(db, providerID, { enabled });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function moveProvider(
|
|
114
|
+
db: Database,
|
|
115
|
+
providerID: string,
|
|
116
|
+
direction: -1 | 1,
|
|
117
|
+
) {
|
|
118
|
+
const list = listProviderPriority(db);
|
|
119
|
+
const index = list.findIndex((entry) => entry.providerID === providerID);
|
|
120
|
+
if (index === -1) return;
|
|
121
|
+
const target = index + direction;
|
|
122
|
+
if (target < 0 || target >= list.length) return;
|
|
123
|
+
|
|
124
|
+
const reordered = list.slice();
|
|
125
|
+
const [moved] = reordered.splice(index, 1);
|
|
126
|
+
reordered.splice(target, 0, moved);
|
|
127
|
+
|
|
128
|
+
const persist = db.transaction(() => {
|
|
129
|
+
for (const [position, entry] of reordered.entries()) {
|
|
130
|
+
upsert(db, entry.providerID, { position });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
persist();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function getBalancingEnabled(db: Database): boolean {
|
|
137
|
+
const row = db
|
|
138
|
+
.query<{ value: string }, [string]>(
|
|
139
|
+
"SELECT value FROM settings WHERE key = ?",
|
|
140
|
+
)
|
|
141
|
+
.get("balancing_enabled");
|
|
142
|
+
|
|
143
|
+
return row?.value === "1";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function setBalancingEnabled(db: Database, enabled: boolean) {
|
|
147
|
+
db.query<unknown, [string]>(
|
|
148
|
+
`INSERT INTO settings (key, value) VALUES ('balancing_enabled', ?)
|
|
149
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
|
|
150
|
+
).run(enabled ? "1" : "0");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function chooseHealthyAccount(
|
|
154
|
+
db: Database,
|
|
155
|
+
providerID: string,
|
|
156
|
+
nowMs: number,
|
|
157
|
+
): Account | undefined {
|
|
158
|
+
const healthy = listAccounts(db, providerID).filter(
|
|
159
|
+
(account) =>
|
|
160
|
+
!account.disabled &&
|
|
161
|
+
(!account.rateLimitedUntil || account.rateLimitedUntil <= nowMs),
|
|
162
|
+
);
|
|
163
|
+
if (healthy.length === 0) return undefined;
|
|
164
|
+
|
|
165
|
+
const active = getActiveAccount(db, providerID);
|
|
166
|
+
if (active && healthy.some((account) => account.alias === active.alias))
|
|
167
|
+
return active;
|
|
168
|
+
return healthy[0];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function resolveActiveSelection(
|
|
172
|
+
db: Database,
|
|
173
|
+
nowMs: number = now(),
|
|
174
|
+
preferredProviderID?: string,
|
|
175
|
+
): ActiveSelection | undefined {
|
|
176
|
+
const entries = listProviderPriority(db);
|
|
177
|
+
const ordered = preferredProviderID
|
|
178
|
+
? entries.slice().sort((a, b) => {
|
|
179
|
+
if (a.providerID === preferredProviderID) return -1;
|
|
180
|
+
if (b.providerID === preferredProviderID) return 1;
|
|
181
|
+
return a.position - b.position;
|
|
182
|
+
})
|
|
183
|
+
: entries;
|
|
184
|
+
|
|
185
|
+
for (const entry of ordered) {
|
|
186
|
+
if (!entry.enabled) continue;
|
|
187
|
+
if (!entry.modelID) continue;
|
|
188
|
+
const account = chooseHealthyAccount(db, entry.providerID, nowMs);
|
|
189
|
+
if (!account) continue;
|
|
190
|
+
return { account, modelID: entry.modelID, providerID: entry.providerID };
|
|
191
|
+
}
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|