@askalf/dario 6.9.1 → 6.9.2

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 CHANGED
@@ -473,6 +473,7 @@ The split isn't live, but it was announced once on short notice and could return
473
473
  | Credentials | Your own subscription tokens, never logged, redacted from errors, `0600` on disk in `0700` dirs |
474
474
  | Network | Binds `127.0.0.1` by default; upstream only to configured backends over HTTPS; hardcoded SSRF allow-list; refuses a non-loopback bind without `DARIO_API_KEY` |
475
475
  | Telemetry | **None.** No analytics, no tracking, nothing phones home |
476
+ | Overhead | Measured in the open on every PR: [`scripts/bench-overhead.mjs`](./scripts/bench-overhead.mjs) runs a real proxy against an instant upstream beside a bare http server serving the same bytes. On loopback dario adds no measurable p50 wall time over that floor; the CPU per request is the number to watch, and the per-request [timing split](./docs/analytics.md#the-timing-split) shows it live |
476
477
  | This README | CI fails if the line count above drifts from `src/` or a link or anchor here stops resolving ([`check-readme-line-count.mjs`](./scripts/check-readme-line-count.mjs), [`check-readme-links.mjs`](./scripts/check-readme-links.mjs)); the TUI screenshots are rendered from the real TUI and the diagrams are briefed art, not screenshots ([how](./scripts/readme/README.md)) |
477
478
 
478
479
  ```bash
package/dist/proxy.d.ts CHANGED
@@ -636,6 +636,21 @@ export interface ProxyLogEntry {
636
636
  * null (logFile not configured). Errors are swallowed — log writes
637
637
  * must never break the request path.
638
638
  */
639
+ /**
640
+ * The `data:` line of one SSE frame, without allocating a per-line array
641
+ * (the frame is `event: x\ndata: {...}\n\n`; the data line is the one that
642
+ * starts with the field name, at the start of the frame or after a newline).
643
+ * Null when the frame has no data line (a comment, a bare event).
644
+ */
645
+ export declare function sseDataLine(frame: string): string | null;
646
+ /**
647
+ * Whether the analytics tap needs to parse this frame at all: only the
648
+ * message_start usage, the message_delta usage and thinking deltas feed a
649
+ * number it keeps. A text or tool-input delta — most of any stream — is
650
+ * skipped before JSON.parse. Substring tests on the data line; a frame that
651
+ * happens to contain these words inside a text delta merely costs a parse.
652
+ */
653
+ export declare function analyticsFrameOfInterest(dataLine: string): boolean;
639
654
  export declare function writeLogLine(stream: WriteStream | null, entry: ProxyLogEntry): void;
640
655
  export declare function sanitizeError(err: unknown): string;
641
656
  /**
package/dist/proxy.js CHANGED
@@ -990,6 +990,31 @@ export function requiresClaudeLogin(poolSize, adminEnabled, hasUpstreamApiKey, n
990
990
  * null (logFile not configured). Errors are swallowed — log writes
991
991
  * must never break the request path.
992
992
  */
993
+ /**
994
+ * The `data:` line of one SSE frame, without allocating a per-line array
995
+ * (the frame is `event: x\ndata: {...}\n\n`; the data line is the one that
996
+ * starts with the field name, at the start of the frame or after a newline).
997
+ * Null when the frame has no data line (a comment, a bare event).
998
+ */
999
+ export function sseDataLine(frame) {
1000
+ let at = frame.startsWith('data: ') ? 0 : frame.indexOf('\ndata: ');
1001
+ if (at < 0)
1002
+ return null;
1003
+ if (at > 0)
1004
+ at += 1;
1005
+ const end = frame.indexOf('\n', at);
1006
+ return end < 0 ? frame.slice(at) : frame.slice(at, end);
1007
+ }
1008
+ /**
1009
+ * Whether the analytics tap needs to parse this frame at all: only the
1010
+ * message_start usage, the message_delta usage and thinking deltas feed a
1011
+ * number it keeps. A text or tool-input delta — most of any stream — is
1012
+ * skipped before JSON.parse. Substring tests on the data line; a frame that
1013
+ * happens to contain these words inside a text delta merely costs a parse.
1014
+ */
1015
+ export function analyticsFrameOfInterest(dataLine) {
1016
+ return dataLine.includes('"message_start"') || dataLine.includes('"message_delta"') || dataLine.includes('thinking_delta');
1017
+ }
993
1018
  export function writeLogLine(stream, entry) {
994
1019
  if (!stream)
995
1020
  return;
@@ -4077,7 +4102,14 @@ export async function startProxy(opts = {}) {
4077
4102
  // hand the client an OpenAI-shaped response for a Messages request. This
4078
4103
  // route still has no reverse translation; the codex route above does,
4079
4104
  // which is why it takes both shapes and this one does not.
4080
- const fallbackModel = selectPoolFallbackForBody(body)[0] ?? null;
4105
+ // Only the drained-pool branch below reads this, and computing it means
4106
+ // parsing the whole client body again — on a 200 KB Claude Code turn
4107
+ // that was ~2% of dario's own CPU on every request that never took the
4108
+ // branch (scripts/bench-overhead.mjs profile, v6.9.2). Resolved only
4109
+ // when the branch can be taken.
4110
+ const fallbackModel = (!upstreamApiKey && !poolAccount && openaiBackend && isOpenAI)
4111
+ ? (selectPoolFallbackForBody(body)[0] ?? null)
4112
+ : null;
4081
4113
  if (!upstreamApiKey && !poolAccount && fallbackModel && openaiBackend && isOpenAI) {
4082
4114
  const fallbackBody = buildPoolFallbackBody(body, fallbackModel);
4083
4115
  if (!fallbackBody) {
@@ -5287,14 +5319,22 @@ export async function startProxy(opts = {}) {
5287
5319
  upstreamDoneAt = Date.now();
5288
5320
  break;
5289
5321
  }
5290
- // Parse SSE events for analytics regardless of routing branch
5322
+ // Parse SSE events for analytics regardless of routing branch.
5323
+ // Only three frame kinds carry a number this tap reads — the
5324
+ // message_start usage, the message_delta usage, and thinking
5325
+ // deltas (for the ~4-chars-per-token estimate). Every other frame
5326
+ // is a text or tool delta, i.e. most of a stream, and parsing
5327
+ // them was the single largest cost in dario's own streaming path
5328
+ // (scripts/bench-overhead.mjs profile, v6.9.2). A substring test
5329
+ // on the data line decides before JSON.parse; the parse itself
5330
+ // is unchanged for the frames that pass.
5291
5331
  if (analyticsDecoder && value) {
5292
5332
  analyticsBuffer += analyticsDecoder.decode(value, { stream: true });
5293
5333
  const parts = analyticsBuffer.split('\n\n');
5294
5334
  analyticsBuffer = parts.pop() ?? '';
5295
5335
  for (const part of parts) {
5296
- const dataLine = part.split('\n').find(l => l.startsWith('data: '));
5297
- if (!dataLine)
5336
+ const dataLine = sseDataLine(part);
5337
+ if (!dataLine || !analyticsFrameOfInterest(dataLine))
5298
5338
  continue;
5299
5339
  try {
5300
5340
  const e = JSON.parse(dataLine.slice(6));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "6.9.1",
3
+ "version": "6.9.2",
4
4
  "description": "Use your Claude and ChatGPT subscriptions in Cursor, Cline, Aider, Claude Code and the Agent SDK — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint: either plan answers either wire shape, with automatic failover when one hits its limit.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,7 +42,8 @@
42
42
  "fix:pkg": "node -e \"const fs=require('fs');fs.writeFileSync('package.json',JSON.stringify(JSON.parse(fs.readFileSync('package.json','utf-8')),null,2)+'\\n')\"",
43
43
  "audit:tui": "node tools/tui-audit/audit.mjs",
44
44
  "readme:assets": "node scripts/readme/terminal.mjs && node scripts/readme/tui.mjs",
45
- "check:readme": "node scripts/check-readme-line-count.mjs && node scripts/check-readme-links.mjs"
45
+ "check:readme": "node scripts/check-readme-line-count.mjs && node scripts/check-readme-links.mjs",
46
+ "bench": "node scripts/bench-overhead.mjs"
46
47
  },
47
48
  "keywords": [
48
49
  "llm",