@elyracode/jev-tools 0.9.39 → 0.9.40
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 +20 -9
- package/extensions/index.ts +117 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,8 +14,15 @@ elyra install npm:@elyracode/jev-tools
|
|
|
14
14
|
|
|
15
15
|
## Requirements
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
One of two API keys:
|
|
18
|
+
|
|
19
|
+
- **TypeSafe direct** (preferred): a key from [typesafe.ai](https://typesafe.ai). Set `TYPESAFE_API_KEY`.
|
|
20
|
+
Requests go to `https://api.typesafe.ai/v1/systemone` with model `jev-latest`.
|
|
21
|
+
- **OpenRouter**: log in with `/login` and pick OpenRouter, or set `OPENROUTER_API_KEY`. Requests go to
|
|
22
|
+
OpenRouter's alpha decisions endpoint with model `~typesafe/jev-latest`.
|
|
23
|
+
|
|
24
|
+
When both are present, TypeSafe direct is used. Force one with `JEV_BACKEND=typesafe|openrouter`.
|
|
25
|
+
The request and response shape is identical on both; only endpoint, model slug, and billing differ.
|
|
19
26
|
|
|
20
27
|
## Tools
|
|
21
28
|
|
|
@@ -25,7 +32,7 @@ The extension reuses whatever key Elyra already has for the `openrouter` provide
|
|
|
25
32
|
|
|
26
33
|
## Commands
|
|
27
34
|
|
|
28
|
-
- `/jev` -- show
|
|
35
|
+
- `/jev` -- show active backend, key status, and bash gate state
|
|
29
36
|
- `/jev gate on|off` -- toggle the bash gate for the current session
|
|
30
37
|
|
|
31
38
|
## Bash gate (opt-in)
|
|
@@ -85,15 +92,19 @@ Jev reads criteria literally and does not infer intent beyond what is stated.
|
|
|
85
92
|
|
|
86
93
|
| Variable | Default | Purpose |
|
|
87
94
|
|----------|---------|---------|
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
95
|
+
| `TYPESAFE_API_KEY` | -- | TypeSafe direct API key |
|
|
96
|
+
| `OPENROUTER_API_KEY` | -- | OpenRouter API key, if not stored via `/login` |
|
|
97
|
+
| `JEV_BACKEND` | auto | Force `typesafe` or `openrouter` instead of picking by available key |
|
|
98
|
+
| `JEV_MODEL` | `jev-latest` / `~typesafe/jev-latest` | Override the model slug for the chosen backend |
|
|
99
|
+
| `JEV_BASE_URL` | per backend | Override the endpoint URL (proxies, or if OpenRouter moves out of alpha) |
|
|
91
100
|
| `JEV_BASH_GATE` | off | Set to `1` to enable the bash gate at startup |
|
|
92
101
|
| `JEV_BASH_GATE_THRESHOLD` | `0.7` | Probability (0-1) at or above which the gate intervenes |
|
|
93
102
|
|
|
94
103
|
## Notes
|
|
95
104
|
|
|
96
|
-
- The
|
|
97
|
-
|
|
98
|
-
- Retries with backoff on 429/5xx, up to three attempts. Requests time out after 60 seconds.
|
|
105
|
+
- The request contract follows the [TypeSafe API reference](https://docs.typesafe.ai/api). OpenRouter's
|
|
106
|
+
decisions endpoint is an alpha API mirroring the same shape and may change.
|
|
107
|
+
- Retries with backoff on 429/529/5xx, up to three attempts. Requests time out after 60 seconds.
|
|
108
|
+
- Question design guidance: see the [primitives docs](https://docs.typesafe.ai/primitives). A Noul near 0.5
|
|
109
|
+
means yes and no are equally likely, not "medium"; use a Score for degree.
|
|
99
110
|
- Unofficial community integration. Not affiliated with TypeSafe or OpenRouter.
|
package/extensions/index.ts
CHANGED
|
@@ -1,14 +1,50 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@elyracode/coding-agent";
|
|
2
2
|
import { type Static, Type } from "typebox";
|
|
3
3
|
|
|
4
|
-
// TypeSafe Jev is a decision model, not a chat model. It
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
// TypeSafe Jev is a decision model, not a chat model. It takes a `state` to
|
|
5
|
+
// judge plus a map of typed `questions` and returns probabilities; it never
|
|
6
|
+
// generates text. The same request shape is served by two backends:
|
|
7
|
+
// - TypeSafe direct: POST https://api.typesafe.ai/v1/systemone (model "jev-latest")
|
|
8
|
+
// - OpenRouter alpha: POST https://openrouter.ai/api/alpha/decisions (model "~typesafe/jev-latest")
|
|
9
|
+
// Direct is preferred when a TypeSafe key is available; OpenRouter is the fallback.
|
|
10
|
+
type BackendId = "typesafe" | "openrouter";
|
|
11
|
+
|
|
12
|
+
interface Backend {
|
|
13
|
+
id: BackendId;
|
|
14
|
+
label: string;
|
|
15
|
+
baseUrl: string;
|
|
16
|
+
model: string;
|
|
17
|
+
apiKey: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface BackendSpec {
|
|
21
|
+
id: BackendId;
|
|
22
|
+
label: string;
|
|
23
|
+
baseUrl: string;
|
|
24
|
+
model: string;
|
|
25
|
+
envKey: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const BACKENDS: Record<BackendId, BackendSpec> = {
|
|
29
|
+
typesafe: {
|
|
30
|
+
id: "typesafe",
|
|
31
|
+
label: "TypeSafe (direct)",
|
|
32
|
+
baseUrl: "https://api.typesafe.ai/v1/systemone",
|
|
33
|
+
model: "jev-latest",
|
|
34
|
+
envKey: "TYPESAFE_API_KEY",
|
|
35
|
+
},
|
|
36
|
+
openrouter: {
|
|
37
|
+
id: "openrouter",
|
|
38
|
+
label: "OpenRouter",
|
|
39
|
+
baseUrl: "https://openrouter.ai/api/alpha/decisions",
|
|
40
|
+
model: "~typesafe/jev-latest",
|
|
41
|
+
envKey: "OPENROUTER_API_KEY",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const ENV_BACKEND = "JEV_BACKEND";
|
|
9
46
|
const ENV_BASE_URL = "JEV_BASE_URL";
|
|
10
47
|
const ENV_MODEL = "JEV_MODEL";
|
|
11
|
-
const PROVIDER = "openrouter";
|
|
12
48
|
const MAX_ATTEMPTS = 3;
|
|
13
49
|
const REQUEST_TIMEOUT_MS = 60_000;
|
|
14
50
|
|
|
@@ -153,11 +189,11 @@ export default function (elyra: ExtensionAPI): void {
|
|
|
153
189
|
execute: async (_toolCallId, params, signal, _onUpdate, ctx) => {
|
|
154
190
|
validateQuestions(params.questions);
|
|
155
191
|
|
|
156
|
-
const
|
|
192
|
+
const backend = await requireBackend(ctx);
|
|
157
193
|
const state = parseState(params.state);
|
|
158
194
|
const questions = toApiQuestions(params.questions);
|
|
159
195
|
const warning = sizeWarning(state, questions);
|
|
160
|
-
const response = await requestDecision(
|
|
196
|
+
const response = await requestDecision(backend, state, questions, signal);
|
|
161
197
|
const text = formatResponse(params.questions, response);
|
|
162
198
|
|
|
163
199
|
return {
|
|
@@ -188,16 +224,16 @@ export default function (elyra: ExtensionAPI): void {
|
|
|
188
224
|
const command = event.input.command;
|
|
189
225
|
if (typeof command !== "string" || !command.trim()) return undefined;
|
|
190
226
|
|
|
191
|
-
const
|
|
192
|
-
if (!
|
|
193
|
-
ctx.ui.notify("Jev bash gate: no OpenRouter API key, skipping check", "warning");
|
|
227
|
+
const backend = await resolveBackend(ctx);
|
|
228
|
+
if (!backend) {
|
|
229
|
+
ctx.ui.notify("Jev bash gate: no TypeSafe or OpenRouter API key, skipping check", "warning");
|
|
194
230
|
return undefined;
|
|
195
231
|
}
|
|
196
232
|
|
|
197
233
|
let probability: number;
|
|
198
234
|
try {
|
|
199
235
|
const response = await requestDecision(
|
|
200
|
-
|
|
236
|
+
backend,
|
|
201
237
|
{ command, cwd: ctx.cwd },
|
|
202
238
|
{ [GATE_QUESTION_NAME]: GATE_QUESTION },
|
|
203
239
|
ctx.signal,
|
|
@@ -246,14 +282,17 @@ export default function (elyra: ExtensionAPI): void {
|
|
|
246
282
|
return;
|
|
247
283
|
}
|
|
248
284
|
|
|
249
|
-
const
|
|
285
|
+
const typesafeKey = await resolveApiKey(ctx, "typesafe");
|
|
286
|
+
const openrouterKey = await resolveApiKey(ctx, "openrouter");
|
|
287
|
+
const backend = await resolveBackend(ctx);
|
|
250
288
|
const lines = [
|
|
251
|
-
`
|
|
252
|
-
`
|
|
253
|
-
`OpenRouter key: ${
|
|
289
|
+
`Active backend: ${backend ? `${backend.label} (${backend.model} at ${backend.baseUrl})` : "none"}`,
|
|
290
|
+
`TypeSafe key: ${typesafeKey ? "configured" : "missing (set TYPESAFE_API_KEY)"}`,
|
|
291
|
+
`OpenRouter key: ${openrouterKey ? "configured" : "missing (run /login or set OPENROUTER_API_KEY)"}`,
|
|
292
|
+
`Backend override: ${process.env[ENV_BACKEND] || "auto (TypeSafe if keyed, else OpenRouter)"}`,
|
|
254
293
|
`Bash gate: ${gateEnabled ? "on" : "off"} (threshold ${gateThreshold}, toggle with /jev gate on|off)`,
|
|
255
294
|
];
|
|
256
|
-
ctx.ui.notify(lines.join("\n"),
|
|
295
|
+
ctx.ui.notify(lines.join("\n"), backend ? "info" : "warning");
|
|
257
296
|
},
|
|
258
297
|
});
|
|
259
298
|
}
|
|
@@ -272,12 +311,56 @@ function parseThreshold(value: string | undefined): number {
|
|
|
272
311
|
return parsed;
|
|
273
312
|
}
|
|
274
313
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
314
|
+
// ── Backend resolution ──
|
|
315
|
+
|
|
316
|
+
function parseBackendId(value: string | undefined): BackendId | undefined {
|
|
317
|
+
const normalized = value?.trim().toLowerCase();
|
|
318
|
+
return normalized === "typesafe" || normalized === "openrouter" ? normalized : undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Env var first, then Elyra's auth storage (/login or auth.json entry under the backend id). */
|
|
322
|
+
async function resolveApiKey(ctx: ExtensionContext, id: BackendId): Promise<string | undefined> {
|
|
323
|
+
const fromEnv = process.env[BACKENDS[id].envKey]?.trim();
|
|
324
|
+
if (fromEnv) return fromEnv;
|
|
325
|
+
return ctx.modelRegistry.getApiKeyForProvider(id);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Pick the backend. JEV_BACKEND forces one; otherwise TypeSafe direct is used when
|
|
330
|
+
* a key exists, falling back to OpenRouter. JEV_BASE_URL / JEV_MODEL override the
|
|
331
|
+
* chosen backend's endpoint and model slug.
|
|
332
|
+
*/
|
|
333
|
+
async function resolveBackend(ctx: ExtensionContext): Promise<Backend | undefined> {
|
|
334
|
+
const forced = parseBackendId(process.env[ENV_BACKEND]);
|
|
335
|
+
const order: BackendId[] = forced ? [forced] : ["typesafe", "openrouter"];
|
|
336
|
+
|
|
337
|
+
for (const id of order) {
|
|
338
|
+
const apiKey = await resolveApiKey(ctx, id);
|
|
339
|
+
if (!apiKey) continue;
|
|
340
|
+
const spec = BACKENDS[id];
|
|
341
|
+
return {
|
|
342
|
+
id,
|
|
343
|
+
label: spec.label,
|
|
344
|
+
apiKey,
|
|
345
|
+
baseUrl: process.env[ENV_BASE_URL] || spec.baseUrl,
|
|
346
|
+
model: process.env[ENV_MODEL] || spec.model,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function requireBackend(ctx: ExtensionContext): Promise<Backend> {
|
|
353
|
+
const backend = await resolveBackend(ctx);
|
|
354
|
+
if (!backend) {
|
|
355
|
+
const forced = parseBackendId(process.env[ENV_BACKEND]);
|
|
356
|
+
if (forced) {
|
|
357
|
+
throw new Error(`JEV_BACKEND=${forced} but no ${BACKENDS[forced].envKey} is set.`);
|
|
358
|
+
}
|
|
359
|
+
throw new Error(
|
|
360
|
+
"No Jev API key found. Set TYPESAFE_API_KEY for direct access, or run /login and pick OpenRouter (or set OPENROUTER_API_KEY).",
|
|
361
|
+
);
|
|
279
362
|
}
|
|
280
|
-
return
|
|
363
|
+
return backend;
|
|
281
364
|
}
|
|
282
365
|
|
|
283
366
|
// ── Validation ──
|
|
@@ -366,27 +449,23 @@ function sizeWarning(state: unknown, questions: Record<string, ApiQuestion>): st
|
|
|
366
449
|
|
|
367
450
|
// ── HTTP ──
|
|
368
451
|
|
|
369
|
-
function requestDecision(
|
|
370
|
-
|
|
452
|
+
async function requestDecision(
|
|
453
|
+
backend: Backend,
|
|
371
454
|
state: unknown,
|
|
372
455
|
questions: Record<string, ApiQuestion>,
|
|
373
456
|
signal: AbortSignal | undefined,
|
|
374
457
|
): Promise<DecisionsResponse> {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
apiKey: string,
|
|
380
|
-
payload: Record<string, unknown>,
|
|
381
|
-
signal: AbortSignal | undefined,
|
|
382
|
-
): Promise<DecisionsResponse> {
|
|
383
|
-
const url = process.env[ENV_BASE_URL] || DEFAULT_BASE_URL;
|
|
384
|
-
const headers = {
|
|
385
|
-
Authorization: `Bearer ${apiKey}`,
|
|
458
|
+
const payload = { model: backend.model, state, questions };
|
|
459
|
+
const url = backend.baseUrl;
|
|
460
|
+
const headers: Record<string, string> = {
|
|
461
|
+
Authorization: `Bearer ${backend.apiKey}`,
|
|
386
462
|
"Content-Type": "application/json",
|
|
387
|
-
"X-Title": "elyra-jev-tools",
|
|
388
|
-
"HTTP-Referer": "https://github.com/kwhorne/elyra",
|
|
389
463
|
};
|
|
464
|
+
if (backend.id === "openrouter") {
|
|
465
|
+
// OpenRouter attribution headers; not part of the TypeSafe API.
|
|
466
|
+
headers["X-Title"] = "elyra-jev-tools";
|
|
467
|
+
headers["HTTP-Referer"] = "https://github.com/kwhorne/elyra";
|
|
468
|
+
}
|
|
390
469
|
|
|
391
470
|
for (let attempt = 1; ; attempt++) {
|
|
392
471
|
signal?.throwIfAborted();
|
|
@@ -485,7 +564,7 @@ function formatResponse(questions: QuestionType[], response: DecisionsResponse):
|
|
|
485
564
|
if (usage) {
|
|
486
565
|
const tokens = (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
|
|
487
566
|
const cost = usage.cost !== undefined ? ` · $${usage.cost.toFixed(6)}` : "";
|
|
488
|
-
lines.push("", `[${response.model ??
|
|
567
|
+
lines.push("", `[${response.model ?? "jev"} · ${tokens} tok${cost}]`);
|
|
489
568
|
}
|
|
490
569
|
return lines.join("\n");
|
|
491
570
|
}
|
package/package.json
CHANGED