jev-gateway 0.1.0 → 0.2.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/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { TypeSafeClient } from "@typesafe-ai/sdk";
3
3
  import { createApp } from "./app.js";
4
4
  import { loadConfig } from "./config.js";
5
5
  import { createDump } from "./debug.js";
6
+ import { createEventLog } from "./events.js";
6
7
  const config = loadConfig();
7
8
  // Reads TYPESAFE_API_KEY. One fast retry only: past that, failing open to the LLM is quicker.
8
9
  const jev = new TypeSafeClient({
@@ -14,8 +15,10 @@ const app = createApp({
14
15
  config,
15
16
  askJev: (request) => jev.systemOne(request),
16
17
  dump: createDump(config.debugDumpDir),
18
+ events: createEventLog({ historyFile: config.logFile }),
17
19
  log: (entry) => console.log(JSON.stringify({ time: new Date().toISOString(), ...entry })),
18
20
  });
19
- serve({ fetch: app.fetch, port: config.port }, ({ port }) => {
21
+ serve({ fetch: app.fetch, hostname: config.host, port: config.port }, ({ port }) => {
20
22
  console.log(`jev-gateway listening on http://localhost:${port} → ${config.upstreamBaseUrl} (jev: ${config.jevModel})`);
23
+ console.log(`dashboard: http://localhost:${port}/dashboard`);
21
24
  });
package/dist/usage.js ADDED
@@ -0,0 +1,86 @@
1
+ const num = (value) => (typeof value === "number" && Number.isFinite(value) ? value : 0);
2
+ const obj = (value) => (value && typeof value === "object" ? value : {});
3
+ /** Fold one provider `usage` object into the running total; later reports override earlier ones. */
4
+ function merge(into, raw) {
5
+ if ("prompt_tokens" in raw || "completion_tokens" in raw) {
6
+ // Chat Completions
7
+ into.input = num(raw.prompt_tokens);
8
+ into.output = num(raw.completion_tokens);
9
+ into.cached = num(obj(raw.prompt_tokens_details).cached_tokens);
10
+ into.reasoning = num(obj(raw.completion_tokens_details).reasoning_tokens);
11
+ }
12
+ else if ("cache_read_input_tokens" in raw || "cache_creation_input_tokens" in raw) {
13
+ // Anthropic, message_start: the input side, with a placeholder output count.
14
+ into.cached = num(raw.cache_read_input_tokens);
15
+ into.cacheWrite = num(raw.cache_creation_input_tokens);
16
+ into.input = num(raw.input_tokens) + into.cached + into.cacheWrite;
17
+ into.output = num(raw.output_tokens);
18
+ }
19
+ else {
20
+ // Responses API — or Anthropic's message_delta, which only updates the output count.
21
+ if ("input_tokens" in raw) {
22
+ into.input = num(raw.input_tokens);
23
+ into.cached = num(obj(raw.input_tokens_details).cached_tokens);
24
+ }
25
+ if ("output_tokens" in raw) {
26
+ into.output = num(raw.output_tokens);
27
+ into.reasoning = num(obj(raw.output_tokens_details).reasoning_tokens);
28
+ }
29
+ }
30
+ }
31
+ function collect(payload, into) {
32
+ const root = obj(payload);
33
+ // Where each API keeps it: top level (JSON replies, chat chunks, message_delta),
34
+ // `response.usage` (Responses events), `message.usage` (Anthropic message_start).
35
+ for (const holder of [root, obj(root.response), obj(root.message)]) {
36
+ if (holder.usage && typeof holder.usage === "object")
37
+ merge(into, holder.usage);
38
+ }
39
+ }
40
+ /**
41
+ * Read a reply to its end and report what it cost. Works on a clone, in the background: the
42
+ * client's own stream is never delayed. A stream cut short (Codex hangs up as soon as it has
43
+ * `response.completed`) still yields whatever usage arrived before the cut.
44
+ */
45
+ export async function readUsage(response) {
46
+ const found = {};
47
+ const streaming = response.headers.get("content-type")?.includes("text/event-stream") ?? false;
48
+ let pending = "";
49
+ const scan = (line) => {
50
+ // Most stream events are text deltas; only the few that mention usage are worth parsing.
51
+ if (!line.startsWith("data:") || !line.includes('"usage"'))
52
+ return;
53
+ try {
54
+ collect(JSON.parse(line.slice(5)), found);
55
+ }
56
+ catch {
57
+ // A line cut short by the client hanging up.
58
+ }
59
+ };
60
+ try {
61
+ for await (const chunk of response.body?.pipeThrough(new TextDecoderStream()) ?? []) {
62
+ pending += chunk;
63
+ if (!streaming)
64
+ continue;
65
+ const lines = pending.split("\n");
66
+ pending = lines.pop() ?? "";
67
+ lines.forEach(scan);
68
+ }
69
+ }
70
+ catch {
71
+ // Aborted mid-stream: keep what was seen.
72
+ }
73
+ if (streaming)
74
+ scan(pending);
75
+ else {
76
+ try {
77
+ collect(JSON.parse(pending), found);
78
+ }
79
+ catch {
80
+ // Not JSON (an HTML error page, an empty body): nothing to report.
81
+ }
82
+ }
83
+ if (found.input === undefined && found.output === undefined)
84
+ return undefined;
85
+ return { input: 0, output: 0, cached: 0, cacheWrite: 0, reasoning: 0, ...found };
86
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "jev-gateway",
3
- "version": "0.1.0",
4
- "description": "LLM gateway that hands tool-selection decisions to TypeSafe's Jev model \u2014 with launchers for Codex and Claude Code",
3
+ "version": "0.2.0",
4
+ "description": "Local LLM gateway that lets TypeSafe's Jev model pick the tool for coding agents. Launchers for Codex and Claude Code, with a monitoring dashboard.",
5
5
  "keywords": [
6
6
  "llm",
7
7
  "gateway",
@@ -16,9 +16,17 @@
16
16
  ],
17
17
  "license": "MIT",
18
18
  "author": "Vinicius Lana",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/vinilana/jev-gateway.git"
22
+ },
23
+ "homepage": "https://github.com/vinilana/jev-gateway#readme",
24
+ "bugs": {
25
+ "url": "https://github.com/vinilana/jev-gateway/issues"
26
+ },
19
27
  "type": "module",
20
28
  "engines": {
21
- "node": ">=22.9"
29
+ "node": ">=22.15"
22
30
  },
23
31
  "bin": {
24
32
  "jev-codex": "bin/jev-codex.mjs",
@@ -28,12 +36,13 @@
28
36
  "dist",
29
37
  "bin",
30
38
  "scripts/mock-jev.mjs",
31
- ".env.example"
39
+ ".env.example",
40
+ "CHANGELOG.md"
32
41
  ],
33
42
  "scripts": {
34
43
  "dev": "tsx watch --env-file-if-exists=.env src/index.ts",
35
44
  "start": "node --env-file-if-exists=.env dist/index.js",
36
- "build": "rm -rf dist && tsc -p tsconfig.build.json",
45
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && cp src/dashboard.html dist/",
37
46
  "typecheck": "tsc --noEmit",
38
47
  "test": "vitest run",
39
48
  "codex": "node bin/jev-codex.mjs",