@pi-in-go/pigpen-pi-typesafe 0.1.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.
- package/CREDITS.md +14 -0
- package/LICENSE +22 -0
- package/README.md +45 -0
- package/extensions/pi-typesafe/branches_test.go +185 -0
- package/extensions/pi-typesafe/command.go +319 -0
- package/extensions/pi-typesafe/export_test.go +9 -0
- package/extensions/pi-typesafe/extension.go +188 -0
- package/extensions/pi-typesafe/extension_test.go +321 -0
- package/extensions/pi-typesafe/fakehost_test.go +548 -0
- package/extensions/pi-typesafe/format.go +191 -0
- package/extensions/pi-typesafe/format_test.go +75 -0
- package/extensions/pi-typesafe/go.mod +10 -0
- package/extensions/pi-typesafe/go.sum +2 -0
- package/extensions/pi-typesafe/go.work +11 -0
- package/extensions/pi-typesafe/harness_test.go +200 -0
- package/extensions/pi-typesafe/ownmodel_test.go +100 -0
- package/extensions/pi-typesafe/review_test.go +134 -0
- package/extensions/pi-typesafe/tool.go +193 -0
- package/extensions/pi-typesafe/twin_test.go +28 -0
- package/libs/pi-typesafe-api/CREDITS.md +14 -0
- package/libs/pi-typesafe-api/LICENSE +22 -0
- package/libs/pi-typesafe-api/README.md +30 -0
- package/libs/pi-typesafe-api/ask.go +62 -0
- package/libs/pi-typesafe-api/ask_test.go +76 -0
- package/libs/pi-typesafe-api/auth.go +249 -0
- package/libs/pi-typesafe-api/auth_test.go +131 -0
- package/libs/pi-typesafe-api/backends.go +336 -0
- package/libs/pi-typesafe-api/backends_test.go +404 -0
- package/libs/pi-typesafe-api/batch.go +202 -0
- package/libs/pi-typesafe-api/batch_test.go +202 -0
- package/libs/pi-typesafe-api/battery_test.go +41 -0
- package/libs/pi-typesafe-api/calibrate.go +354 -0
- package/libs/pi-typesafe-api/calibrate_test.go +186 -0
- package/libs/pi-typesafe-api/client.go +615 -0
- package/libs/pi-typesafe-api/client_test.go +490 -0
- package/libs/pi-typesafe-api/credentials.go +252 -0
- package/libs/pi-typesafe-api/credentials_test.go +216 -0
- package/libs/pi-typesafe-api/doc.go +14 -0
- package/libs/pi-typesafe-api/errors.go +143 -0
- package/libs/pi-typesafe-api/evaluation.go +86 -0
- package/libs/pi-typesafe-api/evaluation_schema.json +264 -0
- package/libs/pi-typesafe-api/gaps_test.go +77 -0
- package/libs/pi-typesafe-api/go.mod +9 -0
- package/libs/pi-typesafe-api/go.sum +2 -0
- package/libs/pi-typesafe-api/helpers_test.go +169 -0
- package/libs/pi-typesafe-api/hostmodel/hostmodel.go +87 -0
- package/libs/pi-typesafe-api/json.go +299 -0
- package/libs/pi-typesafe-api/json_test.go +92 -0
- package/libs/pi-typesafe-api/ownmodel_test.go +79 -0
- package/libs/pi-typesafe-api/package.json +40 -0
- package/libs/pi-typesafe-api/provenance.json +18 -0
- package/libs/pi-typesafe-api/review_test.go +23 -0
- package/libs/pi-typesafe-api/schema.go +473 -0
- package/libs/pi-typesafe-api/schema_test.go +262 -0
- package/libs/pi-typesafe-api/testdata/tools/typebox-messages.mts +5 -0
- package/libs/pi-typesafe-api/testdata/typebox-messages.json +285 -0
- package/libs/pi-typesafe-api/twin_test.go +28 -0
- package/libs/pi-typesafe-api/ui/fakehost_test.go +548 -0
- package/libs/pi-typesafe-api/ui/keyprompt.go +115 -0
- package/libs/pi-typesafe-api/ui/login.go +106 -0
- package/libs/pi-typesafe-api/ui/twin_test.go +28 -0
- package/libs/pi-typesafe-api/ui/ui_test.go +285 -0
- package/libs/pi-typesafe-api/usage.go +366 -0
- package/libs/pi-typesafe-api/usage_test.go +139 -0
- package/libs/typesafe/CONTRACT.md +125 -0
- package/libs/typesafe/CREDITS.md +37 -0
- package/libs/typesafe/LICENSE +23 -0
- package/libs/typesafe/README.md +19 -0
- package/libs/typesafe/go.mod +3 -0
- package/libs/typesafe/libraries/ownmodel/backend_test.go +496 -0
- package/libs/typesafe/libraries/ownmodel/canon.go +190 -0
- package/libs/typesafe/libraries/ownmodel/convert.go +199 -0
- package/libs/typesafe/libraries/ownmodel/doc.go +15 -0
- package/libs/typesafe/libraries/ownmodel/equivalence_test.go +199 -0
- package/libs/typesafe/libraries/ownmodel/helpers_test.go +155 -0
- package/libs/typesafe/libraries/ownmodel/mutation_test.go +31 -0
- package/libs/typesafe/libraries/ownmodel/ownmodel.go +225 -0
- package/libs/typesafe/libraries/ownmodel/plan.go +442 -0
- package/libs/typesafe/libraries/ownmodel/run.go +288 -0
- package/libs/typesafe/libraries/ownmodel/schema_test.go +254 -0
- package/libs/typesafe/libraries/ownmodel/twins_test.go +169 -0
- package/libs/typesafe/libraries/ownmodel/utils_test.go +125 -0
- package/libs/typesafe/libraries/pigmodel/pigmodel.go +264 -0
- package/libs/typesafe/libraries/pigmodel/pigmodel_test.go +410 -0
- package/libs/typesafe/libraries/typesafe/answers.go +268 -0
- package/libs/typesafe/libraries/typesafe/api_response_test.go +113 -0
- package/libs/typesafe/libraries/typesafe/batch.go +80 -0
- package/libs/typesafe/libraries/typesafe/batch_test.go +133 -0
- package/libs/typesafe/libraries/typesafe/bench_test.go +71 -0
- package/libs/typesafe/libraries/typesafe/client.go +561 -0
- package/libs/typesafe/libraries/typesafe/client_test.go +495 -0
- package/libs/typesafe/libraries/typesafe/crosscheck_test.go +464 -0
- package/libs/typesafe/libraries/typesafe/crosscheck_workflowevals_test.go +219 -0
- package/libs/typesafe/libraries/typesafe/doc.go +27 -0
- package/libs/typesafe/libraries/typesafe/entry.go +142 -0
- package/libs/typesafe/libraries/typesafe/env.go +11 -0
- package/libs/typesafe/libraries/typesafe/errors.go +310 -0
- package/libs/typesafe/libraries/typesafe/errors_test.go +175 -0
- package/libs/typesafe/libraries/typesafe/helpers_test.go +294 -0
- package/libs/typesafe/libraries/typesafe/live_test.go +96 -0
- package/libs/typesafe/libraries/typesafe/logging.go +160 -0
- package/libs/typesafe/libraries/typesafe/logging_test.go +259 -0
- package/libs/typesafe/libraries/typesafe/marshal_test.go +112 -0
- package/libs/typesafe/libraries/typesafe/mutation_test.go +39 -0
- package/libs/typesafe/libraries/typesafe/questions.go +490 -0
- package/libs/typesafe/libraries/typesafe/questions_test.go +166 -0
- package/libs/typesafe/libraries/typesafe/regressions_test.go +159 -0
- package/libs/typesafe/libraries/typesafe/reliability_test.go +649 -0
- package/libs/typesafe/libraries/typesafe/retry.go +350 -0
- package/libs/typesafe/libraries/typesafe/retry_test.go +297 -0
- package/libs/typesafe/libraries/typesafe/runtime_test.go +26 -0
- package/libs/typesafe/libraries/typesafe/transport_test.go +163 -0
- package/libs/typesafe/libraries/typesafe/twins_test.go +127 -0
- package/libs/typesafe/libraries/typesafe/types_test.go +165 -0
- package/libs/typesafe/libraries/typesafe/version.go +10 -0
- package/libs/typesafe/package.json +37 -0
- package/libs/typesafe/provenance.json +49 -0
- package/package.json +42 -0
- package/port/PORT.md +98 -0
- package/port/accepted-gaps.json +3 -0
- package/port/golden/enable-confirm.jsonl +11 -0
- package/port/golden/enable-decline.jsonl +20 -0
- package/port/golden/enable-missing-key.jsonl +4 -0
- package/port/golden/login-shadow.jsonl +4 -0
- package/port/golden/logout-env-key.jsonl +6 -0
- package/port/golden/playground-cancel.jsonl +4 -0
- package/port/golden/playground-invalid-json.jsonl +5 -0
- package/port/golden/playground-invalid-questions.jsonl +5 -0
- package/port/golden/status-env-key.jsonl +6 -0
- package/port/golden/status-no-key.jsonl +6 -0
- package/port/golden/tool-disabled.jsonl +18 -0
- package/port/golden/trailing-words.jsonl +10 -0
- package/port/library-mutations.py +44 -0
- package/port/mutations.json +302 -0
- package/port/oracle/.env.example +4 -0
- package/port/oracle/CHANGELOG.md +91 -0
- package/port/oracle/CONTRIBUTING.md +35 -0
- package/port/oracle/LICENSE +21 -0
- package/port/oracle/README.md +159 -0
- package/port/oracle/docs/api.md +143 -0
- package/port/oracle/docs/ci-cd.md +97 -0
- package/port/oracle/examples/decision-extension.ts +41 -0
- package/port/oracle/extensions/index.js +2 -0
- package/port/oracle/package.json +89 -0
- package/port/oracle/scripts/dev-pi.mjs +23 -0
- package/port/oracle/scripts/live-smoke.mjs +35 -0
- package/port/oracle/src/ask.ts +42 -0
- package/port/oracle/src/auth.ts +171 -0
- package/port/oracle/src/backends.ts +196 -0
- package/port/oracle/src/batch.ts +170 -0
- package/port/oracle/src/calibrate.ts +237 -0
- package/port/oracle/src/client.ts +310 -0
- package/port/oracle/src/credentials.ts +136 -0
- package/port/oracle/src/errors.ts +53 -0
- package/port/oracle/src/extension.ts +204 -0
- package/port/oracle/src/index.ts +31 -0
- package/port/oracle/src/key-prompt.ts +51 -0
- package/port/oracle/src/login.ts +60 -0
- package/port/oracle/src/schema.ts +158 -0
- package/port/oracle/src/ui.ts +4 -0
- package/port/oracle/src/usage.ts +258 -0
- package/port/oracle/tests/ask.test.ts +63 -0
- package/port/oracle/tests/auth.test.ts +141 -0
- package/port/oracle/tests/backends.test.ts +380 -0
- package/port/oracle/tests/batch.test.ts +156 -0
- package/port/oracle/tests/calibrate.test.ts +144 -0
- package/port/oracle/tests/client.test.ts +499 -0
- package/port/oracle/tests/credentials.test.ts +144 -0
- package/port/oracle/tests/extension.test.ts +276 -0
- package/port/oracle/tests/key-prompt.test.ts +47 -0
- package/port/oracle/tests/login.test.ts +101 -0
- package/port/oracle/tests/schema.test.ts +85 -0
- package/port/oracle/tests/usage.test.ts +106 -0
- package/port/oracle/tsconfig.build.json +10 -0
- package/port/oracle/tsconfig.json +14 -0
- package/port/scenarios/enable-confirm.json +5 -0
- package/port/scenarios/enable-decline.json +3 -0
- package/port/scenarios/enable-missing-key.json +2 -0
- package/port/scenarios/login-shadow.json +2 -0
- package/port/scenarios/logout-env-key.json +3 -0
- package/port/scenarios/playground-cancel.json +2 -0
- package/port/scenarios/playground-invalid-json.json +2 -0
- package/port/scenarios/playground-invalid-questions.json +2 -0
- package/port/scenarios/status-env-key.json +3 -0
- package/port/scenarios/status-no-key.json +3 -0
- package/port/scenarios/tool-disabled.json +2 -0
- package/port/scenarios/trailing-words.json +5 -0
- package/port/upstream-tests.json +160 -0
- package/provenance.json +18 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { DEFAULT_BACKEND, TYPESAFE_KEY_ENV, resolveBackend, usesTypesafeKey } from "./backends.js";
|
|
4
|
+
import type { BackendSpec } from "./backends.js";
|
|
5
|
+
import { credentialsPath, keySituation, keySourceLabel, piTypesafeDir } from "./credentials.js";
|
|
6
|
+
import type { KeySource } from "./credentials.js";
|
|
7
|
+
import { TypeSafeIntegrationError } from "./errors.js";
|
|
8
|
+
import type { IntegrationErrorCode } from "./errors.js";
|
|
9
|
+
|
|
10
|
+
const AUTH_VERSION = 1;
|
|
11
|
+
const CODES: ReadonlySet<string> = new Set<IntegrationErrorCode>([
|
|
12
|
+
"configuration", "validation", "budget", "aborted", "timeout", "http", "connection", "response",
|
|
13
|
+
]);
|
|
14
|
+
/** Statuses that mean the key itself was refused, not that the service was busy. */
|
|
15
|
+
const REJECTED_STATUSES = new Set([401, 403]);
|
|
16
|
+
|
|
17
|
+
/** The last request that degraded TypeSafe, with no upstream body, header, key, or submitted state. */
|
|
18
|
+
export interface AuthFailure {
|
|
19
|
+
readonly code: IntegrationErrorCode;
|
|
20
|
+
readonly status?: number;
|
|
21
|
+
readonly message: string;
|
|
22
|
+
readonly at: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The whole answer to "is Jev actually available right now": which key is in effect, whether it has been accepted, and
|
|
27
|
+
* the last failure that degraded it. Consumers must consult this instead of treating their own consent flag as proof
|
|
28
|
+
* that judgments will happen — an enabled extension with no key used to look identical to a working one.
|
|
29
|
+
*/
|
|
30
|
+
export interface AuthState {
|
|
31
|
+
/** The judgment backend this state describes; each backend has its own key. Holds the value the caller passed. */
|
|
32
|
+
readonly backend: BackendSpec;
|
|
33
|
+
/** Same kinds as KeySituation: where the key in effect comes from. */
|
|
34
|
+
readonly kind: "environment" | "stored" | "missing" | "unusable";
|
|
35
|
+
readonly source?: KeySource;
|
|
36
|
+
/** Where the key would be read from. */
|
|
37
|
+
readonly path: string;
|
|
38
|
+
/** Why a stored key cannot be used, when that is the case. */
|
|
39
|
+
readonly reason?: string;
|
|
40
|
+
/** Short human label for the key source: `TYPESAFE_API_KEY`, `OPENROUTER_API_KEY`, `/typesafe login`, `no key`, `unusable key`. */
|
|
41
|
+
readonly keyName: string;
|
|
42
|
+
/**
|
|
43
|
+
* True when the key in effect was accepted by the backend (login verifies a TypeSafe key; a successful request proves
|
|
44
|
+
* any key). The record is shared across backends: switching backends keeps the last outcome until the next request.
|
|
45
|
+
*/
|
|
46
|
+
readonly verified: boolean;
|
|
47
|
+
readonly verifiedAt?: string;
|
|
48
|
+
/** The last failure, cleared by the next successful request. */
|
|
49
|
+
readonly lastFailure?: AuthFailure;
|
|
50
|
+
/** A key is present and the last authentication outcome was not a rejection. False means judgments are skipped. */
|
|
51
|
+
readonly usable: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The auth-state file: one small, owner-only record that outlives the process that wrote it. */
|
|
55
|
+
export function authStatePath(): string {
|
|
56
|
+
return join(piTypesafeDir(), "auth-state.json");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readState(path: string): { verifiedAt?: string; lastFailure?: AuthFailure } {
|
|
60
|
+
try {
|
|
61
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
62
|
+
if (!parsed || typeof parsed !== "object") return {};
|
|
63
|
+
const raw = parsed as { verifiedAt?: unknown; lastFailure?: unknown };
|
|
64
|
+
const verifiedAt = typeof raw.verifiedAt === "string" && raw.verifiedAt.length <= 40 ? raw.verifiedAt : undefined;
|
|
65
|
+
const failure = raw.lastFailure && typeof raw.lastFailure === "object" ? raw.lastFailure as Record<string, unknown> : undefined;
|
|
66
|
+
const code = typeof failure?.code === "string" && CODES.has(failure.code) ? failure.code as IntegrationErrorCode : undefined;
|
|
67
|
+
const message = typeof failure?.message === "string" ? failure.message.slice(0, 300) : undefined;
|
|
68
|
+
const at = typeof failure?.at === "string" && failure.at.length <= 40 ? failure.at : undefined;
|
|
69
|
+
const status = typeof failure?.status === "number" && Number.isSafeInteger(failure.status) ? failure.status : undefined;
|
|
70
|
+
const lastFailure = code && message && at
|
|
71
|
+
? { code, message, at, ...(status === undefined ? {} : { status }) }
|
|
72
|
+
: undefined;
|
|
73
|
+
return { ...(verifiedAt === undefined ? {} : { verifiedAt }), ...(lastFailure === undefined ? {} : { lastFailure }) };
|
|
74
|
+
} catch {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Owner-only, atomic, and best-effort: an unwritable auth record never changes how a request behaves. */
|
|
80
|
+
function writeState(path: string, state: { verifiedAt?: string; lastFailure?: AuthFailure }): void {
|
|
81
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
82
|
+
try {
|
|
83
|
+
mkdirSync(piTypesafeDir(), { recursive: true, mode: 0o700 });
|
|
84
|
+
writeFileSync(temporary, `${JSON.stringify({ version: AUTH_VERSION, ...state }, null, 2)}\n`, { mode: 0o600, flag: "w" });
|
|
85
|
+
chmodSync(temporary, 0o600);
|
|
86
|
+
renameSync(temporary, path);
|
|
87
|
+
} catch {
|
|
88
|
+
try { rmSync(temporary, { force: true }); } catch { /* best-effort cleanup only */ }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* What the key situation, the last outcome, and the clock add up to for a valid backend; it never throws for one. An
|
|
94
|
+
* invalid backend throws the same `configuration` error as resolveBackend(), so validate a user-supplied endpoint
|
|
95
|
+
* with resolveBackend() first.
|
|
96
|
+
*/
|
|
97
|
+
export function authState(options: { path?: string; backend?: BackendSpec } = {}): AuthState {
|
|
98
|
+
const path = options.path ?? authStatePath();
|
|
99
|
+
const backend = options.backend ?? DEFAULT_BACKEND;
|
|
100
|
+
const situation = keySituation(backend);
|
|
101
|
+
const stored = readState(path);
|
|
102
|
+
const source: KeySource | undefined = situation.kind === "environment" ? "environment" : situation.kind === "stored" ? "stored" : undefined;
|
|
103
|
+
const rejected = stored.lastFailure?.code === "http" && stored.lastFailure.status !== undefined && REJECTED_STATUSES.has(stored.lastFailure.status);
|
|
104
|
+
const usable = source !== undefined && !rejected;
|
|
105
|
+
return {
|
|
106
|
+
backend,
|
|
107
|
+
kind: situation.kind,
|
|
108
|
+
...(source === undefined ? {} : { source }),
|
|
109
|
+
path: situation.kind === "unusable" ? situation.path : credentialsPath(),
|
|
110
|
+
...(situation.kind === "unusable" ? { reason: situation.reason } : {}),
|
|
111
|
+
keyName: keySourceLabel(situation),
|
|
112
|
+
verified: stored.verifiedAt !== undefined && !rejected,
|
|
113
|
+
...(stored.verifiedAt === undefined ? {} : { verifiedAt: stored.verifiedAt }),
|
|
114
|
+
...(stored.lastFailure === undefined ? {} : { lastFailure: stored.lastFailure }),
|
|
115
|
+
usable,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Record that the key was accepted: login verification, or any successful request. Clears the last failure. */
|
|
120
|
+
export function recordAuthVerified(at: Date = new Date()): void {
|
|
121
|
+
writeState(authStatePath(), { verifiedAt: at.toISOString() });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Record the failure that degraded TypeSafe. The verification timestamp is kept so a recovered key stays known. */
|
|
125
|
+
export function recordAuthFailure(error: TypeSafeIntegrationError, at: Date = new Date()): void {
|
|
126
|
+
const current = readState(authStatePath());
|
|
127
|
+
const failure: AuthFailure = {
|
|
128
|
+
code: error.code,
|
|
129
|
+
message: error.message,
|
|
130
|
+
at: at.toISOString(),
|
|
131
|
+
...(error.status === undefined ? {} : { status: error.status }),
|
|
132
|
+
};
|
|
133
|
+
writeState(authStatePath(), { ...(current.verifiedAt === undefined ? {} : { verifiedAt: current.verifiedAt }), lastFailure: failure });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Forget verification and degradation: used when the key itself changes (login or logout). */
|
|
137
|
+
export function clearAuthState(): void {
|
|
138
|
+
try { rmSync(authStatePath(), { force: true }); } catch { /* nothing to clear */ }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface AuthReport {
|
|
142
|
+
/** `error` when judgments are skipped or were rejected, `warning` when the key is unverified, otherwise `ok`. */
|
|
143
|
+
readonly level: "ok" | "warning" | "error";
|
|
144
|
+
/** One line naming the key source and, when degraded, the reason. Safe to display. */
|
|
145
|
+
readonly text: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* One line plus a level, so a status command, a headless log, or a consumer's own status line can call out a degraded
|
|
150
|
+
* state instead of reporting "enabled".
|
|
151
|
+
*/
|
|
152
|
+
export function describeAuth(state: AuthState = authState()): AuthReport {
|
|
153
|
+
const config = resolveBackend(state.backend);
|
|
154
|
+
const label = `${config.label} key`;
|
|
155
|
+
const since = state.lastFailure ? ` Last failure: ${state.lastFailure.message}${state.lastFailure.at ? ` (${state.lastFailure.at})` : ""}` : "";
|
|
156
|
+
if (state.kind === "missing") {
|
|
157
|
+
const how = usesTypesafeKey(config) ? `a key is configured (/typesafe login or ${TYPESAFE_KEY_ENV})` : `${config.keyEnv} is set in the environment`;
|
|
158
|
+
return { level: "error", text: `${label}: missing — every Jev judgment is skipped until ${how}.${since}` };
|
|
159
|
+
}
|
|
160
|
+
if (state.kind === "unusable") {
|
|
161
|
+
return { level: "error", text: `${label}: unusable (${state.reason ?? "unknown reason"}) — judgments are skipped until the key is fixed.${since}` };
|
|
162
|
+
}
|
|
163
|
+
const rejected = state.lastFailure?.code === "http" && state.lastFailure.status !== undefined && REJECTED_STATUSES.has(state.lastFailure.status);
|
|
164
|
+
if (rejected) {
|
|
165
|
+
return { level: "error", text: `${label}: ${state.keyName} was rejected.${since}` };
|
|
166
|
+
}
|
|
167
|
+
if (!state.verified) {
|
|
168
|
+
return { level: "warning", text: `${label}: ${state.keyName} (not verified yet — the first request proves it).${since}` };
|
|
169
|
+
}
|
|
170
|
+
return { level: "ok", text: `${label}: ${state.keyName} (verified${state.verifiedAt ? ` ${state.verifiedAt}` : ""}).${since}` };
|
|
171
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { TypeSafeIntegrationError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
export type TypeSafeBackend = "typesafe" | "openrouter" | "commandcode";
|
|
4
|
+
|
|
5
|
+
export interface BackendConfig {
|
|
6
|
+
/** Human name for status lines: "TypeSafe", "OpenRouter", "Command Code". */
|
|
7
|
+
label: string;
|
|
8
|
+
host: string;
|
|
9
|
+
/** The environment variable that carries this backend's key. Absent means the TypeSafe key resolution applies. */
|
|
10
|
+
keyEnv?: string;
|
|
11
|
+
/** Request path, when the backend does not serve the SDK's own `/v1/systemone`. */
|
|
12
|
+
path?: string;
|
|
13
|
+
/** Request path for the model list, when the backend does not serve the SDK's own `/v1/models`. */
|
|
14
|
+
modelsPath?: string;
|
|
15
|
+
/** Field the model list arrives in, when the backend does not use the SDK's own `models`. */
|
|
16
|
+
modelsField?: string;
|
|
17
|
+
/** Entry field carrying the id callers pass as `model:`, when the SDK's own `name` is only a label. */
|
|
18
|
+
modelsIdField?: string;
|
|
19
|
+
/** Whether the model list checks the key. A public list accepts any key, so it proves nothing. Absent means it does. */
|
|
20
|
+
modelsVerifyKey?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A caller-supplied endpoint that serves the Jev decisions protocol. Passed per call; never added to the registry. */
|
|
24
|
+
export interface BackendEndpoint extends BackendConfig {
|
|
25
|
+
/** Required. The environment variable with this endpoint's key. Must not be TYPESAFE_API_KEY. */
|
|
26
|
+
readonly keyEnv: string;
|
|
27
|
+
/** Model sent when the caller names none. Without it, `model` must be passed to createTypeSafe. */
|
|
28
|
+
readonly defaultModel?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A registry name or a caller-supplied endpoint. */
|
|
32
|
+
export type BackendSpec = TypeSafeBackend | BackendEndpoint;
|
|
33
|
+
|
|
34
|
+
/** A backend resolved and validated: what the client will actually use. */
|
|
35
|
+
export interface ResolvedBackend extends BackendConfig {
|
|
36
|
+
/** Registry name, absent for a caller-supplied endpoint. */
|
|
37
|
+
readonly name?: TypeSafeBackend;
|
|
38
|
+
/** Origin only: scheme, host, and port. */
|
|
39
|
+
readonly host: string;
|
|
40
|
+
readonly keyEnv: string;
|
|
41
|
+
/** The model id sent when the caller names none, already in the backend's form. Absent when the endpoint names none. */
|
|
42
|
+
readonly defaultModel?: string;
|
|
43
|
+
/** Always explicit after resolution. */
|
|
44
|
+
readonly modelsVerifyKey: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The backend every key and auth function assumes when none is named. */
|
|
48
|
+
export const DEFAULT_BACKEND: TypeSafeBackend = "typesafe";
|
|
49
|
+
|
|
50
|
+
/** The environment variable and login store that the default backend reads. */
|
|
51
|
+
export const TYPESAFE_KEY_ENV = "TYPESAFE_API_KEY";
|
|
52
|
+
|
|
53
|
+
/** Registry of known judgment backends. Callers pass a `BackendEndpoint` for a host this registry does not name. */
|
|
54
|
+
export const DECISIONS_BACKENDS: Record<TypeSafeBackend, BackendConfig> = {
|
|
55
|
+
typesafe: { label: "TypeSafe", host: "https://api.typesafe.ai", keyEnv: TYPESAFE_KEY_ENV },
|
|
56
|
+
openrouter: {
|
|
57
|
+
label: "OpenRouter",
|
|
58
|
+
host: "https://openrouter.ai",
|
|
59
|
+
keyEnv: "OPENROUTER_API_KEY",
|
|
60
|
+
path: "/api/alpha/decisions",
|
|
61
|
+
modelsPath: "/api/v1/models",
|
|
62
|
+
modelsField: "data",
|
|
63
|
+
modelsIdField: "id",
|
|
64
|
+
modelsVerifyKey: false,
|
|
65
|
+
},
|
|
66
|
+
commandcode: {
|
|
67
|
+
label: "Command Code",
|
|
68
|
+
host: "https://api.commandcode.ai",
|
|
69
|
+
keyEnv: "COMMANDCODE_API_KEY",
|
|
70
|
+
path: "/provider/v1/systemone",
|
|
71
|
+
modelsPath: "/provider/v1/models",
|
|
72
|
+
modelsField: "data",
|
|
73
|
+
modelsIdField: "id",
|
|
74
|
+
modelsVerifyKey: false,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** The registry entry for a backend name; a `configuration` error for a name the registry does not know. */
|
|
79
|
+
export function backendConfig(name: TypeSafeBackend): BackendConfig {
|
|
80
|
+
const backend = DECISIONS_BACKENDS[name];
|
|
81
|
+
if (!backend) throw new TypeSafeIntegrationError("configuration", `Unknown judgment backend "${name}". Valid backends: ${Object.keys(DECISIONS_BACKENDS).join(", ")}.`);
|
|
82
|
+
return backend;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Each backend's default model id as the caller writes it, before mapping: OpenRouter pins a version, TypeSafe follows latest. */
|
|
86
|
+
const DEFAULT_MODEL: Record<TypeSafeBackend, string> = {
|
|
87
|
+
typesafe: "jev-latest",
|
|
88
|
+
openrouter: "typesafe/jev-1.13",
|
|
89
|
+
commandcode: "typesafe/jev",
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The model id to send for a caller's `model` on this backend. OpenRouter routes a bare Jev id under the `typesafe`
|
|
94
|
+
* author: `jev-latest` becomes its alias form `~typesafe/jev-latest`, and a bare `jev-<major>.<minor>` — with or
|
|
95
|
+
* without TypeSafe direct's optional `.<patch>` segment — becomes `typesafe/jev-<major>.<minor>`. An id that already
|
|
96
|
+
* carries an author (`vendor/model`), a bare id this rule does not know, and every model on a backend without a
|
|
97
|
+
* mapping go through unchanged. Every mapped id contains `/`, so mapping an already-mapped id changes nothing.
|
|
98
|
+
*/
|
|
99
|
+
export function backendModelId(backend: TypeSafeBackend, model: string): string {
|
|
100
|
+
if (model.includes("/")) return model;
|
|
101
|
+
if (backend !== "openrouter") return model;
|
|
102
|
+
if (model === "jev-latest") return "~typesafe/jev-latest";
|
|
103
|
+
const version = /^jev-(\d+)\.(\d+)(?:\.\d+)?$/.exec(model);
|
|
104
|
+
return version === null ? model : `typesafe/jev-${version[1]}.${version[2]}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The model a client sends when the caller names none: the backend's own default, in the form that backend accepts. */
|
|
108
|
+
export function defaultModelId(backend: TypeSafeBackend): string {
|
|
109
|
+
return backendModelId(backend, DEFAULT_MODEL[backend]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Whether a backend's key comes from the TypeSafe resolution (`TYPESAFE_API_KEY`, then the login store) or only from
|
|
114
|
+
* its own environment variable. Only the TypeSafe backend has a login store; every other backend is environment-only.
|
|
115
|
+
*/
|
|
116
|
+
export function usesTypesafeKey(backend: BackendConfig): boolean {
|
|
117
|
+
return (backend.keyEnv ?? TYPESAFE_KEY_ENV) === TYPESAFE_KEY_ENV;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const KEY_ENV_MESSAGE = "Backend keyEnv must name an environment variable: letters, digits, and underscores, not starting with a digit.";
|
|
121
|
+
const HOST_MESSAGE = "Backend host must be an absolute https: URL with no user info, path, query, or fragment (http: is allowed only for localhost, 127.0.0.0/8, and [::1]).";
|
|
122
|
+
|
|
123
|
+
function refuse(message: string): never {
|
|
124
|
+
throw new TypeSafeIntegrationError("configuration", message);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** `https://gw.example.com?` and `https://gw.example.com#` parse clean, so the raw string itself must carry neither. */
|
|
128
|
+
function hasRawQueryOrFragment(value: string): boolean {
|
|
129
|
+
return value.includes("?") || value.includes("#");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Resolve a registry name or a caller-supplied endpoint into the validated form the client uses. A name resolves to
|
|
134
|
+
* its registry entry unchanged; an endpoint is validated field by field and returned as a fresh object whose `host`
|
|
135
|
+
* is origin-only. Messages never quote a caller value — a host can carry credentials in its user info — except the
|
|
136
|
+
* label, and only after it is validated. Validation runs on every call; nothing is cached.
|
|
137
|
+
*/
|
|
138
|
+
export function resolveBackend(backend?: BackendSpec): ResolvedBackend {
|
|
139
|
+
const spec: BackendSpec = backend === undefined ? DEFAULT_BACKEND : backend;
|
|
140
|
+
if (typeof spec === "string") {
|
|
141
|
+
const entry = backendConfig(spec);
|
|
142
|
+
return {
|
|
143
|
+
...entry,
|
|
144
|
+
name: spec,
|
|
145
|
+
host: entry.host,
|
|
146
|
+
keyEnv: entry.keyEnv ?? TYPESAFE_KEY_ENV,
|
|
147
|
+
defaultModel: defaultModelId(spec),
|
|
148
|
+
modelsVerifyKey: entry.modelsVerifyKey !== false,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (spec === null || typeof spec !== "object") {
|
|
152
|
+
refuse("backend must be a registry name or a backend object.");
|
|
153
|
+
}
|
|
154
|
+
const label = typeof spec.label === "string" ? spec.label.trim() : "";
|
|
155
|
+
if (!label || label.length > 60) refuse("Backend label must be a nonempty string of at most 60 characters.");
|
|
156
|
+
let host: URL;
|
|
157
|
+
if (typeof spec.host !== "string" || hasRawQueryOrFragment(spec.host)) refuse(HOST_MESSAGE);
|
|
158
|
+
try {
|
|
159
|
+
host = new URL(spec.host);
|
|
160
|
+
} catch {
|
|
161
|
+
refuse(HOST_MESSAGE);
|
|
162
|
+
}
|
|
163
|
+
const loopback = host.hostname === "localhost" || host.hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(host.hostname);
|
|
164
|
+
if (host.protocol !== "https:" && !(host.protocol === "http:" && loopback)) refuse(HOST_MESSAGE);
|
|
165
|
+
if (host.username !== "" || host.password !== "" || host.pathname !== "/" || host.search !== "" || host.hash !== "") refuse(HOST_MESSAGE);
|
|
166
|
+
for (const [field, message] of [["path", 'Backend path must be a string that starts with "/".'], ["modelsPath", 'Backend modelsPath must be a string that starts with "/".']] as const) {
|
|
167
|
+
const value = spec[field];
|
|
168
|
+
if (value !== undefined && (typeof value !== "string" || !value.startsWith("/") || hasRawQueryOrFragment(value))) refuse(message);
|
|
169
|
+
}
|
|
170
|
+
if (spec.modelsField !== undefined && (typeof spec.modelsField !== "string" || !spec.modelsField)) refuse("Backend modelsField must be a nonempty string.");
|
|
171
|
+
if (spec.modelsIdField !== undefined && (typeof spec.modelsIdField !== "string" || !spec.modelsIdField)) refuse("Backend modelsIdField must be a nonempty string.");
|
|
172
|
+
if (spec.modelsVerifyKey !== undefined && typeof spec.modelsVerifyKey !== "boolean") refuse("Backend modelsVerifyKey must be a boolean.");
|
|
173
|
+
if (typeof spec.keyEnv !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.keyEnv)) refuse(KEY_ENV_MESSAGE);
|
|
174
|
+
if (spec.keyEnv.toUpperCase() === TYPESAFE_KEY_ENV) {
|
|
175
|
+
refuse("Backend keyEnv must not be TYPESAFE_API_KEY: the TypeSafe key is only sent to the typesafe backend. Give this endpoint its own variable.");
|
|
176
|
+
}
|
|
177
|
+
if (spec.defaultModel !== undefined && (typeof spec.defaultModel !== "string" || !spec.defaultModel.trim() || spec.defaultModel.length > 100)) {
|
|
178
|
+
refuse("Backend defaultModel must be a nonempty string of at most 100 characters.");
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
label,
|
|
182
|
+
host: host.origin,
|
|
183
|
+
keyEnv: spec.keyEnv,
|
|
184
|
+
modelsVerifyKey: spec.modelsVerifyKey === true,
|
|
185
|
+
...(spec.path === undefined ? {} : { path: spec.path }),
|
|
186
|
+
...(spec.modelsPath === undefined ? {} : { modelsPath: spec.modelsPath }),
|
|
187
|
+
...(spec.modelsField === undefined ? {} : { modelsField: spec.modelsField }),
|
|
188
|
+
...(spec.modelsIdField === undefined ? {} : { modelsIdField: spec.modelsIdField }),
|
|
189
|
+
...(spec.defaultModel === undefined ? {} : { defaultModel: spec.defaultModel }),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The destination host only — scheme, host, and port, e.g. `api.commandcode.ai` — for consent text. */
|
|
194
|
+
export function backendHost(backend?: BackendSpec): string {
|
|
195
|
+
return new URL(resolveBackend(backend).host).host;
|
|
196
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { Questions, SystemOneRequest, Usage } from "@typesafe-ai/sdk";
|
|
2
|
+
import type { Evaluation, TypeSafe } from "./client.js";
|
|
3
|
+
import { TypeSafeIntegrationError } from "./errors.js";
|
|
4
|
+
import { DEFAULT_MAX_QUESTIONS, prepareEvaluationRequest } from "./schema.js";
|
|
5
|
+
|
|
6
|
+
/** Default number of requests in flight. TypeSafe answers in isolation, so a small pool is enough. */
|
|
7
|
+
export const DEFAULT_CONCURRENCY = 4;
|
|
8
|
+
|
|
9
|
+
/** One item's outcome. `skipped` marks work that was never started because of an abort or a stop rule. */
|
|
10
|
+
export type Settled<T> =
|
|
11
|
+
| { readonly ok: true; readonly index: number; readonly value: T }
|
|
12
|
+
| { readonly ok: false; readonly index: number; readonly error: unknown; readonly skipped: boolean };
|
|
13
|
+
|
|
14
|
+
export interface FanOutOptions {
|
|
15
|
+
/** Requests in flight at once. Default: DEFAULT_CONCURRENCY. */
|
|
16
|
+
concurrency?: number;
|
|
17
|
+
/** Stops starting new work once aborted; in-flight work still finishes. */
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
/** Stop launching new work once this returns true for a failure, e.g. a `budget` error. */
|
|
20
|
+
stopOn?: (error: unknown) => boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Run `worker` over `items` with bounded concurrency, preserving input order. Never throws: every item comes back as a
|
|
25
|
+
* settled result. This is the pool the client's batching methods use, exported so script authors stop hand-rolling one.
|
|
26
|
+
*/
|
|
27
|
+
export async function fanOut<I, O>(items: readonly I[], worker: (item: I, index: number) => Promise<O>, options: FanOutOptions = {}): Promise<Settled<O>[]> {
|
|
28
|
+
const concurrency = Math.max(1, Math.floor(options.concurrency ?? DEFAULT_CONCURRENCY));
|
|
29
|
+
const results = new Array<Settled<O> | undefined>(items.length);
|
|
30
|
+
let next = 0;
|
|
31
|
+
let stopped = false;
|
|
32
|
+
const run = async (): Promise<void> => {
|
|
33
|
+
while (!stopped) {
|
|
34
|
+
const index = next++;
|
|
35
|
+
if (index >= items.length) return;
|
|
36
|
+
if (options.signal?.aborted) {
|
|
37
|
+
stopped = true;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
results[index] = { ok: true, index, value: await worker(items[index] as I, index) };
|
|
42
|
+
} catch (error) {
|
|
43
|
+
results[index] = { ok: false, index, error, skipped: false };
|
|
44
|
+
if (options.stopOn?.(error)) stopped = true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, run));
|
|
49
|
+
const reason = options.signal?.aborted ? "TypeSafe batch cancelled before this request was submitted." : "TypeSafe batch stopped after a failed request; this request was not submitted.";
|
|
50
|
+
for (let index = 0; index < items.length; index++) {
|
|
51
|
+
if (!results[index]) results[index] = { ok: false, index, error: new TypeSafeIntegrationError("aborted", reason), skipped: true };
|
|
52
|
+
}
|
|
53
|
+
return results as Settled<O>[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface BatchOptions {
|
|
57
|
+
/** Requests in flight at once. Default: DEFAULT_CONCURRENCY. */
|
|
58
|
+
concurrency?: number;
|
|
59
|
+
signal?: AbortSignal;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Per-request outcomes plus the merged view callers usually want. */
|
|
63
|
+
export interface BatchEvaluation<Q extends Questions = Questions> {
|
|
64
|
+
/** True when every request succeeded. */
|
|
65
|
+
readonly ok: boolean;
|
|
66
|
+
/** Per-request outcomes in input order. */
|
|
67
|
+
readonly results: readonly Settled<Evaluation<Q>>[];
|
|
68
|
+
/** Failures, including requests that were never submitted. */
|
|
69
|
+
readonly failures: number;
|
|
70
|
+
/** Work never started, because of an abort or a `budget` stop. */
|
|
71
|
+
readonly skipped: number;
|
|
72
|
+
/** Answers merged in input order; a repeated question id keeps the last answer. Empty when nothing succeeded. */
|
|
73
|
+
readonly answers: Partial<Evaluation<Q>["answers"]>;
|
|
74
|
+
/** The model of the first successful request, when there is one. */
|
|
75
|
+
readonly model?: string;
|
|
76
|
+
/** Usage summed over the requests that succeeded. */
|
|
77
|
+
readonly usage: Usage;
|
|
78
|
+
/** Wall-clock time for the whole batch. */
|
|
79
|
+
readonly elapsedMs: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function summarize<Q extends Questions>(results: readonly Settled<Evaluation<Q>>[], elapsedMs: number): BatchEvaluation<Q> {
|
|
83
|
+
const answers: Partial<Evaluation<Q>["answers"]> = {};
|
|
84
|
+
let inputTokens = 0;
|
|
85
|
+
let outputTokens = 0;
|
|
86
|
+
let succeeded = 0;
|
|
87
|
+
let skipped = 0;
|
|
88
|
+
let model: string | undefined;
|
|
89
|
+
for (const result of results) {
|
|
90
|
+
if (!result.ok) {
|
|
91
|
+
if (result.skipped) skipped++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
succeeded++;
|
|
95
|
+
Object.assign(answers, result.value.answers);
|
|
96
|
+
inputTokens += result.value.usage.input_tokens;
|
|
97
|
+
outputTokens += result.value.usage.output_tokens;
|
|
98
|
+
model ??= result.value.model;
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
ok: succeeded === results.length,
|
|
102
|
+
results,
|
|
103
|
+
failures: results.length - succeeded,
|
|
104
|
+
skipped,
|
|
105
|
+
answers,
|
|
106
|
+
...(model === undefined ? {} : { model }),
|
|
107
|
+
usage: { input_tokens: inputTokens, output_tokens: outputTokens },
|
|
108
|
+
elapsedMs,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A failure that is worth stopping the batch for: no more requests will be accepted, or the caller cancelled. */
|
|
113
|
+
function stopsBatch(error: unknown): boolean {
|
|
114
|
+
return error instanceof TypeSafeIntegrationError && (error.code === "budget" || error.code === "aborted");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Send several requests with bounded concurrency, in input order, and merge what came back. Each request passes through
|
|
119
|
+
* the same admission seam as `evaluate`, so an invalid request is one settled failure, not a thrown error. A `budget` or
|
|
120
|
+
* cancellation failure stops the rest from being submitted. Never throws.
|
|
121
|
+
*/
|
|
122
|
+
export async function evaluateMany<Q extends Questions>(
|
|
123
|
+
client: TypeSafe,
|
|
124
|
+
requests: readonly SystemOneRequest<Q>[],
|
|
125
|
+
options: BatchOptions = {},
|
|
126
|
+
): Promise<BatchEvaluation<Q>> {
|
|
127
|
+
const start = performance.now();
|
|
128
|
+
const results = await fanOut(requests, (request) => client.evaluate(prepareEvaluationRequest(request) as SystemOneRequest<Q>, options.signal ? { signal: options.signal } : {}), {
|
|
129
|
+
...(options.concurrency === undefined ? {} : { concurrency: options.concurrency }),
|
|
130
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
131
|
+
stopOn: stopsBatch,
|
|
132
|
+
});
|
|
133
|
+
return summarize(results, Math.round(performance.now() - start));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Split a request that asks more questions than one request may carry into chunks of at most `maxQuestions`. Sharing
|
|
138
|
+
* one state across several questions is one request; asking more than the per-request limit is the only reason to fan
|
|
139
|
+
* out, and the state is repeated in each chunk. The order of `questions` is preserved. A pure splitter: admission still
|
|
140
|
+
* happens once per chunk, in `evaluate` or `evaluateMany`, so what one accepts the others accept.
|
|
141
|
+
*/
|
|
142
|
+
export function chunkEvaluationRequest(
|
|
143
|
+
request: SystemOneRequest<Questions>,
|
|
144
|
+
options: { maxQuestions?: number } = {},
|
|
145
|
+
): SystemOneRequest<Questions>[] {
|
|
146
|
+
const limit = Math.max(1, Math.floor(options.maxQuestions ?? DEFAULT_MAX_QUESTIONS));
|
|
147
|
+
const questions = request.questions as Record<string, unknown>;
|
|
148
|
+
const entries = questions && typeof questions === "object" && !Array.isArray(questions) ? Object.entries(questions) : undefined;
|
|
149
|
+
// Anything that is not a plain question map, or that already fits, is one chunk and is validated later.
|
|
150
|
+
if (!entries || entries.length <= limit) return [request];
|
|
151
|
+
const chunks: SystemOneRequest<Questions>[] = [];
|
|
152
|
+
for (let index = 0; index < entries.length; index += limit) {
|
|
153
|
+
const questions = Object.fromEntries(entries.slice(index, index + limit));
|
|
154
|
+
chunks.push({ ...request, questions } as SystemOneRequest<Questions>);
|
|
155
|
+
}
|
|
156
|
+
return chunks;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Ask any number of questions about one state: chunk to the per-request limit, fan out, and merge the answers, usage,
|
|
161
|
+
* and model. Use this when one coherent state carries many independent questions; use `evaluate` for one request.
|
|
162
|
+
*/
|
|
163
|
+
export async function evaluateAll<Q extends Questions>(
|
|
164
|
+
client: TypeSafe,
|
|
165
|
+
request: SystemOneRequest<Q>,
|
|
166
|
+
options: BatchOptions & { maxQuestions?: number } = {},
|
|
167
|
+
): Promise<BatchEvaluation<Q>> {
|
|
168
|
+
const chunks = chunkEvaluationRequest(request as SystemOneRequest<Questions>, options.maxQuestions === undefined ? {} : { maxQuestions: options.maxQuestions }) as SystemOneRequest<Q>[];
|
|
169
|
+
return evaluateMany(client, chunks, options);
|
|
170
|
+
}
|