@aexol/spectral 0.3.1 → 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.
@@ -234,6 +234,17 @@ export class RelayClient extends EventEmitter {
234
234
  this.exit(1);
235
235
  return;
236
236
  }
237
+ // Machine was revoked by the team admin from the Aexol Studio panel
238
+ // (backend sets `KnownMachine.revokedAt` and closes the socket with
239
+ // 4001 "machine-revoked"). Exit immediately — the machine JWT is now
240
+ // invalid and re-registration requires a fresh `spectral serve`.
241
+ if (code === 4001 && reasonStr === "machine-revoked") {
242
+ this.logger.error("\n✗ This machine has been disconnected from the Aexol Studio panel.");
243
+ this.logger.error(" Run `spectral serve` again to re-register.\n");
244
+ this.dispose();
245
+ this.exit(1);
246
+ return;
247
+ }
237
248
  this.scheduleReconnect();
238
249
  });
239
250
  }
@@ -30,6 +30,7 @@ const KNOWN_KEYS = new Set([
30
30
  "machineJwt",
31
31
  "teamId",
32
32
  "ownerId",
33
+ "visibility",
33
34
  "registeredAt",
34
35
  "hostname",
35
36
  "version",
@@ -79,6 +80,7 @@ export async function loadMachine() {
79
80
  machineJwt: parsed.machineJwt,
80
81
  teamId: typeof parsed.teamId === "string" ? parsed.teamId : undefined,
81
82
  ownerId: typeof parsed.ownerId === "string" ? parsed.ownerId : undefined,
83
+ visibility: typeof parsed.visibility === "string" ? parsed.visibility : undefined,
82
84
  registeredAt: parsed.registeredAt,
83
85
  hostname: parsed.hostname,
84
86
  version: parsed.version,
@@ -109,6 +111,8 @@ export async function saveMachine(rec) {
109
111
  toWrite.teamId = rec.teamId;
110
112
  if (rec.ownerId !== undefined)
111
113
  toWrite.ownerId = rec.ownerId;
114
+ if (rec.visibility !== undefined)
115
+ toWrite.visibility = rec.visibility;
112
116
  if (rec.extra) {
113
117
  for (const [k, v] of Object.entries(rec.extra))
114
118
  toWrite[k] = v;
@@ -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
  }
@@ -131,6 +131,7 @@ export async function ensureMachineRegistered(deps) {
131
131
  machineJwt: obj.jwt,
132
132
  teamId: typeof obj.teamId === "string" ? obj.teamId : undefined,
133
133
  ownerId: typeof obj.ownerId === "string" ? obj.ownerId : undefined,
134
+ visibility: typeof obj.visibility === "string" ? obj.visibility : undefined,
134
135
  registeredAt: Date.now(),
135
136
  hostname: hostname(),
136
137
  version: deps.version,
@@ -150,10 +150,20 @@ const MODEL_PRICING = [
150
150
  { prefix: "meta-llama/llama-4", input: 0.20, output: 0.80, cacheWrite: 0, cacheRead: 0 },
151
151
  { prefix: "meta-llama/llama-3.3", input: 0.20, output: 0.50, cacheWrite: 0, cacheRead: 0 },
152
152
  ];
153
+ /**
154
+ * Strip vendor prefix to extract the bare model name.
155
+ * e.g. "openai/gpt-5.3-codex" → "gpt-5.3-codex".
156
+ * Returns the original string when no separator is present.
157
+ */
158
+ function bareModelId(modelId) {
159
+ const idx = modelId.lastIndexOf("/");
160
+ return idx === -1 ? modelId : modelId.slice(idx + 1);
161
+ }
153
162
  /** Look up pricing for a modelId. Returns null when unknown. */
154
163
  function lookupPricing(modelId) {
164
+ const bare = bareModelId(modelId);
155
165
  for (const entry of MODEL_PRICING) {
156
- if (modelId.startsWith(entry.prefix)) {
166
+ if (modelId.startsWith(entry.prefix) || bare.startsWith(entry.prefix)) {
157
167
  return {
158
168
  input: entry.input,
159
169
  output: entry.output,
@@ -177,7 +187,30 @@ const REASONING_SUPPORT_PREFIXES = [
177
187
  ];
178
188
  /** Check if a modelId prefix indicates reasoning/thinking support. */
179
189
  function supportsReasoning(modelId) {
180
- return REASONING_SUPPORT_PREFIXES.some((p) => modelId.startsWith(p));
190
+ const bare = bareModelId(modelId);
191
+ return REASONING_SUPPORT_PREFIXES.some((p) => modelId.startsWith(p) || bare.startsWith(p));
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;
181
214
  }
182
215
  /**
183
216
  * Parse the newline-delimited JSON of wire events persisted alongside an
@@ -283,6 +316,8 @@ export class PiBridge {
283
316
  * envelopes carrying the same modelId don't churn pi's internal state.
284
317
  */
285
318
  lastAppliedModelId;
319
+ /** Current model's credit rates (from availableBaseModels), used for token_usage. */
320
+ activeCreditRates = null;
286
321
  constructor(opts) {
287
322
  this.opts = opts;
288
323
  }
@@ -649,6 +684,16 @@ export class PiBridge {
649
684
  try {
650
685
  await this.session.setModel(model);
651
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;
652
697
  return true;
653
698
  }
654
699
  catch (err) {
@@ -878,8 +923,11 @@ export class PiBridge {
878
923
  this.pending.wireEvents.push(endEvent);
879
924
  this.opts.emit(endEvent);
880
925
  // Emit token usage for this assistant message. pi provides token
881
- // counts via ev.message.usage; cost is computed from the model's
882
- // 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.
883
931
  //
884
932
  // Skip zero-total-usage events — they happen when a turn is
885
933
  // cancelled before the provider starts streaming, and emitting
@@ -901,6 +949,7 @@ export class PiBridge {
901
949
  cacheWriteTokens: usage.cacheWrite ?? 0,
902
950
  totalTokens: usage.totalTokens ?? totalTokens,
903
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),
904
953
  },
905
954
  };
906
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.1",
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,