pi-pignon 0.1.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/CHANGELOG.md +40 -0
- package/LICENSE +21 -0
- package/README.md +514 -0
- package/examples/pignon.json +35 -0
- package/package.json +68 -0
- package/schema/config.schema.json +446 -0
- package/src/compare.ts +109 -0
- package/src/config/defaults.ts +95 -0
- package/src/config/describe.ts +30 -0
- package/src/config/load.ts +445 -0
- package/src/config/migrate.ts +86 -0
- package/src/config/presets.ts +54 -0
- package/src/config/schema.ts +224 -0
- package/src/deciders/create.ts +102 -0
- package/src/deciders/jev.ts +226 -0
- package/src/deciders/laya-local.ts +718 -0
- package/src/deciders/laya-serve.ts +53 -0
- package/src/deciders/parse.ts +48 -0
- package/src/deciders/questions.ts +34 -0
- package/src/deciders/strategy.ts +225 -0
- package/src/deciders/types.ts +70 -0
- package/src/extension.ts +439 -0
- package/src/onboarding.ts +203 -0
- package/src/policy.ts +215 -0
- package/src/report.ts +171 -0
- package/src/router.ts +215 -0
- package/src/stats.ts +55 -0
- package/src/types.ts +341 -0
- package/src/ui.ts +153 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `laya-serve` decider: a Laya model served by the official `laya-serve`
|
|
3
|
+
* (`pip install "laya[serve]"`), which speaks TypeSafe's Jev API on
|
|
4
|
+
* `POST /v1/systemone`. It reuses the Jev client, pointed at the server.
|
|
5
|
+
*
|
|
6
|
+
* The user runs the server; pignon only connects. While it is down or still
|
|
7
|
+
* loading, connections are refused within milliseconds, so a prompt is never
|
|
8
|
+
* held: the decision fails and the prompt keeps the current model.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { LayaServeDeciderSpec } from "../types.js";
|
|
12
|
+
import type { Fetch } from "@typesafe-ai/sdk";
|
|
13
|
+
|
|
14
|
+
import { JevDecider } from "./jev.js";
|
|
15
|
+
|
|
16
|
+
/** laya-serve's default port, on the loopback interface. */
|
|
17
|
+
export const LAYA_SERVE_DEFAULT_URL = "http://127.0.0.1:8000";
|
|
18
|
+
|
|
19
|
+
export function createLayaServeDecider(
|
|
20
|
+
spec: Omit<LayaServeDeciderSpec, "type">,
|
|
21
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
22
|
+
fetch?: Fetch,
|
|
23
|
+
): JevDecider {
|
|
24
|
+
const url = spec.url ?? LAYA_SERVE_DEFAULT_URL;
|
|
25
|
+
return new JevDecider({
|
|
26
|
+
id: "laya-serve",
|
|
27
|
+
baseURL: url,
|
|
28
|
+
requireApiKey: false,
|
|
29
|
+
unreachableHint: `is laya-serve running at ${url}?`,
|
|
30
|
+
// Only the server's own key: never TYPESAFE_API_KEY or other TYPESAFE_* settings.
|
|
31
|
+
env: spec.apiKeyEnv ? { [spec.apiKeyEnv]: env[spec.apiKeyEnv] } : {},
|
|
32
|
+
...(spec.apiKeyEnv ? { apiKeyEnv: spec.apiKeyEnv } : {}),
|
|
33
|
+
...(spec.model !== undefined ? { model: spec.model } : {}),
|
|
34
|
+
...(spec.timeoutMs !== undefined ? { timeoutMs: spec.timeoutMs } : {}),
|
|
35
|
+
...(fetch ? { fetch } : {}),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Whether a laya-serve answers at `url` (GET /health), within `timeoutMs`. */
|
|
40
|
+
export async function probeLayaServe(
|
|
41
|
+
url: string = LAYA_SERVE_DEFAULT_URL,
|
|
42
|
+
timeoutMs = 500,
|
|
43
|
+
fetchFn: typeof fetch = fetch,
|
|
44
|
+
): Promise<boolean> {
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetchFn(`${url.replace(/\/+$/, "")}/health`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
47
|
+
if (!response.ok) return false;
|
|
48
|
+
const body = (await response.json()) as { status?: unknown };
|
|
49
|
+
return body.status === "ok";
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn raw decider answers into the routing-relevant `RoutingDecision`.
|
|
3
|
+
*
|
|
4
|
+
* Shared by every decider, so the policy never sees decider-specific shapes.
|
|
5
|
+
* Tolerant of malformed input: a missing or invalid answer reads as "no
|
|
6
|
+
* signal", which the policy treats as fail-open.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { DEFAULT_CONFIG } from "../config/defaults.js";
|
|
10
|
+
import type { ConfidenceSource, RouterConfig, RoutingDecision } from "../types.js";
|
|
11
|
+
import { EXPLORATION_QUESTION, TIER_QUESTION } from "./questions.js";
|
|
12
|
+
import type { RawAnswers } from "./types.js";
|
|
13
|
+
|
|
14
|
+
export function parseDecision(
|
|
15
|
+
answers: RawAnswers,
|
|
16
|
+
latencyMs: number,
|
|
17
|
+
config: Pick<RouterConfig, "table" | "confidenceSource"> = DEFAULT_CONFIG,
|
|
18
|
+
): RoutingDecision {
|
|
19
|
+
const tierAnswer = choiceOf(answers[TIER_QUESTION], config.confidenceSource);
|
|
20
|
+
const tier = tierAnswer && config.table.some((t) => t.id === tierAnswer.choice) ? tierAnswer.choice : null;
|
|
21
|
+
|
|
22
|
+
const explorationAnswer = choiceOf(answers[EXPLORATION_QUESTION], config.confidenceSource);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
tier,
|
|
26
|
+
tierConfidence: tier ? tierAnswer!.confidence : 0,
|
|
27
|
+
needsExploration: explorationAnswer?.choice === "yes",
|
|
28
|
+
explorationConfidence: explorationAnswer?.confidence ?? 0,
|
|
29
|
+
latencyMs,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extract a choice answer. The Laya worker tags answers with `type`, the
|
|
35
|
+
* TypeSafe SDK does not, so the tag is only checked when present.
|
|
36
|
+
*/
|
|
37
|
+
function choiceOf(value: unknown, source: ConfidenceSource): { choice: string; confidence: number } | null {
|
|
38
|
+
if (!isRecord(value) || typeof value.choice !== "string") return null;
|
|
39
|
+
if (value.type !== undefined && value.type !== "choice") return null;
|
|
40
|
+
const choice = value.choice;
|
|
41
|
+
const confidence =
|
|
42
|
+
source === "top-probability" && isRecord(value.probabilities) ? value.probabilities[choice] : value.confidence;
|
|
43
|
+
return { choice, confidence: typeof confidence === "number" && Number.isFinite(confidence) ? confidence : 0 };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
47
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
48
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two questions every decider is asked about a prompt, built from the
|
|
3
|
+
* config: the tier criteria come from the routing table, the rest from
|
|
4
|
+
* `config.questions`.
|
|
5
|
+
*
|
|
6
|
+
* Changing the wording changes what the confidences mean, so thresholds
|
|
7
|
+
* calibrated on one wording do not carry over to another; see
|
|
8
|
+
* `QuestionWording.version`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { RouterConfig } from "../types.js";
|
|
12
|
+
import type { DecisionRequest } from "./types.js";
|
|
13
|
+
|
|
14
|
+
/** Question whose choice is the tier id. */
|
|
15
|
+
export const TIER_QUESTION = "reasoning_demand";
|
|
16
|
+
|
|
17
|
+
/** Question whose `yes` choice means the task needs exploration. */
|
|
18
|
+
export const EXPLORATION_QUESTION = "needs_exploration";
|
|
19
|
+
|
|
20
|
+
export function buildQuestions(config: Pick<RouterConfig, "table" | "questions">): DecisionRequest["questions"] {
|
|
21
|
+
const { table, questions } = config;
|
|
22
|
+
return {
|
|
23
|
+
[TIER_QUESTION]: {
|
|
24
|
+
type: "choice",
|
|
25
|
+
instructions: questions.tierInstructions,
|
|
26
|
+
criteria: Object.fromEntries(table.map((tier) => [tier.id, tier.criterion])),
|
|
27
|
+
},
|
|
28
|
+
[EXPLORATION_QUESTION]: {
|
|
29
|
+
type: "choice",
|
|
30
|
+
instructions: questions.explorationInstructions,
|
|
31
|
+
criteria: { yes: questions.explorationCriteria.yes, no: questions.explorationCriteria.no },
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Several deciders behind one: the `strategy` setting.
|
|
3
|
+
*
|
|
4
|
+
* - sequential: ask them in order, moving on when one is not ready, fails,
|
|
5
|
+
* or answers with tier confidence below `escalateBelow`. Remote deciders
|
|
6
|
+
* are only called when needed.
|
|
7
|
+
* - parallel: ask every ready decider at once, then route on one answer
|
|
8
|
+
* (`pick`). Every answer is recorded, which is how deciders are compared.
|
|
9
|
+
*
|
|
10
|
+
* Every attempt is returned in `DeciderResult.attempts`, so both modes log
|
|
11
|
+
* the same data. A shared `budgetMs` bounds the wall time of one decision.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { DeciderAttempt, RoutingDecision, StrategyConfig } from "../types.js";
|
|
15
|
+
import { type Decider, type DeciderResult, type DecisionRequest, type RawAnswers, DeciderError } from "./types.js";
|
|
16
|
+
|
|
17
|
+
/** Reads answers the way the router will (tier ids, confidence source). */
|
|
18
|
+
export type ParseAnswers = (answers: RawAnswers, latencyMs: number) => RoutingDecision;
|
|
19
|
+
|
|
20
|
+
interface Answered {
|
|
21
|
+
index: number;
|
|
22
|
+
result: DeciderResult;
|
|
23
|
+
decision: RoutingDecision;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class StrategyDecider implements Decider {
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly remote: boolean;
|
|
29
|
+
|
|
30
|
+
/** Warmups in flight, so a decider that is not ready is warmed once, not once per prompt. */
|
|
31
|
+
private readonly warming = new Map<Decider, Promise<void>>();
|
|
32
|
+
|
|
33
|
+
constructor(
|
|
34
|
+
private readonly deciders: readonly Decider[],
|
|
35
|
+
private readonly strategy: StrategyConfig,
|
|
36
|
+
private readonly parse: ParseAnswers,
|
|
37
|
+
) {
|
|
38
|
+
if (deciders.length < 2) throw new Error("StrategyDecider needs at least two deciders");
|
|
39
|
+
this.id = `${strategy.mode}(${deciders.map((d) => d.id).join(",")})`;
|
|
40
|
+
this.remote = deciders.some((d) => d.remote);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The combined deciders, in configured order. */
|
|
44
|
+
get members(): readonly Decider[] {
|
|
45
|
+
return this.deciders;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Ready when any decider can answer: the others are skipped. */
|
|
49
|
+
get isReady(): boolean {
|
|
50
|
+
return this.deciders.some((d) => d.isReady);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get model(): string | undefined {
|
|
54
|
+
const models = this.deciders.map((d) => d.model ?? d.id);
|
|
55
|
+
return models.join(this.strategy.mode === "sequential" ? " → " : " + ");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get recentLogs(): readonly string[] {
|
|
59
|
+
return this.deciders.flatMap((d) => d.recentLogs.map((line) => `[${d.id}] ${line}`));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Warm every decider up; resolves as soon as one is ready, so a fast
|
|
64
|
+
* decider can route while a slow one (the local model) keeps loading.
|
|
65
|
+
*/
|
|
66
|
+
async warmup(signal?: AbortSignal): Promise<void> {
|
|
67
|
+
const pending = this.deciders.filter((d) => !d.isReady).map((d) => this.warm(d, signal));
|
|
68
|
+
if (pending.length === this.deciders.length) {
|
|
69
|
+
try {
|
|
70
|
+
await Promise.any(pending);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
const reasons = err instanceof AggregateError ? err.errors : [err];
|
|
73
|
+
throw new DeciderError(reasons.map((e) => (e instanceof Error ? e.message : String(e))).join("; "), err);
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
// Something is ready already; keep loading the rest in the background.
|
|
77
|
+
for (const p of pending) p.catch(() => {});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async decide(request: DecisionRequest, signal?: AbortSignal): Promise<DeciderResult> {
|
|
82
|
+
// The router only warms us up while nothing is ready; bring back a
|
|
83
|
+
// decider that is still loading or crashed without holding this prompt.
|
|
84
|
+
for (const d of this.deciders) if (!d.isReady) this.warm(d).catch(() => {});
|
|
85
|
+
|
|
86
|
+
const started = Date.now();
|
|
87
|
+
const budget = AbortSignal.timeout(this.strategy.budgetMs);
|
|
88
|
+
const bounded = signal ? AbortSignal.any([signal, budget]) : budget;
|
|
89
|
+
const attempts: DeciderAttempt[] = this.deciders.map((d) => ({
|
|
90
|
+
decider: d.id,
|
|
91
|
+
remote: d.remote,
|
|
92
|
+
outcome: d.isReady ? "not-asked" : "not-ready",
|
|
93
|
+
used: false,
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
const answered =
|
|
97
|
+
this.strategy.mode === "sequential"
|
|
98
|
+
? await this.sequential(request, bounded, budget, attempts)
|
|
99
|
+
: await this.parallel(request, bounded, budget, attempts);
|
|
100
|
+
|
|
101
|
+
const chosen = this.choose(answered);
|
|
102
|
+
if (!chosen) {
|
|
103
|
+
const reasons = attempts.map((a) => `${a.decider}: ${a.error ?? a.outcome}`).join("; ");
|
|
104
|
+
throw new DeciderError(`no decider answered (${reasons})`);
|
|
105
|
+
}
|
|
106
|
+
attempts[chosen.index]!.used = true;
|
|
107
|
+
return {
|
|
108
|
+
...chosen.result,
|
|
109
|
+
// What the user waited for, not only the chosen decider's share.
|
|
110
|
+
latencyMs: Date.now() - started,
|
|
111
|
+
remote: attempts.some((a) => a.remote && (a.outcome === "answered" || a.outcome === "failed")),
|
|
112
|
+
attempts,
|
|
113
|
+
...(sumCost(attempts) !== undefined ? { costUsd: sumCost(attempts) } : {}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
stop(): void {
|
|
118
|
+
for (const d of this.deciders) d.stop();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// -------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
private warm(decider: Decider, signal?: AbortSignal): Promise<void> {
|
|
124
|
+
let pending = this.warming.get(decider);
|
|
125
|
+
if (!pending) {
|
|
126
|
+
pending = decider.warmup(signal).finally(() => this.warming.delete(decider));
|
|
127
|
+
this.warming.set(decider, pending);
|
|
128
|
+
}
|
|
129
|
+
return pending;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private async sequential(
|
|
133
|
+
request: DecisionRequest,
|
|
134
|
+
signal: AbortSignal,
|
|
135
|
+
budget: AbortSignal,
|
|
136
|
+
attempts: DeciderAttempt[],
|
|
137
|
+
): Promise<Answered[]> {
|
|
138
|
+
const answered: Answered[] = [];
|
|
139
|
+
for (const [index, decider] of this.deciders.entries()) {
|
|
140
|
+
if (signal.aborted) break;
|
|
141
|
+
if (!decider.isReady) continue;
|
|
142
|
+
const answer = await this.attempt(index, decider, request, signal, budget, attempts);
|
|
143
|
+
if (!answer) continue;
|
|
144
|
+
answered.push(answer);
|
|
145
|
+
if (answer.decision.tier !== null && answer.decision.tierConfidence >= this.strategy.escalateBelow) break;
|
|
146
|
+
}
|
|
147
|
+
return answered;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private async parallel(
|
|
151
|
+
request: DecisionRequest,
|
|
152
|
+
signal: AbortSignal,
|
|
153
|
+
budget: AbortSignal,
|
|
154
|
+
attempts: DeciderAttempt[],
|
|
155
|
+
): Promise<Answered[]> {
|
|
156
|
+
const results = await Promise.all(
|
|
157
|
+
this.deciders.map((decider, index) =>
|
|
158
|
+
decider.isReady ? this.attempt(index, decider, request, signal, budget, attempts) : Promise.resolve(null),
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
return results.filter((r): r is Answered => r !== null);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Call one decider and record the outcome. Never throws. */
|
|
165
|
+
private async attempt(
|
|
166
|
+
index: number,
|
|
167
|
+
decider: Decider,
|
|
168
|
+
request: DecisionRequest,
|
|
169
|
+
signal: AbortSignal,
|
|
170
|
+
budget: AbortSignal,
|
|
171
|
+
attempts: DeciderAttempt[],
|
|
172
|
+
): Promise<Answered | null> {
|
|
173
|
+
const started = Date.now();
|
|
174
|
+
try {
|
|
175
|
+
const result = await decider.decide(request, signal);
|
|
176
|
+
const decision = this.parse(result.answers, result.latencyMs);
|
|
177
|
+
attempts[index] = {
|
|
178
|
+
decider: decider.id,
|
|
179
|
+
remote: decider.remote,
|
|
180
|
+
outcome: "answered",
|
|
181
|
+
used: false,
|
|
182
|
+
model: result.model,
|
|
183
|
+
tier: decision.tier,
|
|
184
|
+
tierConfidence: decision.tierConfidence,
|
|
185
|
+
needsExploration: decision.needsExploration,
|
|
186
|
+
explorationConfidence: decision.explorationConfidence,
|
|
187
|
+
latencyMs: result.latencyMs,
|
|
188
|
+
...(result.costUsd !== undefined ? { costUsd: result.costUsd } : {}),
|
|
189
|
+
};
|
|
190
|
+
return { index, result, decision };
|
|
191
|
+
} catch (err) {
|
|
192
|
+
const error = budget.aborted
|
|
193
|
+
? `over the ${this.strategy.budgetMs} ms budget`
|
|
194
|
+
: err instanceof Error
|
|
195
|
+
? err.message
|
|
196
|
+
: String(err);
|
|
197
|
+
attempts[index] = {
|
|
198
|
+
decider: decider.id,
|
|
199
|
+
remote: decider.remote,
|
|
200
|
+
outcome: "failed",
|
|
201
|
+
used: false,
|
|
202
|
+
latencyMs: Date.now() - started,
|
|
203
|
+
error,
|
|
204
|
+
};
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The answer to route on. Answers without a usable tier only win when nothing else answered. */
|
|
210
|
+
private choose(answered: Answered[]): Answered | undefined {
|
|
211
|
+
const usable = answered.filter((a) => a.decision.tier !== null);
|
|
212
|
+
const pool = usable.length > 0 ? usable : answered;
|
|
213
|
+
if (pool.length === 0) return undefined;
|
|
214
|
+
// "first" (parallel): list order decides. Sequential keeps the most
|
|
215
|
+
// confident answer, since a later decider may be less sure than an
|
|
216
|
+
// earlier one it was asked to double-check.
|
|
217
|
+
if (this.strategy.mode === "parallel" && this.strategy.pick === "first") return pool[0];
|
|
218
|
+
return pool.reduce((best, a) => (a.decision.tierConfidence > best.decision.tierConfidence ? a : best));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function sumCost(attempts: DeciderAttempt[]): number | undefined {
|
|
223
|
+
const costs = attempts.map((a) => a.costUsd).filter((c): c is number => c !== undefined);
|
|
224
|
+
return costs.length > 0 ? costs.reduce((a, b) => a + b, 0) : undefined;
|
|
225
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seam between the router and whatever classifies a prompt.
|
|
3
|
+
*
|
|
4
|
+
* A decider answers typed questions (Laya/Jev format) about a prompt. It may
|
|
5
|
+
* run locally (the Laya stdio worker) or remotely (TypeSafe's Jev); the router
|
|
6
|
+
* only sees this interface, and `parse.ts` turns the raw answers into a
|
|
7
|
+
* `RoutingDecision`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { DeciderAttempt, LayaQuestion } from "../types.js";
|
|
11
|
+
|
|
12
|
+
/** What a decider is asked about one prompt. */
|
|
13
|
+
export interface DecisionRequest {
|
|
14
|
+
/** The prompt, already truncated by the router. */
|
|
15
|
+
text: string;
|
|
16
|
+
/** Typed questions keyed by name; see `questions.ts`. */
|
|
17
|
+
questions: Readonly<Record<string, LayaQuestion>>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Answers keyed by question name, as the decider returned them.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately untyped: answers come from another process or the network and
|
|
24
|
+
* are validated by `parseDecision`.
|
|
25
|
+
*/
|
|
26
|
+
export type RawAnswers = Readonly<Record<string, unknown>>;
|
|
27
|
+
|
|
28
|
+
/** One decider's reply to a `DecisionRequest`. */
|
|
29
|
+
export interface DeciderResult {
|
|
30
|
+
/** `Decider.id` of the decider that answered. */
|
|
31
|
+
deciderId: string;
|
|
32
|
+
/** Checkpoint or remote model version that produced the answers. */
|
|
33
|
+
model: string;
|
|
34
|
+
answers: RawAnswers;
|
|
35
|
+
/** Wall time of the call as seen by the caller. */
|
|
36
|
+
latencyMs: number;
|
|
37
|
+
/** Price of the call in USD, for remote deciders that report it. */
|
|
38
|
+
costUsd?: number;
|
|
39
|
+
/** Whether the prompt left the machine for this call. Defaults to `Decider.remote`. */
|
|
40
|
+
remote?: boolean;
|
|
41
|
+
/** Each decider tried, when several were (see `strategy.ts`). */
|
|
42
|
+
attempts?: DeciderAttempt[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface Decider {
|
|
46
|
+
/** Stable identifier, e.g. `laya-local`. */
|
|
47
|
+
readonly id: string;
|
|
48
|
+
/** Whether prompts leave the machine. */
|
|
49
|
+
readonly remote: boolean;
|
|
50
|
+
/** Whether a `decide` call can be answered now without waiting to load. */
|
|
51
|
+
readonly isReady: boolean;
|
|
52
|
+
/** Model in use once known, for status lines and log entries. */
|
|
53
|
+
readonly model: string | undefined;
|
|
54
|
+
/** Recent diagnostics, oldest first, for `/laya log`. */
|
|
55
|
+
readonly recentLogs: readonly string[];
|
|
56
|
+
|
|
57
|
+
/** Prepare the decider (load a model, check credentials). */
|
|
58
|
+
warmup(signal?: AbortSignal): Promise<void>;
|
|
59
|
+
decide(request: DecisionRequest, signal?: AbortSignal): Promise<DeciderResult>;
|
|
60
|
+
/** Release resources. The decider cannot be used afterwards. */
|
|
61
|
+
stop(): void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Error raised by a decider that could not produce answers. */
|
|
65
|
+
export class DeciderError extends Error {
|
|
66
|
+
constructor(message: string, cause?: unknown) {
|
|
67
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
68
|
+
this.name = "DeciderError";
|
|
69
|
+
}
|
|
70
|
+
}
|