@aexol/spectral 0.3.2 → 0.3.4

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.
@@ -28,7 +28,7 @@ const cache = new Map();
28
28
  export function clearAllowedModelsCache() {
29
29
  cache.clear();
30
30
  }
31
- const QUERY = `query AvailableBaseModels { availableBaseModels { name provider userModelId agentEnabled } }`;
31
+ const QUERY = `query AvailableBaseModels { availableBaseModels { name provider userModelId agentEnabled creditInputPer1M creditOutputPer1M creditCachedInputPer1M creditCacheReadPer1M creditCacheWritePer1M } }`;
32
32
  /**
33
33
  * Fetch the whitelist of allowed base models. Throws on any failure with a
34
34
  * message tailored for an operator running `spectral serve` — the caller
@@ -97,7 +97,17 @@ export async function fetchAllowedModels(opts) {
97
97
  const provider = typeof row?.provider === "string" ? row.provider : null;
98
98
  if (!name || !provider)
99
99
  continue; // skip malformed rows defensively
100
- const model = { modelId: name, displayName: name, provider };
100
+ const asOptionalNumber = (v) => typeof v === "number" ? v : v == null ? null : undefined;
101
+ const model = {
102
+ modelId: name,
103
+ displayName: name,
104
+ provider,
105
+ creditInputPer1M: asOptionalNumber(row?.creditInputPer1M),
106
+ creditOutputPer1M: asOptionalNumber(row?.creditOutputPer1M),
107
+ creditCachedInputPer1M: asOptionalNumber(row?.creditCachedInputPer1M),
108
+ creditCacheReadPer1M: asOptionalNumber(row?.creditCacheReadPer1M),
109
+ creditCacheWritePer1M: asOptionalNumber(row?.creditCacheWritePer1M),
110
+ };
101
111
  if (typeof row?.userModelId === "string") {
102
112
  model.userModelId = row.userModelId;
103
113
  }
@@ -190,6 +190,28 @@ function supportsReasoning(modelId) {
190
190
  const bare = bareModelId(modelId);
191
191
  return REASONING_SUPPORT_PREFIXES.some((p) => modelId.startsWith(p) || bare.startsWith(p));
192
192
  }
193
+ /**
194
+ * Calculate credits from token usage using per-model credit rates.
195
+ * Mirrors backend billing logic for Agent UI telemetry.
196
+ */
197
+ function calculateCredits(inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, creditInputPer1M, creditOutputPer1M, creditCachedInputPer1M, creditCacheReadPer1M, creditCacheWritePer1M) {
198
+ const inputRate = creditInputPer1M ?? 0;
199
+ const outputRate = creditOutputPer1M ?? 0;
200
+ const cachedInputRate = creditCachedInputPer1M ?? 0;
201
+ const cacheReadRate = creditCacheReadPer1M ?? 0;
202
+ const cacheWriteRate = creditCacheWritePer1M ?? 0;
203
+ const credits = (inputTokens / 1_000_000) * inputRate +
204
+ (outputTokens / 1_000_000) * outputRate +
205
+ (cacheReadTokens / 1_000_000) * cacheReadRate +
206
+ (cacheWriteTokens / 1_000_000) * cacheWriteRate;
207
+ if (inputRate === 0 && outputRate === 0 && cacheReadRate === 0 && cacheWriteRate === 0) {
208
+ if (cachedInputRate > 0) {
209
+ return Math.round(((inputTokens + cacheReadTokens + cacheWriteTokens) / 1_000_000) * cachedInputRate * 100) / 100;
210
+ }
211
+ return 0;
212
+ }
213
+ return Math.round(credits * 100) / 100;
214
+ }
193
215
  /**
194
216
  * Parse the newline-delimited JSON of wire events persisted alongside an
195
217
  * assistant message and extract all tool_call / tool_result events.
@@ -294,6 +316,8 @@ export class PiBridge {
294
316
  * envelopes carrying the same modelId don't churn pi's internal state.
295
317
  */
296
318
  lastAppliedModelId;
319
+ /** Current model's credit rates (from availableBaseModels), used for token_usage. */
320
+ activeCreditRates = null;
297
321
  constructor(opts) {
298
322
  this.opts = opts;
299
323
  }
@@ -660,6 +684,16 @@ export class PiBridge {
660
684
  try {
661
685
  await this.session.setModel(model);
662
686
  this.lastAppliedModelId = modelId;
687
+ const selected = this.allowedModels?.find((m) => m.modelId === modelId);
688
+ this.activeCreditRates = selected
689
+ ? {
690
+ creditInputPer1M: selected.creditInputPer1M ?? null,
691
+ creditOutputPer1M: selected.creditOutputPer1M ?? null,
692
+ creditCachedInputPer1M: selected.creditCachedInputPer1M ?? null,
693
+ creditCacheReadPer1M: selected.creditCacheReadPer1M ?? null,
694
+ creditCacheWritePer1M: selected.creditCacheWritePer1M ?? null,
695
+ }
696
+ : null;
663
697
  return true;
664
698
  }
665
699
  catch (err) {
@@ -889,8 +923,11 @@ export class PiBridge {
889
923
  this.pending.wireEvents.push(endEvent);
890
924
  this.opts.emit(endEvent);
891
925
  // Emit token usage for this assistant message. pi provides token
892
- // counts via ev.message.usage; cost is computed from the model's
893
- // configured pricing (or null when unavailable).
926
+ // counts via ev.message.usage; credits are computed from the active
927
+ // model's configured credit rates.
928
+ //
929
+ // NOTE: `usage.cost` is still forwarded as a legacy wire field for
930
+ // backward compatibility, but the UI is credits-first.
894
931
  //
895
932
  // Skip zero-total-usage events — they happen when a turn is
896
933
  // cancelled before the provider starts streaming, and emitting
@@ -912,6 +949,7 @@ export class PiBridge {
912
949
  cacheWriteTokens: usage.cacheWrite ?? 0,
913
950
  totalTokens: usage.totalTokens ?? totalTokens,
914
951
  cost: usage.cost?.total ?? null,
952
+ creditsUsed: calculateCredits(usage.input ?? 0, usage.output ?? 0, usage.cacheRead ?? 0, usage.cacheWrite ?? 0, this.activeCreditRates?.creditInputPer1M, this.activeCreditRates?.creditOutputPer1M, this.activeCreditRates?.creditCachedInputPer1M, this.activeCreditRates?.creditCacheReadPer1M, this.activeCreditRates?.creditCacheWritePer1M),
915
953
  },
916
954
  };
917
955
  this.pending.wireEvents.push(usageEvent);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexol/spectral",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Always-on coding agent for Aexol — branded pi wrapper with relay-based browser access.",
5
5
  "type": "module",
6
6
  "private": false,