@aexol/spectral 0.3.2 → 0.3.5

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,31 @@ 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
+ *
196
+ * NOTE: We intentionally keep higher precision here (no 2-decimal rounding)
197
+ * so small turns don't collapse to 0.00 in live UI aggregation. Rendering
198
+ * layers decide final display precision.
199
+ */
200
+ function calculateCredits(inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, creditInputPer1M, creditOutputPer1M, creditCachedInputPer1M, creditCacheReadPer1M, creditCacheWritePer1M) {
201
+ const inputRate = creditInputPer1M ?? 0;
202
+ const outputRate = creditOutputPer1M ?? 0;
203
+ const cachedInputRate = creditCachedInputPer1M ?? 0;
204
+ const cacheReadRate = creditCacheReadPer1M ?? 0;
205
+ const cacheWriteRate = creditCacheWritePer1M ?? 0;
206
+ const credits = (inputTokens / 1_000_000) * inputRate +
207
+ (outputTokens / 1_000_000) * outputRate +
208
+ (cacheReadTokens / 1_000_000) * cacheReadRate +
209
+ (cacheWriteTokens / 1_000_000) * cacheWriteRate;
210
+ if (inputRate === 0 && outputRate === 0 && cacheReadRate === 0 && cacheWriteRate === 0) {
211
+ if (cachedInputRate > 0) {
212
+ return ((inputTokens + cacheReadTokens + cacheWriteTokens) / 1_000_000) * cachedInputRate;
213
+ }
214
+ return 0;
215
+ }
216
+ return credits;
217
+ }
193
218
  /**
194
219
  * Parse the newline-delimited JSON of wire events persisted alongside an
195
220
  * assistant message and extract all tool_call / tool_result events.
@@ -294,6 +319,8 @@ export class PiBridge {
294
319
  * envelopes carrying the same modelId don't churn pi's internal state.
295
320
  */
296
321
  lastAppliedModelId;
322
+ /** Current model's credit rates (from availableBaseModels), used for token_usage. */
323
+ activeCreditRates = null;
297
324
  constructor(opts) {
298
325
  this.opts = opts;
299
326
  }
@@ -660,6 +687,16 @@ export class PiBridge {
660
687
  try {
661
688
  await this.session.setModel(model);
662
689
  this.lastAppliedModelId = modelId;
690
+ const selected = this.allowedModels?.find((m) => m.modelId === modelId);
691
+ this.activeCreditRates = selected
692
+ ? {
693
+ creditInputPer1M: selected.creditInputPer1M ?? null,
694
+ creditOutputPer1M: selected.creditOutputPer1M ?? null,
695
+ creditCachedInputPer1M: selected.creditCachedInputPer1M ?? null,
696
+ creditCacheReadPer1M: selected.creditCacheReadPer1M ?? null,
697
+ creditCacheWritePer1M: selected.creditCacheWritePer1M ?? null,
698
+ }
699
+ : null;
663
700
  return true;
664
701
  }
665
702
  catch (err) {
@@ -889,8 +926,11 @@ export class PiBridge {
889
926
  this.pending.wireEvents.push(endEvent);
890
927
  this.opts.emit(endEvent);
891
928
  // 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).
929
+ // counts via ev.message.usage; credits are computed from the active
930
+ // model's configured credit rates.
931
+ //
932
+ // NOTE: `usage.cost` is still forwarded as a legacy wire field for
933
+ // backward compatibility, but the UI is credits-first.
894
934
  //
895
935
  // Skip zero-total-usage events — they happen when a turn is
896
936
  // cancelled before the provider starts streaming, and emitting
@@ -912,6 +952,7 @@ export class PiBridge {
912
952
  cacheWriteTokens: usage.cacheWrite ?? 0,
913
953
  totalTokens: usage.totalTokens ?? totalTokens,
914
954
  cost: usage.cost?.total ?? null,
955
+ 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
956
  },
916
957
  };
917
958
  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.5",
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,