@aexol/spectral 0.2.22 → 0.3.1

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.
@@ -48,8 +48,8 @@
48
48
  * a single pi session also work normally (the same AgentSession
49
49
  * instance is reused across `prompt()` calls).
50
50
  */
51
- import { AuthStorage, createAgentSession, DefaultResourceLoader, ModelRegistry, SessionManager, } from "@mariozechner/pi-coding-agent";
52
51
  import { createJiti } from "@mariozechner/jiti";
52
+ import { AuthStorage, createAgentSession, DefaultResourceLoader, ModelRegistry, SessionManager, } from "@mariozechner/pi-coding-agent";
53
53
  import { randomUUID } from "node:crypto";
54
54
  import { existsSync } from "node:fs";
55
55
  import { dirname, resolve } from "node:path";
@@ -124,6 +124,12 @@ const MODEL_PRICING = [
124
124
  { prefix: "claude-3-sonnet", input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
125
125
  { prefix: "claude-3-haiku", input: 0.25, output: 1.25, cacheWrite: 1.25, cacheRead: 0.025 },
126
126
  // OpenAI models
127
+ { prefix: "gpt-5.5", input: 1.75, output: 14, cacheWrite: 14, cacheRead: 0.35 },
128
+ { prefix: "gpt-5.4", input: 1.75, output: 14, cacheWrite: 14, cacheRead: 0.35 },
129
+ { prefix: "gpt-5.3", input: 1.75, output: 14, cacheWrite: 14, cacheRead: 0.35 },
130
+ { prefix: "gpt-5.2", input: 1.75, output: 14, cacheWrite: 14, cacheRead: 0.35 },
131
+ { prefix: "gpt-5.1", input: 2, output: 8, cacheWrite: 8, cacheRead: 0.50 },
132
+ { prefix: "gpt-5", input: 1.25, output: 10, cacheWrite: 10, cacheRead: 0.25 },
127
133
  { prefix: "gpt-4.1", input: 2, output: 8, cacheWrite: 8, cacheRead: 0.50 },
128
134
  { prefix: "gpt-4o", input: 2.50, output: 10, cacheWrite: 10, cacheRead: 1.25 },
129
135
  { prefix: "gpt-4-turbo", input: 10, output: 30, cacheWrite: 0, cacheRead: 0 },
@@ -158,6 +164,21 @@ function lookupPricing(modelId) {
158
164
  }
159
165
  return null;
160
166
  }
167
+ /**
168
+ * Model prefixes known to support reasoning/thinking.
169
+ * Mirrors pi-ai's supportsXhigh() + additional models.
170
+ */
171
+ const REASONING_SUPPORT_PREFIXES = [
172
+ "gpt-5.2", "gpt-5.3", "gpt-5.4", "gpt-5.5",
173
+ "claude-opus-4",
174
+ "o3", "o4",
175
+ "deepseek-r1",
176
+ "gemini-2.5",
177
+ ];
178
+ /** Check if a modelId prefix indicates reasoning/thinking support. */
179
+ function supportsReasoning(modelId) {
180
+ return REASONING_SUPPORT_PREFIXES.some((p) => modelId.startsWith(p));
181
+ }
161
182
  /**
162
183
  * Parse the newline-delimited JSON of wire events persisted alongside an
163
184
  * assistant message and extract all tool_call / tool_result events.
@@ -249,6 +270,12 @@ export class PiBridge {
249
270
  * `session.setModel()`. Phase 3 (Available Models whitelist).
250
271
  */
251
272
  modelRegistry;
273
+ /**
274
+ * Raw allowed models list from the backend, preserved in sortOrder.
275
+ * Used by `getFirstAvailableModelId()` to return the backend-curated
276
+ * top pick when no explicit model selection is made.
277
+ */
278
+ allowedModels;
252
279
  /**
253
280
  * Last `modelId` we successfully applied via `session.setModel()`, or
254
281
  * `undefined` if we never applied one (pi falls back to its own settings
@@ -426,6 +453,7 @@ export class PiBridge {
426
453
  throw new Error(`Failed to fetch allowed models from backend; check SPECTRAL_BACKEND_URL ` +
427
454
  `and machine JWT. Underlying error: ${e.message}`);
428
455
  }
456
+ this.allowedModels = allowedModels;
429
457
  this.registerSyntheticProviders(allowedModels);
430
458
  console.info(`✓ Inference routed via backend proxy (${allowedModels.length} model(s) available)`);
431
459
  const result = await createAgentSession({
@@ -490,7 +518,7 @@ export class PiBridge {
490
518
  // at our synthetic proxy provider so auth resolves to the machine JWT.
491
519
  provider: SPECTRAL_PROXY_ANTHROPIC,
492
520
  baseUrl,
493
- reasoning: false,
521
+ reasoning: supportsReasoning(m.modelId),
494
522
  input: ["text", "image"],
495
523
  // Real pricing so pi can compute accurate token costs.
496
524
  cost: pricing
@@ -520,7 +548,7 @@ export class PiBridge {
520
548
  // breaking auth lookup against our synthetic proxy provider.
521
549
  provider: SPECTRAL_PROXY_OPENAI,
522
550
  baseUrl,
523
- reasoning: false,
551
+ reasoning: supportsReasoning(m.modelId),
524
552
  input: ["text", "image"],
525
553
  // Real pricing so pi can compute accurate token costs.
526
554
  cost: pricing
@@ -551,7 +579,7 @@ export class PiBridge {
551
579
  api: "openai-completions",
552
580
  provider: SPECTRAL_PROXY_USER_MODEL,
553
581
  baseUrl,
554
- reasoning: false,
582
+ reasoning: supportsReasoning(m.modelId),
555
583
  input: ["text", "image"],
556
584
  cost: pricing
557
585
  ? { input: pricing.input, output: pricing.output, cacheRead: pricing.cacheRead, cacheWrite: pricing.cacheWrite }
@@ -579,6 +607,18 @@ export class PiBridge {
579
607
  *
580
608
  * Phase 3 (Available Models whitelist).
581
609
  */
610
+ /**
611
+ * Return the modelId of the first available model from the backend
612
+ * whitelist (preserving the backend's sortOrder, which is the same
613
+ * ordering the frontend uses in its model picker). Returns `undefined`
614
+ * when no models are available (e.g. backend unreachable at startup).
615
+ *
616
+ * Used by `SessionStreamManager.prompt()` as a defense-in-depth
617
+ * default when neither the envelope nor SQLite supply a modelId.
618
+ */
619
+ getFirstAvailableModelId() {
620
+ return this.allowedModels?.[0]?.modelId;
621
+ }
582
622
  async setModel(modelId) {
583
623
  if (!modelId)
584
624
  return true; // nothing to apply — pi keeps its current model
@@ -232,7 +232,9 @@ export class SessionStreamManager {
232
232
  // b) Else, look up the per-session persisted modelId in SQLite
233
233
  // (cross-restart recovery — server restart wipes pi's in-memory
234
234
  // session model state, but our durable store has the last value).
235
- // c) Else, leave model selection to pi (pre-Phase-3 behaviour).
235
+ // c) Else, ask the bridge for the first available model from the
236
+ // backend whitelist (same sortOrder the frontend uses for its
237
+ // default picker display).
236
238
  //
237
239
  // We apply BEFORE persisting the user message: if the bridge can't
238
240
  // resolve the model (unknown id, registry unavailable), it has already
@@ -242,7 +244,9 @@ export class SessionStreamManager {
242
244
  // Persistence: only the envelope-supplied value is written back. A
243
245
  // recovery-only application (case b) doesn't update the row — the
244
246
  // value is already there.
245
- const effectiveModelId = modelId ?? this.store.getSessionModel(sessionId) ?? undefined;
247
+ const effectiveModelId = modelId ??
248
+ this.store.getSessionModel(sessionId) ??
249
+ stream.bridge.getFirstAvailableModelId?.();
246
250
  if (effectiveModelId && stream.bridge.setModel) {
247
251
  const ok = await stream.bridge.setModel(effectiveModelId);
248
252
  if (!ok) {
@@ -252,9 +256,9 @@ export class SessionStreamManager {
252
256
  return;
253
257
  }
254
258
  }
255
- if (modelId) {
259
+ if (effectiveModelId) {
256
260
  try {
257
- this.store.setSessionModel(sessionId, modelId);
261
+ this.store.setSessionModel(sessionId, effectiveModelId);
258
262
  }
259
263
  catch (err) {
260
264
  // Persisting the sticky model is best-effort: the live turn will
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexol/spectral",
3
- "version": "0.2.22",
3
+ "version": "0.3.1",
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,