@adaptiqai/deepseek-harness 0.2.14 → 0.2.15

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/host.js CHANGED
@@ -152,7 +152,7 @@ function classifyFailure(status, raw, headers, model2, options = {}) {
152
152
  if (effective === 503 && detail.code !== null && MODEL_OFFLINE_CODES.includes(detail.code)) {
153
153
  return make(CODES.server, `${model2 ?? "This model"} is offline right now. It will retry automatically.`, "retrying");
154
154
  }
155
- if (has(detail, "AdaptiqNativePreflightBusy", "session_route_unavailable", "conversation_preparing", "preparing", "upload_capacity_exhausted") || effective === 503 && !has(detail, "offline")) {
155
+ if (has(detail, "AdaptiqNativePreflightBusy", "session_route_unavailable", "conversation_preparing", "preparing", "upload_capacity_exhausted", "AdaptiqMemoryNotReady") || effective === 425 || effective === 503 && !has(detail, "offline")) {
156
156
  return make(CODES.preparing, "AdaptiqAI is getting your conversation ready. It will retry automatically.", "preparing");
157
157
  }
158
158
  if (effective === 409 && has(detail, "AdaptiqNativePreflight", "conversation")) {
@@ -373,6 +373,91 @@ function detailMessage(raw) {
373
373
  }
374
374
  return null;
375
375
  }
376
+ var EXPIRED = "The sign-in link expired. Start again.";
377
+ async function startDeviceCode(client, request) {
378
+ let response;
379
+ try {
380
+ response = await client.fetch(`${client.apiBase}/v1/cli-auth/device`, {
381
+ method: "POST",
382
+ headers: { "content-type": "application/json", accept: "application/json" },
383
+ body: JSON.stringify({
384
+ client_app: request.app,
385
+ client_name: request.app,
386
+ device_name: request.device,
387
+ client_binding: request.binding,
388
+ ...request.tokenLifetime === void 0 ? {} : { token_lifetime: request.tokenLifetime }
389
+ }),
390
+ signal: client.signal === void 0 ? AbortSignal.timeout(2e4) : AbortSignal.any([client.signal, AbortSignal.timeout(2e4)]),
391
+ redirect: "error"
392
+ });
393
+ } catch (error) {
394
+ if (client.signal?.aborted === true)
395
+ throw error;
396
+ throw new Error(UNREACHABLE, { cause: error });
397
+ }
398
+ if (!response.ok) {
399
+ const message = detailMessage(await readJson(response));
400
+ if (response.status >= 500)
401
+ throw new Error("AdaptiqAI isn't answering right now. Try again in a minute.");
402
+ throw new Error(message ?? `AdaptiqAI sign-in could not start (HTTP ${response.status}).`);
403
+ }
404
+ const body = await readJson(response);
405
+ const deviceCode = String(body?.device_code ?? "");
406
+ const userCode = String(body?.user_code ?? "");
407
+ const url = String(body?.verification_uri_complete ?? body?.verification_uri ?? "");
408
+ if (deviceCode === "" || userCode === "" || !/^https?:\/\//u.test(url))
409
+ throw new Error("AdaptiqAI returned an incomplete sign-in.");
410
+ return {
411
+ deviceCode,
412
+ userCode,
413
+ url,
414
+ expiresAt: client.now() + Math.min(DEVICE_TIMEOUT_MS, Math.max(60, Number(body?.expires_in ?? 600)) * 1e3),
415
+ interval: Math.max(1, Number(body?.interval ?? 2)) * 1e3
416
+ };
417
+ }
418
+ async function pollDeviceCode(client, code, extra = {}) {
419
+ let response;
420
+ try {
421
+ response = await client.fetch(`${client.apiBase}/v1/cli-auth/device/token`, {
422
+ method: "POST",
423
+ headers: { "content-type": "application/json", accept: "application/json" },
424
+ body: JSON.stringify({ ...extra, device_code: code.deviceCode }),
425
+ signal: client.signal === void 0 ? AbortSignal.timeout(2e4) : AbortSignal.any([client.signal, AbortSignal.timeout(2e4)]),
426
+ redirect: "error"
427
+ });
428
+ } catch {
429
+ return { state: "waiting", delayMs: code.interval * 2 };
430
+ }
431
+ if (response.status === 202)
432
+ return { state: "waiting", delayMs: code.interval };
433
+ if (response.status === 429) {
434
+ const retry = Number(response.headers.get("retry-after") ?? 0) * 1e3;
435
+ return { state: "waiting", delayMs: Math.max(code.interval, retry) + 250 };
436
+ }
437
+ if (response.status === 403) {
438
+ const raw = await readJson(response);
439
+ return { state: "denied", message: detailCode(raw) === "access_denied" ? "The connection was declined in the browser." : detailMessage(raw) ?? "The connection was declined." };
440
+ }
441
+ if (response.status === 410 || response.status === 404)
442
+ return { state: "expired", message: EXPIRED };
443
+ if (!response.ok) {
444
+ if (response.status >= 500)
445
+ return { state: "waiting", delayMs: code.interval * 2 };
446
+ return { state: "failed", message: detailMessage(await readJson(response)) ?? `Sign-in failed (HTTP ${response.status}).` };
447
+ }
448
+ const body = await readJson(response);
449
+ const access2 = String(body?.access_token ?? "");
450
+ const refresh = String(body?.refresh_token ?? "");
451
+ if (!access2.startsWith("ak_") || refresh !== "" && !refresh.startsWith("akr_"))
452
+ return { state: "failed", message: "AdaptiqAI returned an invalid credential." };
453
+ return {
454
+ state: "approved",
455
+ access: access2,
456
+ refresh: refresh === "" ? null : refresh,
457
+ expiresIn: Math.max(60, Number(body?.expires_in ?? 3600)),
458
+ workspaceLabel: typeof body?.workspace_label === "string" && body.workspace_label !== "" ? body.workspace_label : "Your workspace"
459
+ };
460
+ }
376
461
  var AuthManager = class {
377
462
  options;
378
463
  fetch;
@@ -415,6 +500,9 @@ var AuthManager = class {
415
500
  this.state = phase;
416
501
  this.options.onChange?.();
417
502
  }
503
+ client(signal) {
504
+ return { apiBase: this.options.apiBase, fetch: this.fetch, now: this.now, signal };
505
+ }
418
506
  /** Read the stored state once at startup. */
419
507
  async init() {
420
508
  const stored = await this.options.store.read();
@@ -548,39 +636,11 @@ var AuthManager = class {
548
636
  const clientProof = `${randomUUID()}${randomUUID()}`;
549
637
  const binding = createHash("sha256").update(clientProof).digest("hex");
550
638
  const app = this.options.appName ?? "DeepSeek Harness";
551
- let response;
552
- try {
553
- response = await this.fetch(`${this.options.apiBase}/v1/cli-auth/device`, {
554
- method: "POST",
555
- headers: { "content-type": "application/json", accept: "application/json" },
556
- body: JSON.stringify({
557
- client_app: app,
558
- client_name: app,
559
- device_name: deviceLabel(this.options.deviceName ?? hostname()),
560
- client_binding: binding
561
- }),
562
- signal: AbortSignal.any([flow.signal, AbortSignal.timeout(2e4)]),
563
- redirect: "error"
564
- });
565
- } catch (error) {
566
- if (flow.signal.aborted)
567
- throw error;
568
- throw new Error(UNREACHABLE, { cause: error });
569
- }
570
- if (!response.ok) {
571
- const message = detailMessage(await readJson(response));
572
- if (response.status >= 500)
573
- throw new Error("AdaptiqAI isn't answering right now. Try again in a minute.");
574
- throw new Error(message ?? `AdaptiqAI sign-in could not start (HTTP ${response.status}).`);
575
- }
576
- const body = await readJson(response);
577
- const deviceCode = String(body?.device_code ?? "");
578
- const userCode = String(body?.user_code ?? "");
579
- const url = String(body?.verification_uri_complete ?? body?.verification_uri ?? "");
580
- if (deviceCode === "" || userCode === "" || !/^https?:\/\//u.test(url))
581
- throw new Error("AdaptiqAI returned an incomplete sign-in.");
582
- const expiresAt = this.now() + Math.min(DEVICE_TIMEOUT_MS, Math.max(60, Number(body?.expires_in ?? 600)) * 1e3);
583
- const interval = Math.max(1, Number(body?.interval ?? 2)) * 1e3;
639
+ const { deviceCode, userCode, url, expiresAt, interval } = await startDeviceCode(this.client(flow.signal), {
640
+ app,
641
+ device: deviceLabel(this.options.deviceName ?? hostname()),
642
+ binding
643
+ });
584
644
  this.pendingSignIn = { url, userCode, expiresAt };
585
645
  this.set("signing-in");
586
646
  void this.poll(flow, deviceCode, clientProof, interval, expiresAt);
@@ -592,61 +652,27 @@ var AuthManager = class {
592
652
  while (this.now() < expiresAt) {
593
653
  await this.sleep(delay, flow.signal);
594
654
  delay = interval;
595
- let response;
596
- try {
597
- response = await this.fetch(`${this.options.apiBase}/v1/cli-auth/device/token`, {
598
- method: "POST",
599
- headers: { "content-type": "application/json", accept: "application/json" },
600
- body: JSON.stringify({ device_code: deviceCode }),
601
- signal: AbortSignal.any([flow.signal, AbortSignal.timeout(2e4)]),
602
- redirect: "error"
603
- });
604
- } catch {
605
- if (flow.signal.aborted)
606
- return;
607
- delay = interval * 2;
608
- continue;
609
- }
610
- if (response.status === 202)
611
- continue;
612
- if (response.status === 429) {
613
- const retry = Number(response.headers.get("retry-after") ?? 0) * 1e3;
614
- delay = Math.max(interval, retry) + 250;
655
+ const answer = await pollDeviceCode(this.client(flow.signal), { deviceCode, interval });
656
+ if (answer.state === "waiting") {
657
+ delay = answer.delayMs;
615
658
  continue;
616
659
  }
617
- if (response.status === 403) {
618
- const raw = await readJson(response);
619
- this.fail(flow, { code: "denied", message: detailCode(raw) === "access_denied" ? "The connection was declined in the browser." : detailMessage(raw) ?? "The connection was declined." });
620
- return;
621
- }
622
- if (response.status === 410 || response.status === 404) {
623
- this.fail(flow, { code: "expired", message: "The sign-in link expired. Start again." });
660
+ if (answer.state !== "approved") {
661
+ this.fail(flow, { code: answer.state, message: answer.message });
624
662
  return;
625
663
  }
626
- if (!response.ok) {
627
- if (response.status >= 500) {
628
- delay = interval * 2;
629
- continue;
630
- }
631
- this.fail(flow, { code: "failed", message: detailMessage(await readJson(response)) ?? `Sign-in failed (HTTP ${response.status}).` });
632
- return;
633
- }
634
- const body = await readJson(response);
635
- const access2 = String(body?.access_token ?? "");
636
- const refresh = String(body?.refresh_token ?? "");
637
- if (!access2.startsWith("ak_") || !refresh.startsWith("akr_")) {
664
+ if (answer.refresh === null) {
638
665
  this.fail(flow, { code: "failed", message: "AdaptiqAI returned an invalid credential." });
639
666
  return;
640
667
  }
641
- const workspaceLabel = typeof body?.workspace_label === "string" && body.workspace_label !== "" ? body.workspace_label : "Your workspace";
642
668
  const grant = {
643
669
  schema: "adaptiq-dsh-grant-v1",
644
670
  apiBase: this.options.apiBase,
645
- access: access2,
646
- refresh,
671
+ access: answer.access,
672
+ refresh: answer.refresh,
647
673
  clientProof,
648
- expiresAt: this.now() + Math.max(60, Number(body?.expires_in ?? 3600)) * 1e3,
649
- workspaceLabel,
674
+ expiresAt: this.now() + answer.expiresIn * 1e3,
675
+ workspaceLabel: answer.workspaceLabel,
650
676
  obtainedAt: this.now()
651
677
  };
652
678
  await this.options.store.modify(async () => grant);
@@ -655,12 +681,12 @@ var AuthManager = class {
655
681
  this.flow = null;
656
682
  this.pendingSignIn = null;
657
683
  this.source = "device";
658
- this.label = workspaceLabel;
684
+ this.label = answer.workspaceLabel;
659
685
  this.set("connected");
660
- this.options.onSignedIn?.(workspaceLabel);
686
+ this.options.onSignedIn?.(answer.workspaceLabel);
661
687
  return;
662
688
  }
663
- this.fail(flow, { code: "expired", message: "The sign-in link expired. Start again." });
689
+ this.fail(flow, { code: "expired", message: EXPIRED });
664
690
  } catch (error) {
665
691
  if (flow.signal.aborted)
666
692
  return;
@@ -785,6 +811,20 @@ var obj = (value) => typeof value === "object" && value !== null && !Array.isArr
785
811
  var str = (value) => typeof value === "string" && value.trim() !== "" ? value.trim() : null;
786
812
  var num = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
787
813
  var count = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
814
+ function pricesFromRow(raw) {
815
+ const pricing = obj(raw);
816
+ const price = (value) => {
817
+ const parsed = typeof value === "string" && value.trim() !== "" ? Number(value) : typeof value === "number" ? value : Number.NaN;
818
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
819
+ };
820
+ const input = price(pricing?.prompt);
821
+ const memoryWrite = price(pricing?.input_cache_write);
822
+ const memoryRead = price(pricing?.input_cache_read);
823
+ const output = price(pricing?.completion);
824
+ if (input === null || memoryWrite === null || memoryRead === null || output === null)
825
+ return null;
826
+ return { input, memoryWrite, memoryRead, output };
827
+ }
788
828
  var LEVEL_NAMES = {
789
829
  off: "Off",
790
830
  minimal: "Minimal",
@@ -873,17 +913,17 @@ function placementsFromRoutes(routes) {
873
913
  const model2 = str(route?.model);
874
914
  if (route === null || model2 === null || route.enabled !== true)
875
915
  continue;
876
- const add = (a, b) => a === null ? b : b === null ? a : a + b;
916
+ const add2 = (a, b) => a === null ? b : b === null ? a : a + b;
877
917
  const current = out.get(model2);
878
918
  const running = count(route.running);
879
919
  const waiting = count(route.waiting);
880
920
  out.set(model2, {
881
921
  name: current?.name ?? (str(route.name) !== null ? routeModelName(str(route.name)) : null),
882
922
  status: "running",
883
- lanes: add(current?.lanes ?? null, count(route.lanes)),
884
- maxParallel: add(current?.maxParallel ?? null, count(route.max_parallel_agents)),
885
- runningNow: add(current?.runningNow ?? null, running),
886
- waitingNow: add(current?.waitingNow ?? null, waiting),
923
+ lanes: add2(current?.lanes ?? null, count(route.lanes)),
924
+ maxParallel: add2(current?.maxParallel ?? null, count(route.max_parallel_agents)),
925
+ runningNow: add2(current?.runningNow ?? null, running),
926
+ waitingNow: add2(current?.waitingNow ?? null, waiting),
887
927
  region: current?.region ?? str(route.region),
888
928
  card: null,
889
929
  contextWindow: null,
@@ -944,7 +984,8 @@ function model(id, row, placement) {
944
984
  maxParallel: placement?.maxParallel ?? count(gateway?.max_parallel_agents),
945
985
  runningNow: placement?.runningNow ?? null,
946
986
  waitingNow: placement?.waitingNow ?? null,
947
- region: placement?.region ?? str(gateway?.region)
987
+ region: placement?.region ?? str(gateway?.region),
988
+ prices: pricesFromRow(row?.pricing)
948
989
  };
949
990
  }
950
991
  function selectableLevels(reasoning2) {
@@ -1020,6 +1061,60 @@ function laneLimit(offer) {
1020
1061
  return free < offer.minLanes ? 0 : Math.min(offer.maxLanes, free);
1021
1062
  }
1022
1063
 
1064
+ // ../core/lib/processed.js
1065
+ var NOTHING_PROCESSED = { requests: 0, newInput: 0, fromMemory: 0, written: 0, output: 0, reasoning: 0, costUsd: 0 };
1066
+ function stepCost(usage2, prices) {
1067
+ if (prices === null)
1068
+ return null;
1069
+ return usage2.inputTokens * prices.input + (usage2.cacheWriteTokens ?? 0) * prices.memoryWrite + (usage2.cacheReadTokens ?? 0) * prices.memoryRead + usage2.outputTokens * prices.output;
1070
+ }
1071
+ function add(view, step) {
1072
+ return {
1073
+ requests: view.requests + 1,
1074
+ newInput: view.newInput + step.usage.inputTokens,
1075
+ fromMemory: view.fromMemory + (step.usage.cacheReadTokens ?? 0),
1076
+ written: view.written + (step.usage.cacheWriteTokens ?? 0),
1077
+ output: view.output + step.usage.outputTokens,
1078
+ reasoning: view.reasoning + (step.usage.reasoningTokens ?? 0),
1079
+ costUsd: view.costUsd === null || step.costUsd === null ? null : view.costUsd + step.costUsd
1080
+ };
1081
+ }
1082
+ function addStep(record, step, now) {
1083
+ const turnKey = step.turn ?? record?.turnKey ?? null;
1084
+ const fresh = record == null || step.turn !== void 0 && step.turn !== record.turnKey;
1085
+ return {
1086
+ since: record?.since ?? now,
1087
+ at: now,
1088
+ turnKey,
1089
+ turn: add(fresh ? NOTHING_PROCESSED : record.turn, step),
1090
+ session: add(record?.session ?? NOTHING_PROCESSED, step)
1091
+ };
1092
+ }
1093
+ function readProcessed(raw) {
1094
+ const record = typeof raw === "object" && raw !== null ? raw : null;
1095
+ const view = (value) => {
1096
+ const fields = typeof value === "object" && value !== null ? value : null;
1097
+ if (fields === null)
1098
+ return null;
1099
+ const count3 = (name2) => {
1100
+ const field = fields[name2];
1101
+ return typeof field === "number" && Number.isFinite(field) && field >= 0 ? field : null;
1102
+ };
1103
+ const numbers = { requests: count3("requests"), newInput: count3("newInput"), fromMemory: count3("fromMemory"), written: count3("written"), output: count3("output"), reasoning: count3("reasoning") };
1104
+ if (Object.values(numbers).some((value2) => value2 === null))
1105
+ return null;
1106
+ return { ...numbers, costUsd: count3("costUsd") };
1107
+ };
1108
+ const turn = view(record?.turn);
1109
+ const session = view(record?.session);
1110
+ const since = record?.since;
1111
+ const at = record?.at;
1112
+ if (turn === null || session === null || typeof since !== "number" || typeof at !== "number")
1113
+ return null;
1114
+ const turnKey = typeof record?.turnKey === "number" ? record.turnKey : null;
1115
+ return { since, at, turnKey, turn, session };
1116
+ }
1117
+
1023
1118
  // ../core/lib/relations.js
1024
1119
  var PARENT_SESSION_HEADER = "X-Adaptiq-Parent-Session-Id";
1025
1120
  var SESSION_ROLE_HEADER = "X-Adaptiq-Session-Role";
@@ -1108,6 +1203,22 @@ var en = {
1108
1203
  popKnowledge: "Knowledge",
1109
1204
  addKnowledge: "Add knowledge",
1110
1205
  popWorkspace: "All memory on {model}",
1206
+ popSeesNow: "What the model sees now",
1207
+ procTitle: "Processed",
1208
+ procTurn: "Last turn",
1209
+ procSession: "This conversation",
1210
+ procSteps: "{count} steps",
1211
+ procStepsOne: "1 step",
1212
+ procNewInput: "New input",
1213
+ procFromMemory: "Read from memory",
1214
+ procWritten: "Saved to memory",
1215
+ procOutput: "Output",
1216
+ procThinking: "Thinking, part of output",
1217
+ procCost: "Cost",
1218
+ procExplain: "Each step reads the whole memory again. {percent} came from memory.",
1219
+ procExplainFree: "Each step reads the whole memory again. {percent} came from memory, which is free.",
1220
+ procExplainCheaper: "Each step reads the whole memory again. {percent} came from memory, at a fraction of the input price.",
1221
+ procSince: "Counted on this computer since {date}",
1111
1222
  openInKnowledge: "Open in Knowledge",
1112
1223
  kTitle: "Knowledge",
1113
1224
  kSearch: "Search your knowledge",
@@ -1587,6 +1698,21 @@ function button(id, label2, extra = {}) {
1587
1698
  return { id, label: en[label2], labelKey: label2, ...extra };
1588
1699
  }
1589
1700
  var count2 = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
1701
+ var PROCESSED_SAVE_MS = 2e3;
1702
+ function savedProcessed(raw) {
1703
+ const out = {};
1704
+ if (typeof raw !== "object" || raw === null)
1705
+ return out;
1706
+ for (const [id, value] of Object.entries(raw)) {
1707
+ const record = readProcessed(value);
1708
+ if (record !== null)
1709
+ out[id] = record;
1710
+ }
1711
+ return out;
1712
+ }
1713
+ function processedSummary(record) {
1714
+ return record === void 0 ? null : { turn: record.turn, session: record.session, since: record.since };
1715
+ }
1590
1716
  var PRIVATE_HELD = "A private conversation is not kept on AdaptiqAI, so it can't use this before it exists there. Turn Remember on for this conversation to add it.";
1591
1717
  var ATTACH_TRIES = 3;
1592
1718
  function transient(error) {
@@ -1952,6 +2078,7 @@ var Workspace = class {
1952
2078
  listeners = /* @__PURE__ */ new Set();
1953
2079
  state = { preferences: DEFAULT_PREFERENCES, sessions: {} };
1954
2080
  saving = Promise.resolve();
2081
+ persistTimer;
1955
2082
  refreshing = null;
1956
2083
  timer;
1957
2084
  pendingAttach = /* @__PURE__ */ new Map();
@@ -2011,7 +2138,8 @@ var Workspace = class {
2011
2138
  preferences: { ...DEFAULT_PREFERENCES, ...saved.preferences },
2012
2139
  sessions: typeof saved.sessions === "object" && saved.sessions !== null ? saved.sessions : {},
2013
2140
  purchase: saved.purchase ?? null,
2014
- ...saved.defaultModel === "pending" || saved.defaultModel === "done" ? { defaultModel: saved.defaultModel } : {}
2141
+ ...saved.defaultModel === "pending" || saved.defaultModel === "done" ? { defaultModel: saved.defaultModel } : {},
2142
+ processed: savedProcessed(saved.processed)
2015
2143
  };
2016
2144
  this.purchases.restore(saved.purchase);
2017
2145
  }
@@ -2027,6 +2155,10 @@ var Workspace = class {
2027
2155
  this.purchases.dispose();
2028
2156
  clearTimeout(this.timer);
2029
2157
  clearTimeout(this.attachTimer);
2158
+ if (this.persistTimer !== void 0) {
2159
+ clearTimeout(this.persistTimer);
2160
+ void this.persist();
2161
+ }
2030
2162
  for (const session of this.sessions.values()) {
2031
2163
  clearTimeout(session.timer);
2032
2164
  clearTimeout(session.waitTimer);
@@ -2094,7 +2226,8 @@ var Workspace = class {
2094
2226
  region: model2.region,
2095
2227
  nativeMemory: model2.nativeMemory,
2096
2228
  tools: model2.tools,
2097
- memory: this.modelMemory.get(model2.id) ?? null
2229
+ memory: this.modelMemory.get(model2.id) ?? null,
2230
+ prices: model2.prices
2098
2231
  };
2099
2232
  }
2100
2233
  /**
@@ -2704,12 +2837,16 @@ var Workspace = class {
2704
2837
  return next;
2705
2838
  }
2706
2839
  async persist() {
2840
+ clearTimeout(this.persistTimer);
2841
+ this.persistTimer = void 0;
2707
2842
  const entries = Object.entries(this.state.sessions).sort((a, b) => b[1].at - a[1].at).slice(0, 500);
2843
+ const processed = Object.entries(this.state.processed ?? {}).sort((a, b) => b[1].at - a[1].at).slice(0, 500);
2708
2844
  const snapshot = {
2709
2845
  preferences: this.state.preferences,
2710
2846
  sessions: Object.fromEntries(entries),
2711
2847
  purchase: this.state.purchase ?? null,
2712
- ...this.state.defaultModel === void 0 ? {} : { defaultModel: this.state.defaultModel }
2848
+ ...this.state.defaultModel === void 0 ? {} : { defaultModel: this.state.defaultModel },
2849
+ ...processed.length === 0 ? {} : { processed: Object.fromEntries(processed) }
2713
2850
  };
2714
2851
  this.saving = this.saving.then(() => this.deps.store.save(snapshot)).catch((error) => {
2715
2852
  this.deps.log?.("warn", `adaptiq: could not save preferences: ${String(error)}`);
@@ -2820,6 +2957,7 @@ var Workspace = class {
2820
2957
  saved: session.saved,
2821
2958
  optimizing: session.optimizing,
2822
2959
  lastTurn: session.lastTurn,
2960
+ processed: processedSummary(this.state.processed?.[id]),
2823
2961
  knowledge: [...session.knowledge, ...[...session.pendingKnowledge.values()].filter((item) => !session.knowledge.some((entry) => entry.id === item.id))],
2824
2962
  conversations: [...session.conversations, ...[...session.pendingConversations.values()].filter((item) => !session.conversations.some((entry) => entry.id === item.id))],
2825
2963
  known: session.exists,
@@ -3199,10 +3337,19 @@ var Workspace = class {
3199
3337
  this.setPhase(session, "responding");
3200
3338
  this.clearNotices("busy", "preparing", "offline", "retrying");
3201
3339
  }
3202
- usage(sessionId2, model2, usage2) {
3340
+ usage(sessionId2, model2, usage2, step = {}) {
3203
3341
  if (sessionId2 === void 0)
3204
3342
  return;
3205
3343
  const session = this.session(sessionId2);
3344
+ const costUsd = step.costUsd ?? stepCost(usage2, this.catalog.find((entry) => entry.id === model2)?.prices ?? null);
3345
+ const counted = addStep(this.state.processed?.[sessionId2], { usage: usage2, costUsd, ...step.turn === void 0 ? {} : { turn: step.turn } }, this.now());
3346
+ this.state = { ...this.state, processed: { ...this.state.processed, [sessionId2]: counted } };
3347
+ if (this.persistTimer === void 0) {
3348
+ this.persistTimer = setTimeout(() => {
3349
+ void this.persist();
3350
+ }, PROCESSED_SAVE_MS);
3351
+ this.persistTimer.unref?.();
3352
+ }
3206
3353
  session.lastTurn = {
3207
3354
  fromMemory: usage2.cacheReadTokens ?? 0,
3208
3355
  newInput: usage2.inputTokens,
@@ -4542,6 +4689,12 @@ function mapUsage(raw) {
4542
4689
  ...total > 0 ? { totalTokens: total } : prompt + output > 0 ? { totalTokens: prompt + output } : {}
4543
4690
  };
4544
4691
  }
4692
+ function usageCost(raw) {
4693
+ const adaptiq = typeof raw.adaptiq === "object" && raw.adaptiq !== null ? raw.adaptiq : null;
4694
+ const value = adaptiq?.cost_usd;
4695
+ const cost = typeof value === "string" && value.trim() !== "" ? Number(value) : typeof value === "number" ? value : Number.NaN;
4696
+ return Number.isFinite(cost) && cost >= 0 ? cost : null;
4697
+ }
4545
4698
  function memoryNotice(raw) {
4546
4699
  let value = raw;
4547
4700
  if (typeof raw === "string") {
@@ -4573,6 +4726,7 @@ async function* translate(events, hooks) {
4573
4726
  let reasoningField = null;
4574
4727
  let finish = null;
4575
4728
  let usage2 = null;
4729
+ let costUsd = null;
4576
4730
  let started = false;
4577
4731
  let nextIndex = 0;
4578
4732
  const closed = /* @__PURE__ */ new Set();
@@ -4618,6 +4772,7 @@ async function* translate(events, hooks) {
4618
4772
  }
4619
4773
  if (typeof payload.usage === "object" && payload.usage !== null) {
4620
4774
  usage2 = mapUsage(payload.usage);
4775
+ costUsd = usageCost(payload.usage);
4621
4776
  }
4622
4777
  const choices = Array.isArray(payload.choices) ? payload.choices : [];
4623
4778
  const choice = choices[0];
@@ -4679,7 +4834,7 @@ async function* translate(events, hooks) {
4679
4834
  throw hooks.onError({ error: { message: "The model returned no reply.", type: "empty_response", code: "EMPTY_RESPONSE" } });
4680
4835
  }
4681
4836
  if (usage2 !== null) {
4682
- hooks.onUsage?.(usage2);
4837
+ hooks.onUsage?.(usage2, costUsd);
4683
4838
  yield { type: "usage", usage: usage2 };
4684
4839
  }
4685
4840
  const hasTools = blocks.some((block) => block.kind === "tool-call");
@@ -5190,7 +5345,8 @@ var AdaptiqAdapter = class extends LlmAdapter {
5190
5345
  return toLlmError(failure, cause);
5191
5346
  };
5192
5347
  if (sessionId2 !== void 0) this.deps.events.started(sessionId2, options.model);
5193
- if (sessionId2 !== void 0 && options.purpose === void 0) this.deps.noteUserTexts?.(sessionId2, userTurns(options.messages));
5348
+ const written = options.purpose === void 0 ? userTurns(options.messages) : null;
5349
+ if (sessionId2 !== void 0 && written !== null) this.deps.noteUserTexts?.(sessionId2, written);
5194
5350
  try {
5195
5351
  let credential = await this.deps.auth.credential();
5196
5352
  if (credential === void 0) {
@@ -5299,8 +5455,8 @@ var AdaptiqAdapter = class extends LlmAdapter {
5299
5455
  answered = true;
5300
5456
  if (sessionId2 !== void 0) this.deps.events.output(sessionId2);
5301
5457
  },
5302
- onUsage: (usage2) => {
5303
- if (sessionId2 !== void 0) this.deps.events.usage(sessionId2, options.model, usage2);
5458
+ onUsage: (usage2, costUsd) => {
5459
+ if (sessionId2 !== void 0) this.deps.events.usage(sessionId2, options.model, usage2, { costUsd, ...written === null ? {} : { turn: written.length } });
5304
5460
  },
5305
5461
  onError: (payload) => fail2(classifyFailure(null, payload, response?.headers ?? null, name2, { autoSleep: this.deps.autoSleep?.(options.model) === true }))
5306
5462
  });
@@ -6272,7 +6428,7 @@ Open **Settings \u2192 AdaptiqAI** to connect, or run \`/adaptiq signin\`.
6272
6428
  var ENTRY_PROTOCOL = 1;
6273
6429
  var RELOAD_SIGNATURE = true ? "ada3dfffa1b3d7b1" : "source";
6274
6430
  function pluginVersion() {
6275
- if (true) return "0.2.14";
6431
+ if (true) return "0.2.15";
6276
6432
  try {
6277
6433
  return createRequire(import.meta.url)("../package.json").version;
6278
6434
  } catch {
@@ -1,4 +1,4 @@
1
- import type { AccountSnapshot, SessionView } from '@adaptiqai/core/types';
1
+ import type { AccountSnapshot, ProcessedSummary, SessionView, TokenPrices } from '@adaptiqai/core/types';
2
2
  import type { Kit } from './actions.ts';
3
3
  import { type UseProjection } from './model.ts';
4
4
  import { type T } from './ui.tsx';
@@ -13,6 +13,16 @@ export interface PillProps {
13
13
  }
14
14
  /** The pill in the composer toolbar. */
15
15
  export declare function MemoryPill({ t, kit, sessionId, useAqAccount, useAqSession, useProjection }: PillProps): import("react").JSX.Element | null;
16
+ /**
17
+ * What the conversation's requests processed: the latest turn (every agent
18
+ * step since the person's last message) and the whole conversation. Apart from
19
+ * the memory's size, since each step reads all of it again.
20
+ */
21
+ export declare function ProcessedSection({ t, processed, prices }: {
22
+ t: T;
23
+ processed: ProcessedSummary;
24
+ prices: TokenPrices | null;
25
+ }): import("react").JSX.Element;
16
26
  /**
17
27
  * One calm line above the composer when the chosen model is not the one this
18
28
  * conversation's memory lives on: switch back, or start fresh on the new model.
@@ -1,5 +1,5 @@
1
1
  /** The composer pill's words and timing, kept apart from React so they can be tested. */
2
- import type { WaitKind } from '@adaptiqai/core/types';
2
+ import type { ProcessedView, TokenPrices, WaitKind } from '@adaptiqai/core/types';
3
3
  type T = (key: string, params?: Record<string, unknown>) => string;
4
4
  /** How long "+24.6k from memory" stays after a reply. */
5
5
  export declare const AFTER_REPLY_MS = 4000;
@@ -56,4 +56,26 @@ export declare class AfterReply {
56
56
  } | null): void;
57
57
  dispose(): void;
58
58
  }
59
+ /**
60
+ * The popover's one line on why processed tokens outgrow the memory, from the
61
+ * conversation's own numbers and its model's price for reading from memory.
62
+ * @param t - translate.
63
+ * @param view - the conversation's processed tokens.
64
+ * @param prices - the model's per-token prices, when known.
65
+ * @returns e.g. "Each step reads the whole memory again. 96% came from memory, which is free.", or null before anything was read from memory.
66
+ */
67
+ export declare function processedExplain(t: T, view: ProcessedView, prices: TokenPrices | null): string | null;
68
+ /**
69
+ * How many requests (agent steps) a column counts.
70
+ * @param t - translate.
71
+ * @param count - requests.
72
+ * @returns e.g. "85 steps".
73
+ */
74
+ export declare function stepsText(t: T, count: number): string;
75
+ /**
76
+ * A processed cost, or a dash where a request had no price.
77
+ * @param usd - USD, or null.
78
+ * @returns e.g. "$0.24".
79
+ */
80
+ export declare function costText(usd: number | null): string;
59
81
  export {};
@@ -16,7 +16,8 @@ export interface TranslateHooks {
16
16
  onFirstOutput?(): void;
17
17
  /** An in-band error: map it to the thrown failure. */
18
18
  onError(payload: Record<string, unknown>): Error;
19
- onUsage?(usage: TokenUsage): void;
19
+ /** The request's usage, and what the gateway charged for it when the answer says (USD). */
20
+ onUsage?(usage: TokenUsage, costUsd: number | null): void;
20
21
  }
21
22
  /**
22
23
  * Translate decoded SSE events into harness stream chunks.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adaptiqai/deepseek-harness",
3
- "version": "0.2.14",
3
+ "version": "0.2.15",
4
4
  "description": "AdaptiqAI for DeepSeek Harness: models with 5M-token native memory and Knowledge. Sign up, get a model and work without leaving the harness",
5
5
  "keywords": [
6
6
  "dsh-plugin",
@@ -90,7 +90,7 @@
90
90
  "react-dom": "~18.3.1",
91
91
  "typescript": "~5.9.3",
92
92
  "vitest": "~3.2.7",
93
- "@adaptiqai/core": "0.2.10"
93
+ "@adaptiqai/core": "0.2.11"
94
94
  },
95
95
  "repository": {
96
96
  "type": "git",