@parcha/agentrun-jev 0.1.0-beta.13 → 0.1.0-beta.15
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/README.md +45 -0
- package/dist/index.d.ts +28 -3
- package/dist/index.js +95 -10
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -48,6 +48,21 @@ const runJudge = createJevRunner({
|
|
|
48
48
|
});
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
### Through OpenRouter
|
|
52
|
+
|
|
53
|
+
Jev is also served by OpenRouter's System One path, on the same request shape. Choose it with `provider`:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { createJevRunner, jevProviderFromEnv } from "@parcha/agentrun-jev";
|
|
57
|
+
|
|
58
|
+
const runJudge = createJevRunner({ provider: "openrouter" }); // key: apiKey, else OPENROUTER_API_KEY
|
|
59
|
+
const byEnv = createJevRunner({ provider: jevProviderFromEnv(process.env) }); // OpenRouter only when only OPENROUTER_API_KEY is set
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`provider: "openrouter"` sends to `https://openrouter.ai/api/v1/systemone` (an explicit `baseURL` wins) and defaults the model to `~typesafe/jev-latest`; pin `typesafe/jev-1.14` when a result must be reproducible. It never falls back to `TYPESAFE_API_KEY`: with no OpenRouter key it is a configuration error. The default provider is `typesafe` and nothing changes for it. `provider` cannot be combined with `client`.
|
|
63
|
+
|
|
64
|
+
Cost: when the response reports `usage.cost` (USD) it is `cost_usd`; otherwise a configured `pricing` estimate is used; otherwise `cost_usd` is `null`, never 0. A `usage` with a cost and no token counts is accepted and returns `usage: null`. Requests over about 32 KB of state run in the provider's slower long-context mode.
|
|
65
|
+
|
|
51
66
|
The official `@typesafe-ai/sdk` resolves `TYPESAFE_API_KEY` and `TYPESAFE_BASE_URL`. Explicit `apiKey` and `baseURL` options take precedence. An API root ends before `/v1/systemone`. Set `fetch` for a custom transport, or supply a `client` with a `systemOne(request, options)` method. A custom client must honor the request signal and disable its own retries; it owns its credentials and cannot be combined with transport options.
|
|
52
67
|
|
|
53
68
|
The adapter retries transient HTTP 408, 429, 5xx responses and connection failures with bounded exponential delays. SDK retries are disabled. Validation failures and other HTTP errors do not retry. The deadline and either the runner or per-call signal interrupt both requests and backoff. Cancellation bounds when the adapter returns; an uncooperative custom client can still continue its own work.
|
|
@@ -66,6 +81,36 @@ policy, or the submitted evidence. Unknown causes remain unspecified.
|
|
|
66
81
|
|
|
67
82
|
Successful results retain `answers`, reported `model`, reported token `usage`, and a SHA-256 of the submitted request. `cost_usd` is `null` unless both usage and explicit `pricing: { inputUsdPerMillionTokens, outputUsdPerMillionTokens }` are available. Configured prices produce an estimate for the successful response; costs from unsuccessful or interrupted attempts are unknown.
|
|
68
83
|
|
|
84
|
+
## Images and Box questions
|
|
85
|
+
|
|
86
|
+
Jev 1.13 and 1.14 read still images, and Jev 1.14 can answer a Box question: where in an image something is. TypeSafe leaves both off unless a request says it carries one, and the runner says so for you: it sends `TypeSafe-Danger-Zone-Images: true` on every request that carries an image or a Box question and on no other.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { createJevRunner, type JevJudge } from '@parcha/agentrun-jev';
|
|
90
|
+
|
|
91
|
+
const judge: JevJudge = createJevRunner({ model: 'jev-1.14.0' });
|
|
92
|
+
const { answers, usage, cost_usd } = await judge({
|
|
93
|
+
label: 'door', kind: 'judge',
|
|
94
|
+
state: 'The camera shows <photo>.',
|
|
95
|
+
questions: {
|
|
96
|
+
open: { type: 'noul', instructions: 'Is the door in <photo> open?' },
|
|
97
|
+
door: { type: 'box', instructions: 'the door in <photo>' },
|
|
98
|
+
},
|
|
99
|
+
assets: { '<photo>': { bytes: pngOrJpegFileBytes, mime: 'image/png' } }, // PNG or JPEG file bytes
|
|
100
|
+
});
|
|
101
|
+
// answers.door = { type: 'box', box: { top, bottom, left, right }, confidence } — edges are fractions of the image's height and width
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- `assets` maps each reference, a string that appears literally in `state` or in a question's `instructions` or `criteria`, to the image. The runner builds the SDK's image from the bytes and sends it as a multipart file part. Each reference must appear inside a string value of the state, an instruction or a criterion (a question id or an option name does not count), or the request is refused.
|
|
105
|
+
- A Box question needs `model: 'jev-1.14.0'` (or `'jev-preview'`); `jev-latest` is 1.13 and does not answer it. A Box on any other model, or a Box with no image, is refused with `JevError` code `invalid_request` before any request. `model` can also be named per call.
|
|
106
|
+
- Before any request, the runner also refuses what Jev would reject: a file that is not PNG or JPEG (the bytes are checked against `mime`), an empty file, a file over 16 MiB, a reference nothing uses. It does not check pixel dimensions (each side 280 to 1568, total 78,400 to 2,458,624 pixels) or the 32 MiB request limit; Jev rejects those with a 400 or 413, which surfaces as a typed `http` error.
|
|
107
|
+
- Images are billed as input tokens at the text price, and `usage.input_tokens` counts them. `cost_usd` is as for text: the provider's reported cost, else your `pricing` applied to the counted tokens, else `null`. `request_sha256` names each image by its own SHA-256, so the same request hashes the same and a different image does not.
|
|
108
|
+
- A Box answer is always a box (there is no "not found"): ask a noul question in the same request to check presence. Edges and confidence are checked to lie in [0, 1] and the box to run forward (`top <= bottom`, `left <= right`); a confidence the provider omits is `null`.
|
|
109
|
+
- `JevJudge`, `JevJudgeParams`, `JevJudgeResult` and `JevImage` are the types of this call. A package that asks Jev about images takes a `JevJudge` from its host and never imports the SDK.
|
|
110
|
+
- Images and Box go to TypeSafe's own API only. The OpenRouter path is not verified for them, so a request that carries either is refused with `provider: "openrouter"`.
|
|
111
|
+
- Request bodies are sent uncompressed; the SDK's default compresses them with zstd.
|
|
112
|
+
- Box is a preview: Jev's page says its request format, answers and limits can change between releases.
|
|
113
|
+
|
|
69
114
|
## Offline tests
|
|
70
115
|
|
|
71
116
|
Inject a client to exercise the real adapter without a key or network request. After installing the DSL and Jev packages, save this as `offline-jev.mjs` and run `node offline-jev.mjs`:
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,32 @@ import type { WorkflowDeps, SystemOneResponseReason } from "@parcha/agentrun-dsl
|
|
|
4
4
|
export interface JevClient {
|
|
5
5
|
systemOne(request: SystemOneRequest, options: RequestOptions): PromiseLike<unknown>;
|
|
6
6
|
}
|
|
7
|
+
/** Where Jev is reached: TypeSafe's own API (default) or OpenRouter's System One path. */
|
|
8
|
+
export type JevProvider = "typesafe" | "openrouter";
|
|
9
|
+
/** 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. */
|
|
10
|
+
export declare function jevProviderFromEnv(env?: Record<string, string | undefined>): JevProvider;
|
|
11
|
+
/** A still image a request carries: PNG or JPEG file bytes and the type they are. The runner builds the SDK's image from it and sends it as a multipart file part. */
|
|
12
|
+
export interface JevImage {
|
|
13
|
+
bytes: Uint8Array;
|
|
14
|
+
mime: "image/png" | "image/jpeg";
|
|
15
|
+
}
|
|
16
|
+
type RunJudge = NonNullable<WorkflowDeps["runJudge"]>;
|
|
17
|
+
/** What one call to the runner takes: the interpreter's `runJudge` parameters, plus the images the request carries and an optional model for this call.
|
|
18
|
+
* `assets` maps each reference, a string that appears literally in `state` or in a question's `instructions` or `criteria` (`"<photo>"`), to its image;
|
|
19
|
+
* every reference must appear, and a Box question needs at least one image. */
|
|
20
|
+
export type JevJudgeParams = Parameters<RunJudge>[0] & {
|
|
21
|
+
assets?: Record<string, JevImage>;
|
|
22
|
+
/** Names the model for this call, over the runner's `model`. A Box question needs `jev-1.14.0` or `jev-preview`; the default `jev-latest` is 1.13. */
|
|
23
|
+
model?: string;
|
|
24
|
+
};
|
|
25
|
+
/** The typed answers (choice, noul, score with their probabilities; box with its edges and confidence), the model that answered, its token usage, and
|
|
26
|
+
* the cost: the provider's reported cost, else the host's configured price applied to the counted tokens, else null. */
|
|
27
|
+
export type JevJudgeResult = Awaited<ReturnType<RunJudge>>;
|
|
28
|
+
/** The judge `createJevRunner` returns, and the port a host passes to a package that asks Jev about images without importing the SDK. */
|
|
29
|
+
export type JevJudge = (params: JevJudgeParams) => Promise<JevJudgeResult>;
|
|
7
30
|
export interface JevOptions {
|
|
31
|
+
/** `typesafe` (default) or `openrouter`. OpenRouter sets the API root and the default model (`~typesafe/jev-latest`); its key is `apiKey` or `OPENROUTER_API_KEY`. Cannot be combined with `client`. */
|
|
32
|
+
provider?: JevProvider;
|
|
8
33
|
apiKey?: string;
|
|
9
34
|
/** API root, without /v1/systemone; useful for an explicitly configured gateway. */
|
|
10
35
|
baseURL?: string;
|
|
@@ -20,7 +45,7 @@ export interface JevOptions {
|
|
|
20
45
|
maxAttempts?: number;
|
|
21
46
|
retryBaseMs?: number;
|
|
22
47
|
retryMaxMs?: number;
|
|
23
|
-
/** Explicit estimate; unknown prices are never silently treated as free. */
|
|
48
|
+
/** Explicit estimate, used when the provider reports no cost; unknown prices are never silently treated as free. */
|
|
24
49
|
pricing?: {
|
|
25
50
|
inputUsdPerMillionTokens: number;
|
|
26
51
|
outputUsdPerMillionTokens: number;
|
|
@@ -34,7 +59,7 @@ export type JevResponseDiagnostic = {
|
|
|
34
59
|
reason: JevResponseReason;
|
|
35
60
|
};
|
|
36
61
|
export type JevRequestDiagnostic = {
|
|
37
|
-
reason: "invalid_state" | "state_too_large" | "invalid_questions";
|
|
62
|
+
reason: "invalid_state" | "state_too_large" | "invalid_questions" | "invalid_assets" | "unsupported_model" | "unsupported_provider";
|
|
38
63
|
label?: string;
|
|
39
64
|
stateBytes?: number;
|
|
40
65
|
maxStateBytes?: number;
|
|
@@ -48,5 +73,5 @@ export declare class JevError extends Error {
|
|
|
48
73
|
readonly responseDiagnostic?: JevResponseDiagnostic;
|
|
49
74
|
constructor(code: JevErrorCode, message: string, attempts?: number, status?: number | undefined, requestDiagnostic?: JevRequestDiagnostic | undefined, responseDiagnostic?: JevResponseDiagnostic);
|
|
50
75
|
}
|
|
51
|
-
export declare function createJevRunner(options?: JevOptions):
|
|
76
|
+
export declare function createJevRunner(options?: JevOptions): JevJudge;
|
|
52
77
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { APIConnectionError, APIError, TypeSafeClient } from "@typesafe-ai/sdk";
|
|
2
|
+
import { APIConnectionError, APIError, Image, TypeSafeClient } from "@typesafe-ai/sdk";
|
|
3
3
|
import { validateAnswers, SystemOneError, isSystemOneResponseReason } from "@parcha/agentrun-dsl";
|
|
4
|
+
const keyVariable = { typesafe: "TYPESAFE_API_KEY", openrouter: "OPENROUTER_API_KEY" };
|
|
5
|
+
const OPENROUTER_BASE_URL = "https://openrouter.ai/api";
|
|
6
|
+
const OPENROUTER_DEFAULT_MODEL = "~typesafe/jev-latest";
|
|
7
|
+
/** 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. */
|
|
8
|
+
export function jevProviderFromEnv(env = process.env) {
|
|
9
|
+
const set = (name) => typeof env[name] === "string" && env[name].trim().length > 0;
|
|
10
|
+
return set("OPENROUTER_API_KEY") && !set("TYPESAFE_API_KEY") ? "openrouter" : "typesafe";
|
|
11
|
+
}
|
|
12
|
+
const IMAGE_HEADER = "TypeSafe-Danger-Zone-Images";
|
|
13
|
+
const MAX_IMAGE_BYTES = 16 * 1024 * 1024;
|
|
14
|
+
const BOX_MODEL = "jev-1.14.0";
|
|
15
|
+
/** Models that answer a Box question: Jev 1.14 and later by name, and the preview alias. `jev-latest` is 1.13 and does not. */
|
|
16
|
+
function answersBox(model) {
|
|
17
|
+
if (model === "jev-preview")
|
|
18
|
+
return true;
|
|
19
|
+
const version = /^jev-(\d+)\.(\d+)(?:\.\d+)?$/.exec(model);
|
|
20
|
+
return version !== null && (Number(version[1]) > 1 || (Number(version[1]) === 1 && Number(version[2]) >= 14));
|
|
21
|
+
}
|
|
22
|
+
/** Every string value of a JSON value, not its keys. */
|
|
23
|
+
function* stringValues(value) {
|
|
24
|
+
if (typeof value === "string")
|
|
25
|
+
yield value;
|
|
26
|
+
else if (Array.isArray(value))
|
|
27
|
+
for (const each of value)
|
|
28
|
+
yield* stringValues(each);
|
|
29
|
+
else if (record(value))
|
|
30
|
+
for (const each of Object.values(value))
|
|
31
|
+
yield* stringValues(each);
|
|
32
|
+
}
|
|
33
|
+
const startsWith = (bytes, prefix) => prefix.every((byte, index) => bytes[index] === byte);
|
|
34
|
+
const imageMagic = { "image/png": [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], "image/jpeg": [0xff, 0xd8, 0xff] };
|
|
4
35
|
const adapterResponseReasons = ["response_shape", "token_usage", "model_identifier", "cost_range", "answer_validation", "max_tokens_exceeded"];
|
|
5
36
|
export function isJevResponseReason(value) {
|
|
6
37
|
return isSystemOneResponseReason(value) || typeof value === "string" && adapterResponseReasons.includes(value);
|
|
@@ -62,21 +93,29 @@ export function createJevRunner(options = {}) {
|
|
|
62
93
|
configuration(timeoutMs !== null || options.client !== undefined, "The TypeSafe SDK requires a finite timeout. For timeoutMs: null, supply a cancellation-only host client.");
|
|
63
94
|
configuration(Number.isInteger(maxAttempts) && maxAttempts >= 1 && maxAttempts <= 10, "maxAttempts must be an integer from 1 to 10.");
|
|
64
95
|
configuration(milliseconds(retryBaseMs, true) && milliseconds(retryMaxMs, true) && retryBaseMs <= retryMaxMs, "Retry delays must be nonnegative integers; retryBaseMs cannot exceed retryMaxMs.");
|
|
65
|
-
configuration(!options.client || (options.apiKey === undefined && options.baseURL === undefined && options.fetch === undefined), "Supply either client or transport options, not both.");
|
|
96
|
+
configuration(!options.client || (options.apiKey === undefined && options.baseURL === undefined && options.fetch === undefined && options.provider === undefined), "Supply either client or transport options, not both.");
|
|
97
|
+
const provider = options.provider ?? "typesafe";
|
|
98
|
+
configuration(provider === "typesafe" || provider === "openrouter", "provider must be typesafe or openrouter.");
|
|
66
99
|
const pricing = options.pricing ? { ...options.pricing } : undefined;
|
|
67
100
|
if (pricing)
|
|
68
101
|
configuration([pricing.inputUsdPerMillionTokens, pricing.outputUsdPerMillionTokens].every(n => Number.isFinite(n) && n >= 0), "Configured token prices must be finite nonnegative numbers.");
|
|
69
|
-
const model = options.model ?? "jev-latest";
|
|
102
|
+
const model = options.model ?? (provider === "openrouter" ? OPENROUTER_DEFAULT_MODEL : "jev-latest");
|
|
70
103
|
configuration(typeof model === "string" && model.trim().length > 0, "model must be a nonempty string.");
|
|
71
104
|
let client;
|
|
72
105
|
try {
|
|
106
|
+
const key = options.apiKey ?? (provider === "openrouter" ? process.env.OPENROUTER_API_KEY : undefined);
|
|
107
|
+
// The SDK falls back to TYPESAFE_API_KEY when given no key: for OpenRouter that would send one provider's key to another.
|
|
108
|
+
if (provider === "openrouter" && (typeof key !== "string" || key.trim().length === 0))
|
|
109
|
+
throw new Error("no key");
|
|
73
110
|
client = options.client ?? new TypeSafeClient({
|
|
74
|
-
apiKey:
|
|
111
|
+
apiKey: key, baseURL: options.baseURL ?? (provider === "openrouter" ? OPENROUTER_BASE_URL : undefined), fetch: options.fetch,
|
|
75
112
|
defaultModel: model, retry: { maxRetries: 0 }, timeout: timeoutMs ?? undefined, logLevel: "off",
|
|
113
|
+
// Bodies go as they always have: uncompressed JSON, which a host's own fetch, a gateway and OpenRouter all read.
|
|
114
|
+
compression: false,
|
|
76
115
|
});
|
|
77
116
|
}
|
|
78
117
|
catch {
|
|
79
|
-
throw new JevError("configuration",
|
|
118
|
+
throw new JevError("configuration", `Could not configure Jev. Supply apiKey or set ${keyVariable[provider]}, and check transport options.`);
|
|
80
119
|
}
|
|
81
120
|
const runnerSignal = options.signal;
|
|
82
121
|
return async (params) => {
|
|
@@ -112,8 +151,11 @@ export function createJevRunner(options = {}) {
|
|
|
112
151
|
const stateBytes = Buffer.byteLength(serializedState, "utf8");
|
|
113
152
|
if (maxStateBytes !== null && stateBytes > maxStateBytes)
|
|
114
153
|
throw invalid("state_too_large", `Jev assembled state is ${stateBytes} UTF-8 bytes; configured host limit is ${maxStateBytes} bytes. No request was sent.`, stateBytes);
|
|
154
|
+
const callModel = params.model ?? model;
|
|
155
|
+
if (typeof callModel !== "string" || callModel.trim().length === 0)
|
|
156
|
+
throw invalid("invalid_questions", "model must be a nonempty string; no request was sent.");
|
|
115
157
|
try {
|
|
116
|
-
body = JSON.stringify({ state: JSON.parse(serializedState), model, questions: params.questions });
|
|
158
|
+
body = JSON.stringify({ state: JSON.parse(serializedState), model: callModel, questions: params.questions });
|
|
117
159
|
request = JSON.parse(body);
|
|
118
160
|
if (!record(request.questions) || Object.keys(request.questions).length === 0)
|
|
119
161
|
throw new Error();
|
|
@@ -121,13 +163,50 @@ export function createJevRunner(options = {}) {
|
|
|
121
163
|
catch {
|
|
122
164
|
throw invalid("invalid_questions", "Jev requires a nonempty JSON question map; no request was sent.");
|
|
123
165
|
}
|
|
166
|
+
// Images and Box questions: validated whole before any send; the header goes with exactly these requests.
|
|
167
|
+
const hasBox = Object.values(request.questions).some(question => record(question) && question.type === "box");
|
|
168
|
+
const given = params.assets === undefined || params.assets === null ? {} : params.assets;
|
|
169
|
+
if (!record(given))
|
|
170
|
+
throw invalid("invalid_assets", "assets must map each reference to an image; no request was sent.");
|
|
171
|
+
const references = Object.keys(given);
|
|
172
|
+
const carriesImage = references.length > 0;
|
|
173
|
+
if ((carriesImage || hasBox) && provider !== "typesafe" && options.client === undefined)
|
|
174
|
+
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.");
|
|
175
|
+
if (hasBox && !answersBox(callModel))
|
|
176
|
+
throw invalid("unsupported_model", `A Box question needs a model that answers it: ${BOX_MODEL} (or jev-preview); jev-latest is 1.13. No request was sent.`);
|
|
177
|
+
if (hasBox && !carriesImage)
|
|
178
|
+
throw invalid("invalid_assets", "A Box question locates something in an image, and the request carries none. No request was sent.");
|
|
179
|
+
let assets;
|
|
180
|
+
let assetDigests;
|
|
181
|
+
if (carriesImage) {
|
|
182
|
+
// 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.
|
|
183
|
+
const texts = [...stringValues(JSON.parse(serializedState)), ...Object.values(request.questions).flatMap(question => record(question) ? [...stringValues(question.instructions), ...stringValues(question.criteria)] : [])];
|
|
184
|
+
assets = Object.create(null);
|
|
185
|
+
assetDigests = Object.create(null);
|
|
186
|
+
for (const reference of references) {
|
|
187
|
+
const image = given[reference];
|
|
188
|
+
const sound = typeof reference === "string" && reference.length > 0 && Buffer.byteLength(reference, "utf8") <= 1024 && !/["\\\u0000-\u001f]/.test(reference)
|
|
189
|
+
&& record(image) && image.bytes instanceof Uint8Array && (image.mime === "image/png" || image.mime === "image/jpeg")
|
|
190
|
+
&& image.bytes.byteLength > 0 && image.bytes.byteLength <= MAX_IMAGE_BYTES && startsWith(image.bytes, imageMagic[image.mime]);
|
|
191
|
+
if (!sound)
|
|
192
|
+
throw invalid("invalid_assets", "Each image is a PNG or JPEG file of at most 16 MiB, with bytes that are that type and a plain reference. No request was sent.");
|
|
193
|
+
if (!texts.some(text => text.includes(reference)))
|
|
194
|
+
throw invalid("invalid_assets", "Every image reference must appear in the state or a question. No request was sent.");
|
|
195
|
+
assets[reference] = new Image(image.bytes);
|
|
196
|
+
assetDigests[reference] = { sha256: createHash("sha256").update(image.bytes).digest("hex"), mime: image.mime, bytes: image.bytes.byteLength };
|
|
197
|
+
}
|
|
198
|
+
request = { ...request, assets };
|
|
199
|
+
// The hash names the images by their own digests, so it identifies the request that was sent without holding the bytes.
|
|
200
|
+
body = JSON.stringify({ ...JSON.parse(body), assets: assetDigests });
|
|
201
|
+
}
|
|
202
|
+
const headers = carriesImage || hasBox ? { [IMAGE_HEADER]: "true" } : undefined;
|
|
124
203
|
for (attempt = 1;; attempt++) {
|
|
125
204
|
if (signal.aborted)
|
|
126
205
|
throw abortError();
|
|
127
206
|
let result;
|
|
128
207
|
try {
|
|
129
208
|
result = await abortable(client.systemOne(request, {
|
|
130
|
-
signal, ...(timeoutMs === null ? {} : { timeout: Math.max(1, timeoutMs - (Date.now() - started)) }), retry: { maxRetries: 0 },
|
|
209
|
+
signal, ...(timeoutMs === null ? {} : { timeout: Math.max(1, timeoutMs - (Date.now() - started)) }), retry: { maxRetries: 0 }, ...(headers ? { headers } : {}),
|
|
131
210
|
}), signal, abortError);
|
|
132
211
|
}
|
|
133
212
|
catch (error) {
|
|
@@ -163,14 +242,20 @@ export function createJevRunner(options = {}) {
|
|
|
163
242
|
throw new JevError("invalid_response", "Jev returned invalid answers or probability distributions.", attempt, undefined, undefined, { reason: error instanceof SystemOneError && isSystemOneResponseReason(error.responseReason) ? error.responseReason : "answer_validation" });
|
|
164
243
|
}
|
|
165
244
|
const usage = result.usage;
|
|
166
|
-
|
|
245
|
+
// Usage carries token counts, a provider-reported cost (USD), or both; a half pair of counts or a bad cost is invalid.
|
|
246
|
+
const counted = record(usage) && tokens(usage.input_tokens) && tokens(usage.output_tokens);
|
|
247
|
+
const reported = record(usage) && usage.cost !== undefined && usage.cost !== null;
|
|
248
|
+
const reportedCost = reported && typeof usage.cost === "number" && Number.isFinite(usage.cost) && usage.cost >= 0 ? usage.cost : undefined;
|
|
249
|
+
const noCounts = record(usage) && usage.input_tokens === undefined && usage.output_tokens === undefined;
|
|
250
|
+
if (usage != null && (!record(usage) || (reported ? reportedCost === undefined || !(counted || noCounts) : !counted))) {
|
|
167
251
|
throw new JevError("invalid_response", "Jev returned invalid token usage.", attempt, undefined, undefined, { reason: "token_usage" });
|
|
168
252
|
}
|
|
169
253
|
if (result.model != null && (typeof result.model !== "string" || !result.model.trim())) {
|
|
170
254
|
throw new JevError("invalid_response", "Jev returned an invalid model identifier.", attempt, undefined, undefined, { reason: "model_identifier" });
|
|
171
255
|
}
|
|
172
|
-
const metering =
|
|
173
|
-
|
|
256
|
+
const metering = counted ? { input_tokens: usage.input_tokens, output_tokens: usage.output_tokens } : null;
|
|
257
|
+
// The provider's own cost wins; else the configured estimate; else unknown (null), never 0.
|
|
258
|
+
const cost = reportedCost !== undefined ? reportedCost : pricing && metering ? (metering.input_tokens * pricing.inputUsdPerMillionTokens + metering.output_tokens * pricing.outputUsdPerMillionTokens) / 1_000_000 : null;
|
|
174
259
|
if (cost !== null && !Number.isFinite(cost))
|
|
175
260
|
throw new JevError("invalid_response", "Jev cost estimate exceeded the numeric range.", attempt, undefined, undefined, { reason: "cost_range" });
|
|
176
261
|
return {
|
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.15",
|
|
4
4
|
"description": "Optional Jev system one decisions for AgentRun workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"test": "node --test test/*.test.mjs"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@parcha/agentrun-dsl": "0.1.0-beta.
|
|
31
|
-
"@typesafe-ai/sdk": "0.
|
|
30
|
+
"@parcha/agentrun-dsl": "0.1.0-beta.15",
|
|
31
|
+
"@typesafe-ai/sdk": "0.7.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|