@schlessera/brain-ui-server 0.19.0 → 0.20.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 +5 -1
- package/data/model-prices.json +63 -0
- package/dist/activity/digest.d.ts.map +1 -1
- package/dist/activity/digest.js +30 -2
- package/dist/activity/digest.js.map +1 -1
- package/dist/activity/notify.d.ts.map +1 -1
- package/dist/activity/notify.js +26 -4
- package/dist/activity/notify.js.map +1 -1
- package/dist/activity/query.d.ts.map +1 -1
- package/dist/activity/query.js +28 -1
- package/dist/activity/query.js.map +1 -1
- package/dist/activity/recorder.d.ts +5 -0
- package/dist/activity/recorder.d.ts.map +1 -1
- package/dist/activity/recorder.js +37 -4
- package/dist/activity/recorder.js.map +1 -1
- package/dist/activity/runtime.d.ts +7 -1
- package/dist/activity/runtime.d.ts.map +1 -1
- package/dist/activity/runtime.js +28 -10
- package/dist/activity/runtime.js.map +1 -1
- package/dist/activity/store.d.ts +51 -2
- package/dist/activity/store.d.ts.map +1 -1
- package/dist/activity/store.js +236 -19
- package/dist/activity/store.js.map +1 -1
- package/dist/activity/stream.d.ts +6 -2
- package/dist/activity/stream.d.ts.map +1 -1
- package/dist/activity/stream.js +42 -1
- package/dist/activity/stream.js.map +1 -1
- package/dist/agent/backend.d.ts +13 -4
- package/dist/agent/backend.d.ts.map +1 -1
- package/dist/agent/backend.js +68 -5
- package/dist/agent/backend.js.map +1 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +18 -3
- package/dist/app.js.map +1 -1
- package/dist/config/env.d.ts +36 -0
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +66 -2
- package/dist/config/env.js.map +1 -1
- package/dist/db/settings.d.ts +15 -0
- package/dist/db/settings.d.ts.map +1 -1
- package/dist/db/settings.js +43 -0
- package/dist/db/settings.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pricing/model-pricing.d.ts +61 -0
- package/dist/pricing/model-pricing.d.ts.map +1 -0
- package/dist/pricing/model-pricing.js +379 -0
- package/dist/pricing/model-pricing.js.map +1 -0
- package/dist/routes/activity.d.ts.map +1 -1
- package/dist/routes/activity.js +40 -32
- package/dist/routes/activity.js.map +1 -1
- package/dist/routes/models.d.ts +6 -0
- package/dist/routes/models.d.ts.map +1 -1
- package/dist/routes/models.js +38 -3
- package/dist/routes/models.js.map +1 -1
- package/dist/ws/run-session.d.ts.map +1 -1
- package/dist/ws/run-session.js +36 -2
- package/dist/ws/run-session.js.map +1 -1
- package/migrations/009_activity_followups.sql +22 -0
- package/migrations/010_effective_cost.sql +21 -0
- package/package.json +4 -3
- package/src/activity/digest.ts +30 -2
- package/src/activity/notify.ts +30 -8
- package/src/activity/query.ts +28 -1
- package/src/activity/recorder.ts +57 -5
- package/src/activity/runtime.ts +41 -13
- package/src/activity/store.ts +316 -21
- package/src/activity/stream.ts +39 -9
- package/src/agent/backend.ts +91 -10
- package/src/app.ts +19 -3
- package/src/config/env.ts +90 -2
- package/src/db/settings.ts +56 -0
- package/src/index.ts +1 -0
- package/src/pricing/model-pricing.ts +497 -0
- package/src/routes/activity.ts +41 -33
- package/src/routes/models.ts +57 -6
- package/src/ws/run-session.ts +45 -3
package/src/routes/models.ts
CHANGED
|
@@ -8,27 +8,45 @@
|
|
|
8
8
|
import type { Logger } from "@opentelemetry/api-logs";
|
|
9
9
|
import { Hono } from "hono";
|
|
10
10
|
import type { Database } from "bun:sqlite";
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
import {
|
|
12
|
+
isBillingMode,
|
|
13
|
+
type BillingMode,
|
|
14
|
+
type ModelCatalogEntry,
|
|
15
|
+
type ModelCatalogResponse,
|
|
14
16
|
} from "@schlessera/brain-ui-sdk";
|
|
15
17
|
import type { BackendRegistry } from "../agent/backend.js";
|
|
16
|
-
import {
|
|
18
|
+
import type { ModelPricingState } from "../pricing/model-pricing.js";
|
|
19
|
+
import {
|
|
20
|
+
getBillingOverrides,
|
|
21
|
+
getHiddenModelIds,
|
|
22
|
+
setBillingOverrides,
|
|
23
|
+
setHiddenModelIds,
|
|
24
|
+
} from "../db/settings.js";
|
|
17
25
|
|
|
18
26
|
export function createModelRoutes(deps: {
|
|
19
27
|
registry: BackendRegistry;
|
|
20
28
|
db: Database;
|
|
29
|
+
/** The shared pricing instance — its state drives the client's staleness indicator. */
|
|
30
|
+
pricing?: { state(): ModelPricingState; ensureFresh(): Promise<void> };
|
|
21
31
|
/** Where refresh failures are reported. */
|
|
22
32
|
log?: Logger;
|
|
23
33
|
}): Hono {
|
|
24
|
-
const { registry, db } = deps;
|
|
34
|
+
const { registry, db, pricing } = deps;
|
|
25
35
|
|
|
26
36
|
async function buildCatalog(): Promise<ModelCatalogResponse> {
|
|
27
37
|
const source = await registry.getModelSource();
|
|
28
38
|
const hidden = new Set(getHiddenModelIds(db));
|
|
39
|
+
const overrides = getBillingOverrides(db);
|
|
40
|
+
// `billingMode` already rides each provider entry (the registry applies the
|
|
41
|
+
// override last); the catalog additionally tags WHICH rows carry an explicit
|
|
42
|
+
// override, so the settings screen can render auto vs forced.
|
|
29
43
|
const models: ModelCatalogEntry[] = (
|
|
30
44
|
await registry.listAllProviders({ includeHidden: true })
|
|
31
|
-
).map((profile) => ({
|
|
45
|
+
).map((profile) => ({
|
|
46
|
+
...profile,
|
|
47
|
+
hidden: hidden.has(profile.id),
|
|
48
|
+
...(overrides[profile.id] ? { billingOverride: overrides[profile.id] } : {}),
|
|
49
|
+
}));
|
|
32
50
|
|
|
33
51
|
const state = source?.state();
|
|
34
52
|
return {
|
|
@@ -51,6 +69,17 @@ export function createModelRoutes(deps: {
|
|
|
51
69
|
return c.json(await buildCatalog());
|
|
52
70
|
})
|
|
53
71
|
|
|
72
|
+
.get("/models/pricing", async (c) => {
|
|
73
|
+
// Absent instance (a host that opted out) → 404; the client hides the
|
|
74
|
+
// indicator on any rejection, so old and opted-out servers degrade alike.
|
|
75
|
+
if (!pricing) return c.json({ error: "pricing not available" }, 404);
|
|
76
|
+
// Kick a refresh behind the response when stale — reading the state is
|
|
77
|
+
// also the natural moment to heal it. Never blocks: ensureFresh awaits
|
|
78
|
+
// only on a true cold start.
|
|
79
|
+
void pricing.ensureFresh().catch(() => {});
|
|
80
|
+
return c.json(pricing.state());
|
|
81
|
+
})
|
|
82
|
+
|
|
54
83
|
.put("/models/hidden", async (c) => {
|
|
55
84
|
const body = (await c.req.json().catch(() => null)) as unknown;
|
|
56
85
|
const hidden = (body as { hidden?: unknown } | null)?.hidden;
|
|
@@ -67,6 +96,28 @@ export function createModelRoutes(deps: {
|
|
|
67
96
|
return c.json(await buildCatalog());
|
|
68
97
|
})
|
|
69
98
|
|
|
99
|
+
.put("/models/billing", async (c) => {
|
|
100
|
+
const body = (await c.req.json().catch(() => null)) as unknown;
|
|
101
|
+
const billing = (body as { billing?: unknown } | null)?.billing;
|
|
102
|
+
if (
|
|
103
|
+
typeof billing !== "object" ||
|
|
104
|
+
billing === null ||
|
|
105
|
+
Array.isArray(billing) ||
|
|
106
|
+
Object.values(billing).some((mode) => !isBillingMode(mode))
|
|
107
|
+
) {
|
|
108
|
+
return c.json(
|
|
109
|
+
{ error: 'billing must map profile ids to "subscription" or "api"' },
|
|
110
|
+
400
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
setBillingOverrides(db, billing as Record<string, BillingMode>);
|
|
115
|
+
// Same memo discipline as the hidden set: the next roster read (and the
|
|
116
|
+
// next run's classification) must see the override immediately.
|
|
117
|
+
registry.invalidateProfiles();
|
|
118
|
+
return c.json(await buildCatalog());
|
|
119
|
+
})
|
|
120
|
+
|
|
70
121
|
.post("/models/refresh", async (c) => {
|
|
71
122
|
const source = await registry.getModelSource();
|
|
72
123
|
if (!source) {
|
package/src/ws/run-session.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
BillingMode,
|
|
3
|
+
ChatImageAttachment,
|
|
4
|
+
ClientEnvironment,
|
|
5
|
+
} from "@schlessera/brain-ui-sdk/protocol";
|
|
6
|
+
import type { BackendRegistry } from "../agent/backend.js";
|
|
2
7
|
import type { WSContext } from "./clients.js";
|
|
3
8
|
import { withSessionId, withTurnScope } from "./frames.js";
|
|
4
9
|
import { makeBridge, emitTurnError } from "./bridge.js";
|
|
@@ -116,7 +121,14 @@ export async function runSession(
|
|
|
116
121
|
turn.timeoutHandle = timeoutHandle;
|
|
117
122
|
|
|
118
123
|
// One recorder per turn identity: a queued follow-up re-mints turnId
|
|
119
|
-
// and gets its own run in the activity record.
|
|
124
|
+
// and gets its own run in the activity record. The recorder is handed
|
|
125
|
+
// the RESOLVED profile — `profileId` here is post pin-drop fallback and
|
|
126
|
+
// carries any session_info re-pin forward between queued turns — plus
|
|
127
|
+
// its billing mode, so the run's root span classifies what actually ran,
|
|
128
|
+
// never what was requested.
|
|
129
|
+
const billing = host.activity
|
|
130
|
+
? await resolveRunBilling(host.registry, backend.id, profileId)
|
|
131
|
+
: undefined;
|
|
120
132
|
const recorder: TurnRecorder | undefined = host.activity
|
|
121
133
|
? createTurnRecorder(
|
|
122
134
|
{
|
|
@@ -124,7 +136,7 @@ export async function runSession(
|
|
|
124
136
|
onWrite: () => host.activity!.stream.pump(),
|
|
125
137
|
log: host.log,
|
|
126
138
|
},
|
|
127
|
-
{ turnId: turn.turnId, sessionId: turn.sessionId }
|
|
139
|
+
{ turnId: turn.turnId, sessionId: turn.sessionId, ...billing }
|
|
128
140
|
)
|
|
129
141
|
: undefined;
|
|
130
142
|
const bridge = makeBridge(host, turn, text, backend.id, recorder);
|
|
@@ -194,6 +206,36 @@ export async function runSession(
|
|
|
194
206
|
}
|
|
195
207
|
}
|
|
196
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Profile identity + billing mode for a turn's root span. When the slot has no
|
|
211
|
+
* resolved profile id (a resumed session whose pin was dropped), the backend
|
|
212
|
+
* runs its default profile — the backend's first roster entry — so THAT
|
|
213
|
+
* profile's identity and billing are recorded, not the dead pin's. Hidden
|
|
214
|
+
* profiles are included: a session pinned to a profile the user later hid
|
|
215
|
+
* still runs on it and must classify as it. Never throws: an unreadable
|
|
216
|
+
* roster records no billing attrs, and the rollup falls back to env
|
|
217
|
+
* classification.
|
|
218
|
+
*/
|
|
219
|
+
async function resolveRunBilling(
|
|
220
|
+
registry: BackendRegistry,
|
|
221
|
+
backendId: string,
|
|
222
|
+
profileId: string | undefined
|
|
223
|
+
): Promise<{ profileId: string; billingMode?: BillingMode } | undefined> {
|
|
224
|
+
try {
|
|
225
|
+
const providers = await registry.listAllProviders({ includeHidden: true });
|
|
226
|
+
const resolved = profileId
|
|
227
|
+
? providers.find((provider) => provider.id === profileId)
|
|
228
|
+
: providers.find((provider) => provider.backendId === backendId);
|
|
229
|
+
if (!resolved) return undefined;
|
|
230
|
+
return {
|
|
231
|
+
profileId: resolved.id,
|
|
232
|
+
...(resolved.billingMode ? { billingMode: resolved.billingMode } : {}),
|
|
233
|
+
};
|
|
234
|
+
} catch {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
197
239
|
/** Queue sizes are only ever reported to a human, so one decimal of MB is plenty. */
|
|
198
240
|
function formatMb(bytes: number): string {
|
|
199
241
|
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|