@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,404 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { now } from "./time";
|
|
3
|
+
import type { Account, AuthInfo, SelectedModel } from "./types";
|
|
4
|
+
|
|
5
|
+
type AccountRow = {
|
|
6
|
+
provider_id: string;
|
|
7
|
+
alias: string;
|
|
8
|
+
auth_json: string;
|
|
9
|
+
auth_type: string;
|
|
10
|
+
created_at: number;
|
|
11
|
+
updated_at: number;
|
|
12
|
+
last_used_at: number | null;
|
|
13
|
+
rate_limited_until: number | null;
|
|
14
|
+
failures: number;
|
|
15
|
+
disabled: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const authTypes = ["api", "oauth", "wellknown"] as const;
|
|
19
|
+
|
|
20
|
+
export function normalizeAlias(alias: string) {
|
|
21
|
+
return alias
|
|
22
|
+
.trim()
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
25
|
+
.replace(/^-+|-+$/g, "");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function accountRowID(row: AccountRow) {
|
|
29
|
+
return `${row.provider_id}/${row.alias}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isAuthType(value: string): value is AuthInfo["type"] {
|
|
33
|
+
return authTypes.includes(value as AuthInfo["type"]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseAccountAuth(row: AccountRow): AuthInfo {
|
|
37
|
+
const id = accountRowID(row);
|
|
38
|
+
let auth: unknown;
|
|
39
|
+
try {
|
|
40
|
+
auth = JSON.parse(row.auth_json);
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(`Invalid account row for ${id}: auth_json is invalid`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
!auth ||
|
|
47
|
+
typeof auth !== "object" ||
|
|
48
|
+
!("type" in auth) ||
|
|
49
|
+
typeof auth.type !== "string"
|
|
50
|
+
) {
|
|
51
|
+
throw new Error(`Invalid account row for ${id}: auth_json type is invalid`);
|
|
52
|
+
}
|
|
53
|
+
if (!isAuthType(auth.type)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Invalid account row for ${id}: auth_json type ${auth.type} is invalid`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return auth as AuthInfo;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function validateAccountAuthType(row: AccountRow): AuthInfo["type"] {
|
|
62
|
+
if (!isAuthType(row.auth_type)) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Invalid account row for ${accountRowID(row)}: auth_type ${row.auth_type} is invalid`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return row.auth_type;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function validateDisabled(row: AccountRow) {
|
|
71
|
+
if (row.disabled !== 0 && row.disabled !== 1) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Invalid account row for ${accountRowID(row)}: disabled ${row.disabled} is invalid`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return Boolean(row.disabled);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function accountFromRow(row: AccountRow): Account {
|
|
80
|
+
const auth = parseAccountAuth(row);
|
|
81
|
+
const authType = validateAccountAuthType(row);
|
|
82
|
+
if (auth.type !== authType) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Invalid account row for ${accountRowID(row)}: auth_type ${authType} does not match auth_json type ${auth.type}`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
alias: row.alias,
|
|
90
|
+
auth,
|
|
91
|
+
authType,
|
|
92
|
+
createdAt: row.created_at,
|
|
93
|
+
disabled: validateDisabled(row),
|
|
94
|
+
failures: row.failures,
|
|
95
|
+
lastUsedAt: row.last_used_at ?? undefined,
|
|
96
|
+
providerID: row.provider_id,
|
|
97
|
+
rateLimitedUntil: row.rate_limited_until ?? undefined,
|
|
98
|
+
updatedAt: row.updated_at,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function setActiveAlias(db: Database, providerID: string, alias: string) {
|
|
103
|
+
db.query<unknown, [string, string, number]>(
|
|
104
|
+
`INSERT INTO provider_state (provider_id, active_alias, updated_at, metadata_json)
|
|
105
|
+
VALUES (?, ?, ?, '{}')
|
|
106
|
+
ON CONFLICT(provider_id) DO UPDATE SET
|
|
107
|
+
active_alias = excluded.active_alias,
|
|
108
|
+
updated_at = excluded.updated_at`,
|
|
109
|
+
).run(providerID, alias, now());
|
|
110
|
+
db.query<unknown, [string]>(
|
|
111
|
+
`INSERT INTO settings (key, value) VALUES ('selected_provider_id', ?)
|
|
112
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
|
|
113
|
+
).run(providerID);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseMetadataJson(
|
|
117
|
+
value: string | null | undefined,
|
|
118
|
+
): Record<string, unknown> {
|
|
119
|
+
if (!value) return {};
|
|
120
|
+
try {
|
|
121
|
+
const metadata = JSON.parse(value);
|
|
122
|
+
return metadata && typeof metadata === "object" && !Array.isArray(metadata)
|
|
123
|
+
? metadata
|
|
124
|
+
: {};
|
|
125
|
+
} catch {
|
|
126
|
+
return {};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function saveAccount(
|
|
131
|
+
db: Database,
|
|
132
|
+
providerID: string,
|
|
133
|
+
aliasInput: string,
|
|
134
|
+
auth: AuthInfo,
|
|
135
|
+
): Account {
|
|
136
|
+
const alias = normalizeAlias(aliasInput);
|
|
137
|
+
if (!alias) throw new Error("Invalid alias");
|
|
138
|
+
|
|
139
|
+
const save = db.transaction(() => {
|
|
140
|
+
const timestamp = now();
|
|
141
|
+
db.query<
|
|
142
|
+
unknown,
|
|
143
|
+
[string, string, string, string, number, number, number, number]
|
|
144
|
+
>(
|
|
145
|
+
`INSERT INTO accounts (
|
|
146
|
+
provider_id,
|
|
147
|
+
alias,
|
|
148
|
+
auth_json,
|
|
149
|
+
auth_type,
|
|
150
|
+
created_at,
|
|
151
|
+
updated_at,
|
|
152
|
+
failures,
|
|
153
|
+
disabled
|
|
154
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
155
|
+
ON CONFLICT(provider_id, alias) DO UPDATE SET
|
|
156
|
+
auth_json = excluded.auth_json,
|
|
157
|
+
auth_type = excluded.auth_type,
|
|
158
|
+
updated_at = excluded.updated_at`,
|
|
159
|
+
).run(
|
|
160
|
+
providerID,
|
|
161
|
+
alias,
|
|
162
|
+
JSON.stringify(auth),
|
|
163
|
+
auth.type,
|
|
164
|
+
timestamp,
|
|
165
|
+
timestamp,
|
|
166
|
+
0,
|
|
167
|
+
0,
|
|
168
|
+
);
|
|
169
|
+
setActiveAlias(db, providerID, alias);
|
|
170
|
+
|
|
171
|
+
const account = getAccount(db, providerID, alias);
|
|
172
|
+
if (!account) throw new Error(`Account not found: ${providerID}/${alias}`);
|
|
173
|
+
return account;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return save();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function updateAccountAuth(
|
|
180
|
+
db: Database,
|
|
181
|
+
providerID: string,
|
|
182
|
+
aliasInput: string,
|
|
183
|
+
auth: AuthInfo,
|
|
184
|
+
): Account | undefined {
|
|
185
|
+
const alias = normalizeAlias(aliasInput);
|
|
186
|
+
if (!alias) throw new Error("Invalid alias");
|
|
187
|
+
|
|
188
|
+
db.query<unknown, [string, string, number, string, string]>(
|
|
189
|
+
`UPDATE accounts
|
|
190
|
+
SET auth_json = ?,
|
|
191
|
+
auth_type = ?,
|
|
192
|
+
updated_at = ?
|
|
193
|
+
WHERE provider_id = ? AND alias = ?`,
|
|
194
|
+
).run(JSON.stringify(auth), auth.type, now(), providerID, alias);
|
|
195
|
+
return getAccount(db, providerID, alias);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function renameAccount(
|
|
199
|
+
db: Database,
|
|
200
|
+
providerID: string,
|
|
201
|
+
fromAliasInput: string,
|
|
202
|
+
toAliasInput: string,
|
|
203
|
+
): Account {
|
|
204
|
+
const fromAlias = normalizeAlias(fromAliasInput);
|
|
205
|
+
const toAlias = normalizeAlias(toAliasInput);
|
|
206
|
+
if (!fromAlias || !toAlias) throw new Error("Invalid alias");
|
|
207
|
+
if (fromAlias === toAlias) {
|
|
208
|
+
const account = getAccount(db, providerID, fromAlias);
|
|
209
|
+
if (!account)
|
|
210
|
+
throw new Error(`Account not found: ${providerID}/${fromAlias}`);
|
|
211
|
+
return account;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const rename = db.transaction(() => {
|
|
215
|
+
const existing = getAccount(db, providerID, fromAlias);
|
|
216
|
+
if (!existing)
|
|
217
|
+
throw new Error(`Account not found: ${providerID}/${fromAlias}`);
|
|
218
|
+
if (getAccount(db, providerID, toAlias))
|
|
219
|
+
throw new Error(`Account already exists: ${providerID}/${toAlias}`);
|
|
220
|
+
|
|
221
|
+
db.query<unknown, [string, number, string, string]>(
|
|
222
|
+
`UPDATE accounts
|
|
223
|
+
SET alias = ?, updated_at = ?
|
|
224
|
+
WHERE provider_id = ? AND alias = ?`,
|
|
225
|
+
).run(toAlias, now(), providerID, fromAlias);
|
|
226
|
+
db.query<unknown, [string, string, string]>(
|
|
227
|
+
`UPDATE usage_snapshots
|
|
228
|
+
SET alias = ?
|
|
229
|
+
WHERE provider_id = ? AND alias = ?`,
|
|
230
|
+
).run(toAlias, providerID, fromAlias);
|
|
231
|
+
|
|
232
|
+
const active = db
|
|
233
|
+
.query<{ active_alias: string | null }, [string]>(
|
|
234
|
+
"SELECT active_alias FROM provider_state WHERE provider_id = ?",
|
|
235
|
+
)
|
|
236
|
+
.get(providerID);
|
|
237
|
+
if (active?.active_alias === fromAlias)
|
|
238
|
+
setActiveAlias(db, providerID, toAlias);
|
|
239
|
+
|
|
240
|
+
const account = getAccount(db, providerID, toAlias);
|
|
241
|
+
if (!account)
|
|
242
|
+
throw new Error(`Account not found: ${providerID}/${toAlias}`);
|
|
243
|
+
return account;
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return rename();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function getAccount(
|
|
250
|
+
db: Database,
|
|
251
|
+
providerID: string,
|
|
252
|
+
alias: string,
|
|
253
|
+
): Account | undefined {
|
|
254
|
+
const normalizedAlias = normalizeAlias(alias);
|
|
255
|
+
const row = db
|
|
256
|
+
.query<AccountRow, [string, string]>(
|
|
257
|
+
`SELECT provider_id, alias, auth_json, auth_type, created_at, updated_at,
|
|
258
|
+
last_used_at, rate_limited_until, failures, disabled
|
|
259
|
+
FROM accounts
|
|
260
|
+
WHERE provider_id = ? AND alias = ?`,
|
|
261
|
+
)
|
|
262
|
+
.get(providerID, normalizedAlias);
|
|
263
|
+
return row ? accountFromRow(row) : undefined;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function listAccounts(db: Database, providerID?: string): Account[] {
|
|
267
|
+
const sql = `SELECT provider_id, alias, auth_json, auth_type, created_at, updated_at,
|
|
268
|
+
last_used_at, rate_limited_until, failures, disabled
|
|
269
|
+
FROM accounts`;
|
|
270
|
+
const rows = providerID
|
|
271
|
+
? db
|
|
272
|
+
.query<AccountRow, [string]>(
|
|
273
|
+
`${sql} WHERE provider_id = ? ORDER BY alias`,
|
|
274
|
+
)
|
|
275
|
+
.all(providerID)
|
|
276
|
+
: db.query<AccountRow, []>(`${sql} ORDER BY provider_id, alias`).all();
|
|
277
|
+
return rows.map(accountFromRow);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function setActiveAccount(
|
|
281
|
+
db: Database,
|
|
282
|
+
providerID: string,
|
|
283
|
+
alias: string,
|
|
284
|
+
): Account {
|
|
285
|
+
const normalizedAlias = normalizeAlias(alias);
|
|
286
|
+
const account = getAccount(db, providerID, normalizedAlias);
|
|
287
|
+
if (!account)
|
|
288
|
+
throw new Error(`Account not found: ${providerID}/${normalizedAlias}`);
|
|
289
|
+
|
|
290
|
+
setActiveAlias(db, providerID, normalizedAlias);
|
|
291
|
+
return account;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function removeAccount(db: Database, providerID: string, alias: string) {
|
|
295
|
+
const normalizedAlias = normalizeAlias(alias);
|
|
296
|
+
const remove = db.transaction(() => {
|
|
297
|
+
const existing = getAccount(db, providerID, normalizedAlias);
|
|
298
|
+
if (!existing) return false;
|
|
299
|
+
|
|
300
|
+
db.query<unknown, [string, string]>(
|
|
301
|
+
"DELETE FROM accounts WHERE provider_id = ? AND alias = ?",
|
|
302
|
+
).run(providerID, normalizedAlias);
|
|
303
|
+
db.query<unknown, [string, string]>(
|
|
304
|
+
"DELETE FROM usage_snapshots WHERE provider_id = ? AND alias = ?",
|
|
305
|
+
).run(providerID, normalizedAlias);
|
|
306
|
+
|
|
307
|
+
const active = db
|
|
308
|
+
.query<{ active_alias: string | null }, [string]>(
|
|
309
|
+
"SELECT active_alias FROM provider_state WHERE provider_id = ?",
|
|
310
|
+
)
|
|
311
|
+
.get(providerID);
|
|
312
|
+
if (active?.active_alias === normalizedAlias) {
|
|
313
|
+
const next = db
|
|
314
|
+
.query<{ alias: string }, [string]>(
|
|
315
|
+
"SELECT alias FROM accounts WHERE provider_id = ? ORDER BY alias LIMIT 1",
|
|
316
|
+
)
|
|
317
|
+
.get(providerID);
|
|
318
|
+
db.query<unknown, [string | null, number, string]>(
|
|
319
|
+
`UPDATE provider_state
|
|
320
|
+
SET active_alias = ?, updated_at = ?
|
|
321
|
+
WHERE provider_id = ?`,
|
|
322
|
+
).run(next?.alias ?? null, now(), providerID);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return true;
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
return remove();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function getActiveAccount(
|
|
332
|
+
db: Database,
|
|
333
|
+
providerID: string,
|
|
334
|
+
): Account | undefined {
|
|
335
|
+
const row = db
|
|
336
|
+
.query<{ active_alias: string | null }, [string]>(
|
|
337
|
+
"SELECT active_alias FROM provider_state WHERE provider_id = ?",
|
|
338
|
+
)
|
|
339
|
+
.get(providerID);
|
|
340
|
+
if (!row?.active_alias) return undefined;
|
|
341
|
+
return getAccount(db, providerID, row.active_alias);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function getSelectedAccount(db: Database): Account | undefined {
|
|
345
|
+
const selectedProvider = db
|
|
346
|
+
.query<{ value: string }, []>(
|
|
347
|
+
"SELECT value FROM settings WHERE key = 'selected_provider_id'",
|
|
348
|
+
)
|
|
349
|
+
.get();
|
|
350
|
+
if (selectedProvider?.value) {
|
|
351
|
+
const active = getActiveAccount(db, selectedProvider.value);
|
|
352
|
+
if (active) return active;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const row = db
|
|
356
|
+
.query<{ provider_id: string; active_alias: string }, []>(
|
|
357
|
+
`SELECT provider_id, active_alias
|
|
358
|
+
FROM provider_state
|
|
359
|
+
WHERE active_alias IS NOT NULL
|
|
360
|
+
ORDER BY updated_at DESC, provider_id
|
|
361
|
+
LIMIT 1`,
|
|
362
|
+
)
|
|
363
|
+
.get();
|
|
364
|
+
if (!row?.active_alias) return undefined;
|
|
365
|
+
return getAccount(db, row.provider_id, row.active_alias);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function setSelectedModel(
|
|
369
|
+
db: Database,
|
|
370
|
+
providerID: string,
|
|
371
|
+
modelID: string,
|
|
372
|
+
): SelectedModel {
|
|
373
|
+
const row = db
|
|
374
|
+
.query<{ metadata_json: string | null }, [string]>(
|
|
375
|
+
"SELECT metadata_json FROM provider_state WHERE provider_id = ?",
|
|
376
|
+
)
|
|
377
|
+
.get(providerID);
|
|
378
|
+
const metadata = parseMetadataJson(row?.metadata_json);
|
|
379
|
+
metadata.selected_model_id = modelID;
|
|
380
|
+
|
|
381
|
+
db.query<unknown, [string, number, string]>(
|
|
382
|
+
`INSERT INTO provider_state (provider_id, active_alias, updated_at, metadata_json)
|
|
383
|
+
VALUES (?, NULL, ?, ?)
|
|
384
|
+
ON CONFLICT(provider_id) DO UPDATE SET
|
|
385
|
+
updated_at = excluded.updated_at,
|
|
386
|
+
metadata_json = excluded.metadata_json`,
|
|
387
|
+
).run(providerID, now(), JSON.stringify(metadata));
|
|
388
|
+
|
|
389
|
+
return { modelID, providerID };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function getSelectedModel(
|
|
393
|
+
db: Database,
|
|
394
|
+
providerID: string,
|
|
395
|
+
): SelectedModel | undefined {
|
|
396
|
+
const row = db
|
|
397
|
+
.query<{ metadata_json: string | null }, [string]>(
|
|
398
|
+
"SELECT metadata_json FROM provider_state WHERE provider_id = ?",
|
|
399
|
+
)
|
|
400
|
+
.get(providerID);
|
|
401
|
+
const modelID = parseMetadataJson(row?.metadata_json).selected_model_id;
|
|
402
|
+
if (typeof modelID !== "string" || modelID.length === 0) return undefined;
|
|
403
|
+
return { modelID, providerID };
|
|
404
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { chmodSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
|
|
5
|
+
const databases = new Map<string, Database>();
|
|
6
|
+
|
|
7
|
+
function isMissingFileError(error: unknown) {
|
|
8
|
+
return (error as { code?: string }).code === "ENOENT";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isClosedDatabaseError(error: unknown) {
|
|
12
|
+
return error instanceof Error && /closed/i.test(error.message);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function cachedDatabaseIsOpen(database: Database) {
|
|
16
|
+
try {
|
|
17
|
+
database.exec("SELECT 1;");
|
|
18
|
+
return true;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (isClosedDatabaseError(error)) return false;
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function secureDatabaseFiles(path: string) {
|
|
26
|
+
for (const file of [path, `${path}-wal`, `${path}-shm`]) {
|
|
27
|
+
try {
|
|
28
|
+
chmodSync(file, 0o600);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (!isMissingFileError(error)) throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function openBalancerDatabase(path: string) {
|
|
36
|
+
const existing = databases.get(path);
|
|
37
|
+
if (existing) {
|
|
38
|
+
if (cachedDatabaseIsOpen(existing)) return existing;
|
|
39
|
+
databases.delete(path);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
43
|
+
const database = new Database(path);
|
|
44
|
+
try {
|
|
45
|
+
database.exec("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;");
|
|
46
|
+
secureDatabaseFiles(path);
|
|
47
|
+
databases.set(path, database);
|
|
48
|
+
return database;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
try {
|
|
51
|
+
database.close();
|
|
52
|
+
} catch {}
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function closeBalancerDatabase(path: string) {
|
|
58
|
+
const existing = databases.get(path);
|
|
59
|
+
if (!existing) return;
|
|
60
|
+
|
|
61
|
+
databases.delete(path);
|
|
62
|
+
try {
|
|
63
|
+
existing.close();
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (!isClosedDatabaseError(error)) throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { now } from "./time";
|
|
3
|
+
import type { BalancerEvent, BalancerEventType } from "./types";
|
|
4
|
+
|
|
5
|
+
type EventRow = {
|
|
6
|
+
id: string;
|
|
7
|
+
type: BalancerEventType;
|
|
8
|
+
provider_id: string | null;
|
|
9
|
+
alias: string | null;
|
|
10
|
+
message: string;
|
|
11
|
+
created_at: number;
|
|
12
|
+
metadata_json: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function appendEvent(
|
|
16
|
+
db: Database,
|
|
17
|
+
input: {
|
|
18
|
+
type: BalancerEventType;
|
|
19
|
+
providerID?: string;
|
|
20
|
+
alias?: string;
|
|
21
|
+
message: string;
|
|
22
|
+
metadata?: Record<string, string>;
|
|
23
|
+
},
|
|
24
|
+
) {
|
|
25
|
+
const event: BalancerEvent = {
|
|
26
|
+
alias: input.alias,
|
|
27
|
+
createdAt: now(),
|
|
28
|
+
id: crypto.randomUUID(),
|
|
29
|
+
message: input.message,
|
|
30
|
+
metadata: input.metadata ?? {},
|
|
31
|
+
providerID: input.providerID,
|
|
32
|
+
type: input.type,
|
|
33
|
+
};
|
|
34
|
+
db.query(
|
|
35
|
+
`INSERT INTO events (id, type, provider_id, alias, message, created_at, metadata_json)
|
|
36
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
37
|
+
).run(
|
|
38
|
+
event.id,
|
|
39
|
+
event.type,
|
|
40
|
+
event.providerID ?? null,
|
|
41
|
+
event.alias ?? null,
|
|
42
|
+
event.message,
|
|
43
|
+
event.createdAt,
|
|
44
|
+
JSON.stringify(event.metadata),
|
|
45
|
+
);
|
|
46
|
+
return event;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function listEvents(db: Database, limit = 50) {
|
|
50
|
+
return db
|
|
51
|
+
.query<EventRow, [number]>(
|
|
52
|
+
"SELECT * FROM events ORDER BY created_at DESC LIMIT ?",
|
|
53
|
+
)
|
|
54
|
+
.all(limit)
|
|
55
|
+
.map((row) => {
|
|
56
|
+
let metadata: Record<string, string>;
|
|
57
|
+
try {
|
|
58
|
+
metadata = JSON.parse(row.metadata_json) as Record<string, string>;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
throw new Error(`Invalid event metadata JSON for event ${row.id}`, {
|
|
61
|
+
cause: error,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
alias: row.alias ?? undefined,
|
|
67
|
+
createdAt: row.created_at,
|
|
68
|
+
id: row.id,
|
|
69
|
+
message: row.message,
|
|
70
|
+
metadata,
|
|
71
|
+
providerID: row.provider_id ?? undefined,
|
|
72
|
+
type: row.type,
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { now } from "./time";
|
|
3
|
+
|
|
4
|
+
function key(providerID: string) {
|
|
5
|
+
return `native_auth_suppressed_until:${providerID}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function suppressNativeAuthCapture(
|
|
9
|
+
db: Database,
|
|
10
|
+
providerID: string,
|
|
11
|
+
durationMs = 10_000,
|
|
12
|
+
) {
|
|
13
|
+
db.query<unknown, [string, string]>(
|
|
14
|
+
`INSERT INTO settings (key, value) VALUES (?, ?)
|
|
15
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
|
|
16
|
+
).run(key(providerID), String(now() + durationMs));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isNativeAuthCaptureSuppressed(
|
|
20
|
+
db: Database,
|
|
21
|
+
providerID: string,
|
|
22
|
+
) {
|
|
23
|
+
const settingKey = key(providerID);
|
|
24
|
+
const row = db
|
|
25
|
+
.query<{ value: string }, [string]>(
|
|
26
|
+
"SELECT value FROM settings WHERE key = ?",
|
|
27
|
+
)
|
|
28
|
+
.get(settingKey);
|
|
29
|
+
const suppressedUntil = Number(row?.value ?? 0);
|
|
30
|
+
if (Number.isFinite(suppressedUntil) && suppressedUntil > now()) return true;
|
|
31
|
+
if (row)
|
|
32
|
+
db.query<unknown, [string]>("DELETE FROM settings WHERE key = ?").run(
|
|
33
|
+
settingKey,
|
|
34
|
+
);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { now } from "./time";
|
|
3
|
+
|
|
4
|
+
const key = "native_connect_until";
|
|
5
|
+
|
|
6
|
+
export function markNativeConnectInProgress(
|
|
7
|
+
db: Database,
|
|
8
|
+
durationMs = 180_000,
|
|
9
|
+
) {
|
|
10
|
+
db.query<unknown, [string]>(
|
|
11
|
+
`INSERT INTO settings (key, value) VALUES ('native_connect_until', ?)
|
|
12
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
|
|
13
|
+
).run(String(now() + durationMs));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isNativeConnectInProgress(db: Database) {
|
|
17
|
+
const row = db
|
|
18
|
+
.query<{ value: string }, [string]>(
|
|
19
|
+
"SELECT value FROM settings WHERE key = ?",
|
|
20
|
+
)
|
|
21
|
+
.get(key);
|
|
22
|
+
const until = Number(row?.value ?? 0);
|
|
23
|
+
if (Number.isFinite(until) && until > now()) return true;
|
|
24
|
+
if (row)
|
|
25
|
+
db.query<unknown, [string]>("DELETE FROM settings WHERE key = ?").run(key);
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function clearNativeConnectInProgress(db: Database) {
|
|
30
|
+
db.query<unknown, [string]>("DELETE FROM settings WHERE key = ?").run(key);
|
|
31
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function joinPath(...parts: string[]) {
|
|
2
|
+
return parts.filter(Boolean).join("/").replace(/\/+/g, "/");
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function homeDir() {
|
|
6
|
+
return Bun.env.HOME || "/";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function xdgConfigHome() {
|
|
10
|
+
return Bun.env.XDG_CONFIG_HOME || joinPath(homeDir(), ".config");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function xdgDataHome() {
|
|
14
|
+
return Bun.env.XDG_DATA_HOME || joinPath(homeDir(), ".local", "share");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function configDir() {
|
|
18
|
+
const configured = Bun.env.OPENCODE_CONFIG_DIR;
|
|
19
|
+
return configured
|
|
20
|
+
? joinPath(configured)
|
|
21
|
+
: joinPath(xdgConfigHome(), "opencode");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function dataDir() {
|
|
25
|
+
return joinPath(xdgDataHome(), "opencode");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function storePath() {
|
|
29
|
+
return joinPath(configDir(), "balancer.sqlite");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function nativeAuthPath() {
|
|
33
|
+
return joinPath(dataDir(), "auth.json");
|
|
34
|
+
}
|