@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.
package/dist/relay/client.js
CHANGED
|
@@ -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
|
|
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,
|
package/dist/server/pi-bridge.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
|
882
|
-
// configured
|
|
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);
|