dsh-workbuddy-bridge 0.2.0 → 0.3.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/lib/index.d.ts CHANGED
@@ -1,9 +1,51 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
2
  import "@earendil-works/pi-ai";
3
3
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
4
+ import { IncomingMessage, ServerResponse } from "node:http";
4
5
  import { Context, Volatile } from "@deepseek-ai/cordis";
5
6
  import { AttachmentStore } from "@deepseek-ai/dsh-attachment";
6
7
  //#region src/shared/paths.d.ts
8
+ /**
9
+ * Plugin-owned per-message credit route.
10
+ *
11
+ * A read-only GET taking `?sessionId=…`, answering `messageId → credit` for one
12
+ * Session. It is its own route rather than a field of the status document
13
+ * because the two have different lifetimes: the status document is per
14
+ * *account* and is re-read when the account changes, while credit accounting
15
+ * is per *Session* and grows as messages are sent.
16
+ *
17
+ * **One route for both variants, on purpose.** The accounting is attributed to
18
+ * a DSH `messageId` before it is stored, and a message id belongs to one
19
+ * Session regardless of which provider served it — a conversation can switch
20
+ * between the two WorkBuddy products (or away from both) mid-thread. A route
21
+ * per variant would force the message row to know which provider answered a
22
+ * message *it is merely labelling*, and would strand every message whose turn
23
+ * ran under the other variant.
24
+ *
25
+ * No key is required: the route only reads what this plugin already observed on
26
+ * its own loopback stream, carries no credential material, and answers loopback
27
+ * requests through the same Host/Origin guard as the status route.
28
+ */
29
+ declare const WORKBUDDY_CREDIT_PATH = "/plugins/dsh-workbuddy-bridge/credit";
30
+ /**
31
+ * The credit route's answer: one Session's per-message costs.
32
+ *
33
+ * `credits` is keyed by DSH `messageId` and is always present (an empty object
34
+ * when nothing is recorded), so the browser half reads one shape instead of
35
+ * distinguishing "no data yet" from "no costs".
36
+ *
37
+ * A message *absent* from `credits` means this plugin observed no cost for it —
38
+ * a message predating the feature, a stream that failed before its final frame,
39
+ * or an upstream answer that carried no `credit` field. It never means zero, so
40
+ * the row renders nothing rather than a confident `0`.
41
+ *
42
+ * Distinct from {@link WorkBuddyWebCredits}, which is the account's *remaining*
43
+ * balance for the card. This one is what individual messages *spent*.
44
+ */
45
+ interface WorkBuddyWebSessionCredits {
46
+ sessionId: string;
47
+ credits: Readonly<Record<string, number>>;
48
+ }
7
49
  /**
8
50
  * Why no credential is usable, as a closed enum the browser half switches on.
9
51
  *
@@ -1234,6 +1276,15 @@ interface WorkBuddyShimOptions {
1234
1276
  client: Pick<WorkBuddyUpstreamClient, 'chatStream'>;
1235
1277
  catalog: WorkBuddyCatalog;
1236
1278
  logger?: ShimLogger;
1279
+ /**
1280
+ * Observe what one upstream response cost, as the upstream reports it.
1281
+ *
1282
+ * Called at most once per response, at the frame carrying the number, with
1283
+ * the stream's own response id (`cmb-…`) — the same id DSH records on the
1284
+ * assistant message, which is what lets the Host half attribute a cost to a
1285
+ * message instead of guessing.
1286
+ */
1287
+ observeCredit?: (responseId: string, credit: number) => void;
1237
1288
  }
1238
1289
  /**
1239
1290
  * Start the loopback endpoint. Requests carry any bearer; the loopback bind
@@ -1292,6 +1343,129 @@ interface WorkBuddyAdapter {
1292
1343
  */
1293
1344
  declare function createWorkBuddyAdapter(options: WorkBuddyAdapterOptions): WorkBuddyAdapter;
1294
1345
  //#endregion
1346
+ //#region src/llm/credit-log.d.ts
1347
+ /**
1348
+ * Per-message credit accounting, as two plain registries the Host half fills.
1349
+ *
1350
+ * WorkBuddy reports what a message actually cost as a `credit` field on the
1351
+ * last usage frame of its OpenAI-compatible SSE stream. That number is
1352
+ * **server-supplied**: the desktop app accumulates `cost.amount` stage
1353
+ * increments and never multiplies tokens by the catalog's `x0.79` multiplier
1354
+ * (the multiplier is display copy). So the plugin must not invent a formula —
1355
+ * it observes the number where it exists.
1356
+ *
1357
+ * The field travels on the stream the shim already proxies, and the same
1358
+ * stream carries the upstream response id (`cmb-…`) that DSH records on the
1359
+ * assistant message as `source.replayState.response.responseId`. That pair is
1360
+ * what makes attribution exact rather than guessed:
1361
+ *
1362
+ * ```text
1363
+ * SSE id + SSE usage.credit ──► creditLog.record(id, credit)
1364
+ * │
1365
+ * session/event assistant/message ─────┴──► index.set(sessionId, messageId, credit)
1366
+ * (messageId, responseId) │
1367
+ * browser row fetches by messageId
1368
+ * ```
1369
+ *
1370
+ * Both structures are pure data with no Host dependencies, so the matching
1371
+ * rules are unit-testable without a Session, a socket, or an upstream.
1372
+ *
1373
+ * @module dsh-workbuddy-bridge/credit-log
1374
+ */
1375
+ /**
1376
+ * Bounded `responseId → credit` registry, with one-shot waiters.
1377
+ *
1378
+ * The waiter exists because the two writes race in principle: the shim sees the
1379
+ * credit when the last SSE frame arrives, while the Session event that names
1380
+ * the response id is appended when the stream settles. Either order must
1381
+ * produce the same attribution, so a lookup that misses registers interest and
1382
+ * is answered by the next `record` instead of silently reporting "unknown".
1383
+ */
1384
+ declare class WorkBuddyCreditLog {
1385
+ private readonly limit;
1386
+ /**
1387
+ * Insertion-ordered, so eviction is FIFO. A `Map` is used rather than an
1388
+ * object because the response id is attacker-influenced text: a plain object
1389
+ * would let `__proto__` reach the prototype chain.
1390
+ */
1391
+ private readonly credits;
1392
+ /** Pending attributions, keyed by response id; each resolves at most once. */
1393
+ private readonly waiters;
1394
+ constructor(limit?: number);
1395
+ /**
1396
+ * Remember what one response cost.
1397
+ *
1398
+ * A non-finite or negative value is refused rather than stored: the field is
1399
+ * remote input, and a `NaN` reaching the browser would render as `NaN` in the
1400
+ * credit label instead of being reported as unknown.
1401
+ */
1402
+ record(responseId: string, credit: number): void;
1403
+ /** What one response cost, or undefined when it was never observed. */
1404
+ lookup(responseId: string): number | undefined;
1405
+ /**
1406
+ * Call `onCredit` once, when this response's credit becomes known.
1407
+ *
1408
+ * @returns a canceller; calling it after the credit landed is a no-op.
1409
+ */
1410
+ await(responseId: string, onCredit: (credit: number) => void): () => void;
1411
+ /** Drop the oldest entries until the log is inside its bound. */
1412
+ private evict;
1413
+ }
1414
+ /**
1415
+ * `sessionId → messageId → credit`, the shape the browser row reads.
1416
+ *
1417
+ * Nested rather than a flat `messageId → credit` map so the route can answer
1418
+ * one Session without shipping every Session's accounting, and so a Session's
1419
+ * rows can be dropped as a unit.
1420
+ */
1421
+ declare class WorkBuddySessionCreditIndex {
1422
+ private readonly perSessionLimit;
1423
+ /** Per-Session maps, each bounded independently. */
1424
+ private readonly sessions;
1425
+ constructor(perSessionLimit?: number);
1426
+ /** Attribute one message's cost, refreshing its position in its Session. */
1427
+ set(sessionId: string, messageId: string, credit: number): void;
1428
+ /**
1429
+ * One Session's accounting as a plain object, or an empty object when the
1430
+ * Session has none. Always an object, never undefined: the browser half then
1431
+ * needs no "no data yet" branch distinct from "no credits recorded".
1432
+ */
1433
+ forSession(sessionId: string): Record<string, number>;
1434
+ /** Forget one Session's accounting, e.g. when it leaves the store. */
1435
+ delete(sessionId: string): void;
1436
+ }
1437
+ //#endregion
1438
+ //#region src/web/credit.d.ts
1439
+ /** Constructor dependencies. */
1440
+ interface WorkBuddyCreditRouteOptions {
1441
+ /** The accounting to read from; see {@link WorkBuddySessionCreditIndex}. */
1442
+ credits: WorkBuddySessionCreditIndex;
1443
+ /**
1444
+ * Route path to mount, defaulting to the CN variant's. The international
1445
+ * variant passes its own so the two halves never share a route: the browser
1446
+ * bundle and the host bundle are built independently, and a computed path is
1447
+ * one build-config drift away from asking a route that was never mounted.
1448
+ */
1449
+ path?: string;
1450
+ }
1451
+ /**
1452
+ * One Session's accounting.
1453
+ *
1454
+ * A missing `sessionId` is answered with an empty document rather than an
1455
+ * error: the browser half asks on behalf of whatever Session it is rendering,
1456
+ * and "this Session has no recorded costs" is a normal, silent state — the row
1457
+ * simply shows no label.
1458
+ *
1459
+ * @param deps - the accounting index.
1460
+ * @param sessionId - the Session to answer for.
1461
+ * @returns the document the browser half renders from.
1462
+ */
1463
+ declare function workBuddySessionCredits(deps: WorkBuddyCreditRouteOptions, sessionId: string): WorkBuddyWebSessionCredits;
1464
+ /** The credit route's request handler, extracted so tests can mount it bare. */
1465
+ declare function workBuddyCreditHandler(deps: WorkBuddyCreditRouteOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
1466
+ /** Mount the GET credit route on an optional webServer context. */
1467
+ declare function registerWorkBuddyCreditRoute(ctx: Context, deps: WorkBuddyCreditRouteOptions): void;
1468
+ //#endregion
1295
1469
  //#region src/catalog/store.d.ts
1296
1470
  /** Basename of the CN variant's saved catalog inside the Harness home. */
1297
1471
  declare const WORKBUDDY_CATALOG_FILENAME = ".workbuddy-catalog.json";
@@ -1639,4 +1813,4 @@ declare function visibilityAccountOf(credential: Pick<WorkBuddyCredential, 'uid'
1639
1813
  */
1640
1814
  declare function apply(ctx: Context, refs: ConfigRefs): void;
1641
1815
  //#endregion
1642
- export { AI_VARIANT, type AppVersionInfo, CN_APP_VERSION_FILENAME, CN_VARIANT, type ChatIdentity, Config, ConfigRefs, FALLBACK_CN_APP_VERSION, FALLBACK_WORKBUDDY_AI_MODELS, FALLBACK_WORKBUDDY_MODELS, PROBE_EFFORT_CANDIDATES, type ProbeAttempt, type ProbeOutcome, type ProbeSender, type ResolveChatIdentityOptions, type UpstreamErrorKind, WORKBUDDY_APP_VERSION_FILENAME, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CATALOG_FILENAME, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_PROBE_FILENAME, WORKBUDDY_PROVIDER, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_VARIANTS, WORKBUDDY_VISIBILITY_FILENAME, type WorkBuddyAdapter, type WorkBuddyAppVersionSource, type WorkBuddyAuthStatus, WorkBuddyCatalog, type WorkBuddyCatalogFetch, WorkBuddyCatalogStore, type WorkBuddyChatResult, type WorkBuddyCredential, WorkBuddyCredentialStore, type WorkBuddyCredits, type WorkBuddyEffort, type WorkBuddyHostHeartbeat, type WorkBuddyModelBilling, type WorkBuddyModelInfo, type WorkBuddyModelReasoning, type WorkBuddyProbeRecord, WorkBuddyProbeService, type WorkBuddyProbeStatus, WorkBuddyProbeStore, type WorkBuddyProbeValidation, type WorkBuddyPromotion, type WorkBuddyRefreshOutcome, type WorkBuddyShim, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyVariant, WorkBuddyVisibilityStore, appUserAgent, apply, chatUserAgent, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthPath, desktopAuthCandidatesFor, fallbackChatIdentity, fingerprintModel, inject, installedAppVersion, isHeartbeatProcessAlive, modelWithCurrentPromotion, name, normalizeCredits, parseModelCatalog, parseWorkBuddyAuth, prepareChatBody, prepareInternationalChatBody, probeModel, processStartTimeMs, randomSentinel, readBundleVersion, readCliVersion, readHostHeartbeat, regionOf, resolveAppVersion, resolveChatIdentity, validAppVersion, validCliVersion, variantFor, visibilityAccountOf, workbuddyCatalogPath, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, workbuddyProbePath, workbuddyVisibilityPath };
1816
+ export { AI_VARIANT, type AppVersionInfo, CN_APP_VERSION_FILENAME, CN_VARIANT, type ChatIdentity, Config, ConfigRefs, FALLBACK_CN_APP_VERSION, FALLBACK_WORKBUDDY_AI_MODELS, FALLBACK_WORKBUDDY_MODELS, PROBE_EFFORT_CANDIDATES, type ProbeAttempt, type ProbeOutcome, type ProbeSender, type ResolveChatIdentityOptions, type UpstreamErrorKind, WORKBUDDY_APP_VERSION_FILENAME, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CATALOG_FILENAME, WORKBUDDY_CREDIT_PATH, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_PROBE_FILENAME, WORKBUDDY_PROVIDER, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_VARIANTS, WORKBUDDY_VISIBILITY_FILENAME, type WorkBuddyAdapter, type WorkBuddyAppVersionSource, type WorkBuddyAuthStatus, WorkBuddyCatalog, type WorkBuddyCatalogFetch, WorkBuddyCatalogStore, type WorkBuddyChatResult, type WorkBuddyCredential, WorkBuddyCredentialStore, WorkBuddyCreditLog, type WorkBuddyCreditRouteOptions, type WorkBuddyCredits, type WorkBuddyEffort, type WorkBuddyHostHeartbeat, type WorkBuddyModelBilling, type WorkBuddyModelInfo, type WorkBuddyModelReasoning, type WorkBuddyProbeRecord, WorkBuddyProbeService, type WorkBuddyProbeStatus, WorkBuddyProbeStore, type WorkBuddyProbeValidation, type WorkBuddyPromotion, type WorkBuddyRefreshOutcome, WorkBuddySessionCreditIndex, type WorkBuddyShim, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyVariant, WorkBuddyVisibilityStore, type WorkBuddyWebSessionCredits, appUserAgent, apply, chatUserAgent, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthPath, desktopAuthCandidatesFor, fallbackChatIdentity, fingerprintModel, inject, installedAppVersion, isHeartbeatProcessAlive, modelWithCurrentPromotion, name, normalizeCredits, parseModelCatalog, parseWorkBuddyAuth, prepareChatBody, prepareInternationalChatBody, probeModel, processStartTimeMs, randomSentinel, readBundleVersion, readCliVersion, readHostHeartbeat, regionOf, registerWorkBuddyCreditRoute, resolveAppVersion, resolveChatIdentity, validAppVersion, validCliVersion, variantFor, visibilityAccountOf, workBuddyCreditHandler, workBuddySessionCredits, workbuddyCatalogPath, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, workbuddyProbePath, workbuddyVisibilityPath };