@aexol/spectral 0.2.22 → 0.3.0

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";
@@ -249,6 +249,12 @@ export class PiBridge {
249
249
  * `session.setModel()`. Phase 3 (Available Models whitelist).
250
250
  */
251
251
  modelRegistry;
252
+ /**
253
+ * Raw allowed models list from the backend, preserved in sortOrder.
254
+ * Used by `getFirstAvailableModelId()` to return the backend-curated
255
+ * top pick when no explicit model selection is made.
256
+ */
257
+ allowedModels;
252
258
  /**
253
259
  * Last `modelId` we successfully applied via `session.setModel()`, or
254
260
  * `undefined` if we never applied one (pi falls back to its own settings
@@ -426,6 +432,7 @@ export class PiBridge {
426
432
  throw new Error(`Failed to fetch allowed models from backend; check SPECTRAL_BACKEND_URL ` +
427
433
  `and machine JWT. Underlying error: ${e.message}`);
428
434
  }
435
+ this.allowedModels = allowedModels;
429
436
  this.registerSyntheticProviders(allowedModels);
430
437
  console.info(`✓ Inference routed via backend proxy (${allowedModels.length} model(s) available)`);
431
438
  const result = await createAgentSession({
@@ -579,6 +586,18 @@ export class PiBridge {
579
586
  *
580
587
  * Phase 3 (Available Models whitelist).
581
588
  */
589
+ /**
590
+ * Return the modelId of the first available model from the backend
591
+ * whitelist (preserving the backend's sortOrder, which is the same
592
+ * ordering the frontend uses in its model picker). Returns `undefined`
593
+ * when no models are available (e.g. backend unreachable at startup).
594
+ *
595
+ * Used by `SessionStreamManager.prompt()` as a defense-in-depth
596
+ * default when neither the envelope nor SQLite supply a modelId.
597
+ */
598
+ getFirstAvailableModelId() {
599
+ return this.allowedModels?.[0]?.modelId;
600
+ }
582
601
  async setModel(modelId) {
583
602
  if (!modelId)
584
603
  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.0",
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,