@parcha/agentrun-jev 0.1.0-beta.23 → 0.1.0-beta.27
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/index.js +132 -120
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -7,8 +7,8 @@ const OPENROUTER_BASE_URL = "https://openrouter.ai/api";
|
|
|
7
7
|
const OPENROUTER_DEFAULT_MODEL = "~typesafe/jev-latest";
|
|
8
8
|
/** The provider an environment's keys select: OpenRouter only when `OPENROUTER_API_KEY` is set and `TYPESAFE_API_KEY` is not. The runner itself never reads this. */
|
|
9
9
|
export function jevProviderFromEnv(env = process.env) {
|
|
10
|
-
const
|
|
11
|
-
return
|
|
10
|
+
const isSet = (name) => typeof env[name] === "string" && env[name].trim().length > 0;
|
|
11
|
+
return isSet(keyVariable.openrouter) && !isSet(keyVariable.typesafe) ? "openrouter" : "typesafe";
|
|
12
12
|
}
|
|
13
13
|
const IMAGE_HEADER = "TypeSafe-Danger-Zone-Images";
|
|
14
14
|
const MAX_IMAGE_BYTES = 16 * 1024 * 1024;
|
|
@@ -30,7 +30,7 @@ function* stringValues(value) {
|
|
|
30
30
|
else if (Array.isArray(value))
|
|
31
31
|
for (const each of value)
|
|
32
32
|
yield* stringValues(each);
|
|
33
|
-
else if (
|
|
33
|
+
else if (isRecord(value))
|
|
34
34
|
for (const each of Object.values(value))
|
|
35
35
|
yield* stringValues(each);
|
|
36
36
|
}
|
|
@@ -58,13 +58,13 @@ export class JevError extends Error {
|
|
|
58
58
|
this.responseDiagnostic = { reason: responseDiagnostic.reason };
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
-
const
|
|
61
|
+
const requireConfig = (condition, message) => {
|
|
62
62
|
if (!condition)
|
|
63
63
|
throw new JevError("configuration", message);
|
|
64
64
|
};
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
65
|
+
const isTimerMs = (n, allowZero = false) => Number.isSafeInteger(n) && n >= (allowZero ? 0 : 1) && n <= MAX_TIMER_MS;
|
|
66
|
+
const isRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
67
|
+
const isTokenCount = (n) => typeof n === "number" && Number.isSafeInteger(n) && n >= 0;
|
|
68
68
|
function abortable(promise, signal, error) {
|
|
69
69
|
return new Promise((resolve, reject) => {
|
|
70
70
|
const abort = () => { signal.removeEventListener("abort", abort); reject(error()); };
|
|
@@ -95,6 +95,119 @@ function retryAfterMs(headers) {
|
|
|
95
95
|
const date = Date.parse(value);
|
|
96
96
|
return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
|
|
97
97
|
}
|
|
98
|
+
/** What one call sends: the request, the body its hash is taken over, and the image header when it carries an image or a Box. All of it is
|
|
99
|
+
* checked before anything is sent: a request that cannot be valid throws the `invalid_request` JevError that names why. */
|
|
100
|
+
function prepareRequest(params, model, maxStateBytes, provider) {
|
|
101
|
+
let body;
|
|
102
|
+
let request;
|
|
103
|
+
const invalid = (reason, message, stateBytes) => new JevError("invalid_request", message, 0, undefined, { reason,
|
|
104
|
+
...(typeof params.label === "string" ? { label: params.label.slice(0, 200) } : {}),
|
|
105
|
+
...(stateBytes === undefined || maxStateBytes === null ? {} : { stateBytes, maxStateBytes }) });
|
|
106
|
+
const state = params.state;
|
|
107
|
+
let serializedState;
|
|
108
|
+
try {
|
|
109
|
+
// The SDK refuses a request with no state, so a null or missing state is refused here, before anything is sent.
|
|
110
|
+
if (typeof state !== "string" && (typeof state !== "object" || state === null))
|
|
111
|
+
throw new Error();
|
|
112
|
+
serializedState = JSON.stringify(state);
|
|
113
|
+
if (typeof serializedState !== "string")
|
|
114
|
+
throw new Error();
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
throw invalid("invalid_state", "Jev state must be JSON-serializable text, an object or an array.");
|
|
118
|
+
}
|
|
119
|
+
const stateBytes = Buffer.byteLength(serializedState, "utf8");
|
|
120
|
+
if (maxStateBytes !== null && stateBytes > maxStateBytes)
|
|
121
|
+
throw invalid("state_too_large", `Jev assembled state is ${stateBytes} UTF-8 bytes; configured host limit is ${maxStateBytes} bytes. No request was sent.`, stateBytes);
|
|
122
|
+
const callModel = params.model ?? model;
|
|
123
|
+
if (typeof callModel !== "string" || callModel.trim().length === 0)
|
|
124
|
+
throw invalid("unsupported_model", "model must be a nonempty string; no request was sent.");
|
|
125
|
+
try {
|
|
126
|
+
body = JSON.stringify({ state: JSON.parse(serializedState), model: callModel, questions: params.questions });
|
|
127
|
+
request = JSON.parse(body);
|
|
128
|
+
if (!isRecord(request.questions) || Object.keys(request.questions).length === 0)
|
|
129
|
+
throw new Error();
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
throw invalid("invalid_questions", "Jev requires a nonempty JSON question map; no request was sent.");
|
|
133
|
+
}
|
|
134
|
+
// Images and Box questions: validated whole before any send; the header goes with exactly these requests.
|
|
135
|
+
const hasBox = Object.values(request.questions).some(question => isRecord(question) && question.type === "box");
|
|
136
|
+
const given = params.assets ?? {};
|
|
137
|
+
if (!isRecord(given))
|
|
138
|
+
throw invalid("invalid_assets", "assets must map each reference to an image; no request was sent.");
|
|
139
|
+
const references = Object.keys(given);
|
|
140
|
+
const carriesImage = references.length > 0;
|
|
141
|
+
if ((carriesImage || hasBox) && provider !== "typesafe")
|
|
142
|
+
throw invalid("unsupported_provider", "Images and Box questions are sent to TypeSafe's own API only; the OpenRouter path is not verified for them. No request was sent.");
|
|
143
|
+
if (hasBox && !answersBox(callModel))
|
|
144
|
+
throw invalid("unsupported_model", `A Box question needs a model that answers it, by name: ${BOX_MODEL} or later, or jev-preview; jev-latest is refused whatever version it points at. No request was sent.`);
|
|
145
|
+
if (hasBox && !carriesImage)
|
|
146
|
+
throw invalid("invalid_assets", "A Box question locates something in an image, and the request carries none. No request was sent.");
|
|
147
|
+
let assets;
|
|
148
|
+
let assetDigests;
|
|
149
|
+
if (carriesImage) {
|
|
150
|
+
// A reference is replaced where it sits inside a string value of the state, an instruction or a criterion; keys (question ids, option names) are never replaced.
|
|
151
|
+
const texts = [...stringValues(request.state), ...Object.values(request.questions).flatMap(question => isRecord(question) ? [...stringValues(question.instructions), ...stringValues(question.criteria)] : [])];
|
|
152
|
+
assets = Object.create(null);
|
|
153
|
+
assetDigests = Object.create(null);
|
|
154
|
+
for (const reference of references) {
|
|
155
|
+
const image = given[reference];
|
|
156
|
+
const sound = reference.length > 0 && Buffer.byteLength(reference, "utf8") <= MAX_REFERENCE_BYTES && !/["\\\u0000-\u001f]/.test(reference)
|
|
157
|
+
&& isRecord(image) && image.bytes instanceof Uint8Array && (image.mime === "image/png" || image.mime === "image/jpeg")
|
|
158
|
+
&& image.bytes.byteLength > 0 && image.bytes.byteLength <= MAX_IMAGE_BYTES && startsWith(image.bytes, imageMagic[image.mime]);
|
|
159
|
+
if (!sound)
|
|
160
|
+
throw invalid("invalid_assets", `Each image is a PNG or JPEG file of at most ${MAX_IMAGE_BYTES / 1024 / 1024} MiB, with bytes that are that type and a plain reference. No request was sent.`);
|
|
161
|
+
if (!texts.some(text => text.includes(reference)))
|
|
162
|
+
throw invalid("invalid_assets", "Every image reference must appear in the state or a question. No request was sent.");
|
|
163
|
+
assets[reference] = new Image(image.bytes);
|
|
164
|
+
assetDigests[reference] = { sha256: createHash("sha256").update(image.bytes).digest("hex"), mime: image.mime, bytes: image.bytes.byteLength };
|
|
165
|
+
}
|
|
166
|
+
// The hash names the images by their own digests, so it identifies the request that was sent without holding the bytes.
|
|
167
|
+
body = JSON.stringify({ ...request, assets: assetDigests });
|
|
168
|
+
request = { ...request, assets };
|
|
169
|
+
}
|
|
170
|
+
const headers = carriesImage || hasBox ? { [IMAGE_HEADER]: "true" } : undefined;
|
|
171
|
+
return { request, body, ...(headers ? { headers } : {}) };
|
|
172
|
+
}
|
|
173
|
+
/** The runner's result from what the client returned at `attempt`, or the `invalid_response` JevError for the first rule it breaks.
|
|
174
|
+
* `questions` are the questions as they were sent. */
|
|
175
|
+
function readResponse(result, questions, body, pricing, attempt) {
|
|
176
|
+
const invalid = (reason, message) => new JevError("invalid_response", message, attempt, undefined, undefined, { reason });
|
|
177
|
+
if (!isRecord(result))
|
|
178
|
+
throw invalid("response_shape", "Jev returned an invalid response.");
|
|
179
|
+
const answers = result.answers;
|
|
180
|
+
// Checked against the questions as they were sent: a value JSON cannot carry (an undefined criterion) was never asked.
|
|
181
|
+
try {
|
|
182
|
+
validateAnswers(questions, answers);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
throw invalid(error instanceof SystemOneError && isSystemOneResponseReason(error.responseReason) ? error.responseReason : "answer_validation", "Jev returned invalid answers or probability distributions.");
|
|
186
|
+
}
|
|
187
|
+
const usage = result.usage;
|
|
188
|
+
// Usage carries token counts, a provider-reported cost (USD), or both; a half pair of counts or a bad cost is invalid.
|
|
189
|
+
const counted = isRecord(usage) && isTokenCount(usage.input_tokens) && isTokenCount(usage.output_tokens);
|
|
190
|
+
const reported = isRecord(usage) && usage.cost !== undefined && usage.cost !== null;
|
|
191
|
+
const reportedCost = reported && typeof usage.cost === "number" && Number.isFinite(usage.cost) && usage.cost >= 0 ? usage.cost : undefined;
|
|
192
|
+
const noCounts = isRecord(usage) && usage.input_tokens === undefined && usage.output_tokens === undefined;
|
|
193
|
+
// Accepted: no usage; both counts; or a valid cost with both counts or with neither.
|
|
194
|
+
const usageValid = usage == null || (isRecord(usage) && (reported ? reportedCost !== undefined && (counted || noCounts) : counted));
|
|
195
|
+
if (!usageValid)
|
|
196
|
+
throw invalid("token_usage", "Jev returned invalid token usage.");
|
|
197
|
+
if (result.model != null && (typeof result.model !== "string" || !result.model.trim())) {
|
|
198
|
+
throw invalid("model_identifier", "Jev returned an invalid model identifier.");
|
|
199
|
+
}
|
|
200
|
+
const metering = counted ? { input_tokens: usage.input_tokens, output_tokens: usage.output_tokens } : null;
|
|
201
|
+
// The provider's own cost wins; else the configured estimate; else unknown (null), never 0.
|
|
202
|
+
const cost = reportedCost !== undefined ? reportedCost : pricing && metering ? (metering.input_tokens * pricing.inputUsdPerMillionTokens + metering.output_tokens * pricing.outputUsdPerMillionTokens) / 1_000_000 : null;
|
|
203
|
+
if (cost !== null && !Number.isFinite(cost))
|
|
204
|
+
throw invalid("cost_range", "Jev cost estimate exceeded the numeric range.");
|
|
205
|
+
return {
|
|
206
|
+
answers, model: typeof result.model === "string" ? result.model : null,
|
|
207
|
+
usage: metering, cost_usd: cost,
|
|
208
|
+
request_sha256: createHash("sha256").update(body).digest("hex"),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
98
211
|
export function createJevRunner(options = {}) {
|
|
99
212
|
const timeoutMs = options.timeoutMs === null ? null : (options.timeoutMs ?? 30_000);
|
|
100
213
|
const maxStateBytes = options.maxStateBytes ?? null;
|
|
@@ -103,19 +216,19 @@ export function createJevRunner(options = {}) {
|
|
|
103
216
|
const retryMaxMs = options.retryMaxMs ?? 5_000;
|
|
104
217
|
// A JavaScript caller's `client: null` is no client, the same as an absent one.
|
|
105
218
|
const hostClient = options.client ?? undefined;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
219
|
+
requireConfig(timeoutMs === null || isTimerMs(timeoutMs), `timeoutMs must be null or a positive integer of at most ${MAX_TIMER_MS}.`);
|
|
220
|
+
requireConfig(maxStateBytes === null || (Number.isSafeInteger(maxStateBytes) && maxStateBytes > 0), "maxStateBytes must be null or a positive safe integer.");
|
|
221
|
+
requireConfig(timeoutMs !== null || hostClient !== undefined, "The TypeSafe SDK requires a finite timeout. For timeoutMs: null, supply a cancellation-only host client.");
|
|
222
|
+
requireConfig(Number.isInteger(maxAttempts) && maxAttempts >= 1 && maxAttempts <= 10, "maxAttempts must be an integer from 1 to 10.");
|
|
223
|
+
requireConfig(isTimerMs(retryBaseMs, true) && isTimerMs(retryMaxMs, true) && retryBaseMs <= retryMaxMs, "Retry delays must be nonnegative integers; retryBaseMs cannot exceed retryMaxMs.");
|
|
224
|
+
requireConfig(hostClient === undefined || (options.apiKey === undefined && options.baseURL === undefined && options.fetch === undefined && options.provider === undefined), "Supply either client or transport options, not both.");
|
|
112
225
|
const provider = options.provider ?? "typesafe";
|
|
113
|
-
|
|
226
|
+
requireConfig(provider === "typesafe" || provider === "openrouter", "provider must be typesafe or openrouter.");
|
|
114
227
|
const pricing = options.pricing ? { ...options.pricing } : undefined;
|
|
115
228
|
if (pricing)
|
|
116
|
-
|
|
229
|
+
requireConfig([pricing.inputUsdPerMillionTokens, pricing.outputUsdPerMillionTokens].every(n => Number.isFinite(n) && n >= 0), "Configured token prices must be finite nonnegative numbers.");
|
|
117
230
|
const model = options.model ?? (provider === "openrouter" ? OPENROUTER_DEFAULT_MODEL : "jev-latest");
|
|
118
|
-
|
|
231
|
+
requireConfig(typeof model === "string" && model.trim().length > 0, "model must be a nonempty string.");
|
|
119
232
|
let client;
|
|
120
233
|
try {
|
|
121
234
|
const key = options.apiKey ?? (provider === "openrouter" ? process.env[keyVariable.openrouter] : undefined);
|
|
@@ -142,82 +255,12 @@ export function createJevRunner(options = {}) {
|
|
|
142
255
|
const abortError = () => (runnerSignal?.aborted || params.signal?.aborted)
|
|
143
256
|
? new JevError("aborted", "Jev request was cancelled.", attempt)
|
|
144
257
|
: new JevError("timeout", "Jev request exceeded its total deadline.", attempt);
|
|
145
|
-
const invalidResponse = (reason, message) => new JevError("invalid_response", message, attempt, undefined, undefined, { reason });
|
|
146
258
|
if (signal.aborted)
|
|
147
259
|
throw abortError();
|
|
148
260
|
const timer = timeoutMs === null ? undefined : setTimeout(() => deadline.abort(), timeoutMs);
|
|
149
261
|
const started = Date.now();
|
|
150
262
|
try {
|
|
151
|
-
|
|
152
|
-
let request;
|
|
153
|
-
const invalid = (reason, message, stateBytes) => new JevError("invalid_request", message, 0, undefined, { reason,
|
|
154
|
-
...(typeof params.label === "string" ? { label: params.label.slice(0, 200) } : {}),
|
|
155
|
-
...(stateBytes === undefined || maxStateBytes === null ? {} : { stateBytes, maxStateBytes }) });
|
|
156
|
-
const state = params.state;
|
|
157
|
-
let serializedState;
|
|
158
|
-
try {
|
|
159
|
-
// The SDK refuses a request with no state, so a null or missing state is refused here, before anything is sent.
|
|
160
|
-
if (typeof state !== "string" && (typeof state !== "object" || state === null))
|
|
161
|
-
throw new Error();
|
|
162
|
-
serializedState = JSON.stringify(state);
|
|
163
|
-
if (typeof serializedState !== "string")
|
|
164
|
-
throw new Error();
|
|
165
|
-
}
|
|
166
|
-
catch {
|
|
167
|
-
throw invalid("invalid_state", "Jev state must be JSON-serializable text, an object or an array.");
|
|
168
|
-
}
|
|
169
|
-
const stateBytes = Buffer.byteLength(serializedState, "utf8");
|
|
170
|
-
if (maxStateBytes !== null && stateBytes > maxStateBytes)
|
|
171
|
-
throw invalid("state_too_large", `Jev assembled state is ${stateBytes} UTF-8 bytes; configured host limit is ${maxStateBytes} bytes. No request was sent.`, stateBytes);
|
|
172
|
-
const callModel = params.model ?? model;
|
|
173
|
-
if (typeof callModel !== "string" || callModel.trim().length === 0)
|
|
174
|
-
throw invalid("unsupported_model", "model must be a nonempty string; no request was sent.");
|
|
175
|
-
try {
|
|
176
|
-
body = JSON.stringify({ state: JSON.parse(serializedState), model: callModel, questions: params.questions });
|
|
177
|
-
request = JSON.parse(body);
|
|
178
|
-
if (!record(request.questions) || Object.keys(request.questions).length === 0)
|
|
179
|
-
throw new Error();
|
|
180
|
-
}
|
|
181
|
-
catch {
|
|
182
|
-
throw invalid("invalid_questions", "Jev requires a nonempty JSON question map; no request was sent.");
|
|
183
|
-
}
|
|
184
|
-
// Images and Box questions: validated whole before any send; the header goes with exactly these requests.
|
|
185
|
-
const hasBox = Object.values(request.questions).some(question => record(question) && question.type === "box");
|
|
186
|
-
const given = params.assets ?? {};
|
|
187
|
-
if (!record(given))
|
|
188
|
-
throw invalid("invalid_assets", "assets must map each reference to an image; no request was sent.");
|
|
189
|
-
const references = Object.keys(given);
|
|
190
|
-
const carriesImage = references.length > 0;
|
|
191
|
-
if ((carriesImage || hasBox) && provider !== "typesafe")
|
|
192
|
-
throw invalid("unsupported_provider", "Images and Box questions are sent to TypeSafe's own API only; the OpenRouter path is not verified for them. No request was sent.");
|
|
193
|
-
if (hasBox && !answersBox(callModel))
|
|
194
|
-
throw invalid("unsupported_model", `A Box question needs a model that answers it, by name: ${BOX_MODEL} or later, or jev-preview; jev-latest is refused whatever version it points at. No request was sent.`);
|
|
195
|
-
if (hasBox && !carriesImage)
|
|
196
|
-
throw invalid("invalid_assets", "A Box question locates something in an image, and the request carries none. No request was sent.");
|
|
197
|
-
let assets;
|
|
198
|
-
let assetDigests;
|
|
199
|
-
if (carriesImage) {
|
|
200
|
-
// A reference is replaced where it sits inside a string value of the state, an instruction or a criterion; keys (question ids, option names) are never replaced.
|
|
201
|
-
const texts = [...stringValues(JSON.parse(serializedState)), ...Object.values(request.questions).flatMap(question => record(question) ? [...stringValues(question.instructions), ...stringValues(question.criteria)] : [])];
|
|
202
|
-
assets = Object.create(null);
|
|
203
|
-
assetDigests = Object.create(null);
|
|
204
|
-
for (const reference of references) {
|
|
205
|
-
const image = given[reference];
|
|
206
|
-
const sound = reference.length > 0 && Buffer.byteLength(reference, "utf8") <= MAX_REFERENCE_BYTES && !/["\\\u0000-\u001f]/.test(reference)
|
|
207
|
-
&& record(image) && image.bytes instanceof Uint8Array && (image.mime === "image/png" || image.mime === "image/jpeg")
|
|
208
|
-
&& image.bytes.byteLength > 0 && image.bytes.byteLength <= MAX_IMAGE_BYTES && startsWith(image.bytes, imageMagic[image.mime]);
|
|
209
|
-
if (!sound)
|
|
210
|
-
throw invalid("invalid_assets", `Each image is a PNG or JPEG file of at most ${MAX_IMAGE_BYTES / 1024 / 1024} MiB, with bytes that are that type and a plain reference. No request was sent.`);
|
|
211
|
-
if (!texts.some(text => text.includes(reference)))
|
|
212
|
-
throw invalid("invalid_assets", "Every image reference must appear in the state or a question. No request was sent.");
|
|
213
|
-
assets[reference] = new Image(image.bytes);
|
|
214
|
-
assetDigests[reference] = { sha256: createHash("sha256").update(image.bytes).digest("hex"), mime: image.mime, bytes: image.bytes.byteLength };
|
|
215
|
-
}
|
|
216
|
-
request = { ...request, assets };
|
|
217
|
-
// The hash names the images by their own digests, so it identifies the request that was sent without holding the bytes.
|
|
218
|
-
body = JSON.stringify({ ...JSON.parse(body), assets: assetDigests });
|
|
219
|
-
}
|
|
220
|
-
const headers = carriesImage || hasBox ? { [IMAGE_HEADER]: "true" } : undefined;
|
|
263
|
+
const { request, body, headers } = prepareRequest(params, model, maxStateBytes, provider);
|
|
221
264
|
// The first attempt that failed in a way the server may have billed (a lost answer, a 408, a 5xx). A later 429 does not make the call free: the give-up reports this one.
|
|
222
265
|
let billable;
|
|
223
266
|
for (attempt = 1;; attempt++) {
|
|
@@ -256,7 +299,7 @@ export function createJevRunner(options = {}) {
|
|
|
256
299
|
throw billable.status === undefined ? new JevError("connection", "Jev transport failed.", attempt) : new JevError("http", `Jev returned HTTP ${billable.status}.`, attempt, billable.status);
|
|
257
300
|
if (status !== undefined) {
|
|
258
301
|
const safeStatus = Number.isInteger(status) && status >= 100 && status <= 599 ? status : undefined;
|
|
259
|
-
const capacity = error instanceof APIError &&
|
|
302
|
+
const capacity = error instanceof APIError && isRecord(error.body) && isRecord(error.body.detail)
|
|
260
303
|
&& error.body.detail.error_type === "max_tokens_exceeded";
|
|
261
304
|
// Only the structured reason is named; the provider's body text never reaches the message.
|
|
262
305
|
const returned = safeStatus === undefined ? "Jev returned an HTTP failure" : `Jev returned HTTP ${safeStatus}`;
|
|
@@ -268,38 +311,7 @@ export function createJevRunner(options = {}) {
|
|
|
268
311
|
throw new JevError("connection", "Jev transport failed.", attempt);
|
|
269
312
|
throw new JevError("invalid_request", "Jev could not process the request. Check question and transport configuration.", attempt);
|
|
270
313
|
}
|
|
271
|
-
|
|
272
|
-
throw invalidResponse("response_shape", "Jev returned an invalid response.");
|
|
273
|
-
const answers = result.answers;
|
|
274
|
-
// Checked against the questions as they were sent: a value JSON cannot carry (an undefined criterion) was never asked.
|
|
275
|
-
try {
|
|
276
|
-
validateAnswers(request.questions, answers);
|
|
277
|
-
}
|
|
278
|
-
catch (error) {
|
|
279
|
-
throw invalidResponse(error instanceof SystemOneError && isSystemOneResponseReason(error.responseReason) ? error.responseReason : "answer_validation", "Jev returned invalid answers or probability distributions.");
|
|
280
|
-
}
|
|
281
|
-
const usage = result.usage;
|
|
282
|
-
// Usage carries token counts, a provider-reported cost (USD), or both; a half pair of counts or a bad cost is invalid.
|
|
283
|
-
const counted = record(usage) && tokens(usage.input_tokens) && tokens(usage.output_tokens);
|
|
284
|
-
const reported = record(usage) && usage.cost !== undefined && usage.cost !== null;
|
|
285
|
-
const reportedCost = reported && typeof usage.cost === "number" && Number.isFinite(usage.cost) && usage.cost >= 0 ? usage.cost : undefined;
|
|
286
|
-
const noCounts = record(usage) && usage.input_tokens === undefined && usage.output_tokens === undefined;
|
|
287
|
-
if (usage != null && (!record(usage) || (reported ? reportedCost === undefined || !(counted || noCounts) : !counted))) {
|
|
288
|
-
throw invalidResponse("token_usage", "Jev returned invalid token usage.");
|
|
289
|
-
}
|
|
290
|
-
if (result.model != null && (typeof result.model !== "string" || !result.model.trim())) {
|
|
291
|
-
throw invalidResponse("model_identifier", "Jev returned an invalid model identifier.");
|
|
292
|
-
}
|
|
293
|
-
const metering = counted ? { input_tokens: usage.input_tokens, output_tokens: usage.output_tokens } : null;
|
|
294
|
-
// The provider's own cost wins; else the configured estimate; else unknown (null), never 0.
|
|
295
|
-
const cost = reportedCost !== undefined ? reportedCost : pricing && metering ? (metering.input_tokens * pricing.inputUsdPerMillionTokens + metering.output_tokens * pricing.outputUsdPerMillionTokens) / 1_000_000 : null;
|
|
296
|
-
if (cost !== null && !Number.isFinite(cost))
|
|
297
|
-
throw invalidResponse("cost_range", "Jev cost estimate exceeded the numeric range.");
|
|
298
|
-
return {
|
|
299
|
-
answers, model: typeof result.model === "string" ? result.model : null,
|
|
300
|
-
usage: metering, cost_usd: cost,
|
|
301
|
-
request_sha256: createHash("sha256").update(body).digest("hex"),
|
|
302
|
-
};
|
|
314
|
+
return readResponse(result, request.questions, body, pricing, attempt);
|
|
303
315
|
}
|
|
304
316
|
}
|
|
305
317
|
finally {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parcha/agentrun-jev",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.27",
|
|
4
4
|
"description": "Optional Jev system one decisions for AgentRun workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"test": "node --test test/*.test.mjs"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@parcha/agentrun-dsl": "0.1.0-beta.
|
|
30
|
+
"@parcha/agentrun-dsl": "0.1.0-beta.27",
|
|
31
31
|
"@typesafe-ai/sdk": "0.7.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|