@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.
package/dist/server/pi-bridge.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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,
|
|
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 ??
|
|
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 (
|
|
259
|
+
if (effectiveModelId) {
|
|
256
260
|
try {
|
|
257
|
-
this.store.setSessionModel(sessionId,
|
|
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
|