@askalf/dario 3.31.20 → 3.32.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/README.md +8 -2
- package/dist/cc-oauth-detect.js +10 -0
- package/dist/cc-template.d.ts +25 -0
- package/dist/cc-template.js +96 -8
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +269 -3
- package/dist/doctor.js +69 -0
- package/dist/live-fingerprint.d.ts +1 -1
- package/dist/live-fingerprint.js +1 -1
- package/dist/mcp/tools.d.ts +28 -0
- package/dist/mcp/tools.js +104 -0
- package/dist/proxy.d.ts +67 -0
- package/dist/proxy.js +191 -10
- package/dist/runtime-fingerprint.d.ts +26 -0
- package/dist/runtime-fingerprint.js +43 -0
- package/package.json +1 -1
package/dist/proxy.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import { randomUUID, randomBytes, timingSafeEqual, createHash } from 'node:crypto';
|
|
3
3
|
import { execSync } from 'node:child_process';
|
|
4
|
-
import { readFileSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { readFileSync, readdirSync, createWriteStream } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { arch, platform } from 'node:process';
|
|
@@ -345,6 +345,21 @@ function translateStreamChunk(line) {
|
|
|
345
345
|
return null;
|
|
346
346
|
}
|
|
347
347
|
const OPENAI_MODELS_LIST = { object: 'list', data: ['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-haiku-4-5'].map(id => ({ id, object: 'model', created: 1700000000, owned_by: 'anthropic' })) };
|
|
348
|
+
/**
|
|
349
|
+
* Append a JSON-ND line to the proxy log file. No-op when stream is
|
|
350
|
+
* null (logFile not configured). Errors are swallowed — log writes
|
|
351
|
+
* must never break the request path.
|
|
352
|
+
*/
|
|
353
|
+
export function writeLogLine(stream, entry) {
|
|
354
|
+
if (!stream)
|
|
355
|
+
return;
|
|
356
|
+
try {
|
|
357
|
+
stream.write(redactSecrets(JSON.stringify(entry)) + '\n');
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// ignore — log mishaps must never affect requests
|
|
361
|
+
}
|
|
362
|
+
}
|
|
348
363
|
export function sanitizeError(err) {
|
|
349
364
|
// Pattern set lives in src/redact.ts so OAuth call sites can run the
|
|
350
365
|
// same redaction directly on response-body strings without importing
|
|
@@ -440,12 +455,74 @@ export async function startProxy(opts = {}) {
|
|
|
440
455
|
// Set once on first sighting per family so the startup log stays
|
|
441
456
|
// short even under heavy traffic. dario#40.
|
|
442
457
|
const detectedClientsLogged = new Set();
|
|
458
|
+
// Per-(client, mapping-mode) keys for which we've already emitted a
|
|
459
|
+
// tool-substitution warn line. Same de-dup contract as
|
|
460
|
+
// detectedClientsLogged so mixed-traffic proxies don't spam.
|
|
461
|
+
const toolSubLogged = new Set();
|
|
443
462
|
// Body-dump mode: set via --verbose=2 / -vv or DARIO_LOG_BODIES=1.
|
|
444
463
|
// When on, every request emits a redacted JSON body to stderr so
|
|
445
464
|
// operators can see exactly what dario forwards upstream. Default
|
|
446
465
|
// -v stays quiet because bodies can carry file content and tool
|
|
447
466
|
// output. Reported in dario#40 by @ringge.
|
|
448
467
|
const verboseBodies = Boolean(opts.verboseBodies) || process.env.DARIO_LOG_BODIES === '1';
|
|
468
|
+
// Operator-declared beta passthrough set. Sourced from CLI flag or env;
|
|
469
|
+
// both are CSV strings of beta-flag names. Trimmed, deduped, empty
|
|
470
|
+
// entries dropped. Stays a Set for fast membership checks in the per-
|
|
471
|
+
// request beta build below.
|
|
472
|
+
const passthroughBetas = new Set([
|
|
473
|
+
...(opts.passthroughBetas ?? []),
|
|
474
|
+
...((process.env.DARIO_PASSTHROUGH_BETAS ?? '').split(',')),
|
|
475
|
+
]
|
|
476
|
+
.map((b) => b.trim())
|
|
477
|
+
.filter((b) => b.length > 0));
|
|
478
|
+
if (passthroughBetas.size > 0) {
|
|
479
|
+
console.log(` Beta passthrough: ${[...passthroughBetas].sort().join(', ')} (always forwarded; per-account rejection cache still applies)`);
|
|
480
|
+
}
|
|
481
|
+
// Tool-routing mode mutex. preserve / hybrid / merge each shape the
|
|
482
|
+
// outbound `tools` array differently; combining two would mean two
|
|
483
|
+
// different bodies. Refuse to start with a clear error rather than
|
|
484
|
+
// silently dropping a flag.
|
|
485
|
+
const toolModes = [
|
|
486
|
+
opts.preserveTools ? 'preserve-tools' : null,
|
|
487
|
+
opts.hybridTools ? 'hybrid-tools' : null,
|
|
488
|
+
opts.mergeTools ? 'merge-tools' : null,
|
|
489
|
+
].filter((m) => m !== null);
|
|
490
|
+
if (toolModes.length > 1) {
|
|
491
|
+
console.error(`[dario] tool-routing flags are mutually exclusive — pick one: ${toolModes.join(', ')}.`);
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
if (opts.mergeTools) {
|
|
495
|
+
// Loud notice — this mode is experimental and operators need to
|
|
496
|
+
// verify their billing classification before relying on it. The
|
|
497
|
+
// wire-shape "tools[]" axis still has CC's array as a prefix, but
|
|
498
|
+
// the suffix is operator-supplied custom shapes. Anthropic's
|
|
499
|
+
// classifier may flip routing on the difference.
|
|
500
|
+
console.log(' Tool routing: merge (CC tools + client custom tools, deduped)');
|
|
501
|
+
console.log(' ⚠ EXPERIMENTAL: validate billing-bucket behavior on the first 1-2 requests with --verbose');
|
|
502
|
+
}
|
|
503
|
+
// Append-only structured request log. One JSON-ND line per completed
|
|
504
|
+
// request — secrets scrubbed via redactSecrets, no bodies. Off by
|
|
505
|
+
// default; opt in with `--log-file <path>` or DARIO_LOG_FILE. See the
|
|
506
|
+
// ProxyLogEntry interface for fields. Useful for backgrounded proxies
|
|
507
|
+
// where stdout is unobserved (`verbose` only helps in foreground).
|
|
508
|
+
// Errors during open are reported once and downgrade to no-op so a
|
|
509
|
+
// log mishap never blocks the proxy from booting.
|
|
510
|
+
const logFilePath = opts.logFile || process.env.DARIO_LOG_FILE || null;
|
|
511
|
+
let logFileStream = null;
|
|
512
|
+
if (logFilePath) {
|
|
513
|
+
try {
|
|
514
|
+
logFileStream = createWriteStream(logFilePath, { flags: 'a' });
|
|
515
|
+
logFileStream.on('error', (err) => {
|
|
516
|
+
console.error(`[dario] log-file write error: ${err.message} (logging disabled)`);
|
|
517
|
+
logFileStream = null;
|
|
518
|
+
});
|
|
519
|
+
console.log(` Request log: ${logFilePath}`);
|
|
520
|
+
}
|
|
521
|
+
catch (err) {
|
|
522
|
+
console.error(`[dario] log-file open failed: ${err instanceof Error ? err.message : err} (continuing without)`);
|
|
523
|
+
logFileStream = null;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
449
526
|
// Multi-provider backends (v3.6.0+). Loaded once at startup; the CLI
|
|
450
527
|
// `dario backend add openai --key=…` writes to ~/.dario/backends/.
|
|
451
528
|
// Routing: a GPT-family model arriving on /v1/chat/completions is
|
|
@@ -707,6 +784,10 @@ export async function startProxy(opts = {}) {
|
|
|
707
784
|
// one-line reject log under -v so operators see auth misfires.
|
|
708
785
|
console.error(`[dario] #${requestCount} 401 rejected (DARIO_API_KEY mismatch): ${describeAuthReject(req.headers)}`);
|
|
709
786
|
}
|
|
787
|
+
writeLogLine(logFileStream, {
|
|
788
|
+
ts: new Date().toISOString(), req: requestCount,
|
|
789
|
+
method: req.method ?? '', path: urlPath, status: 401, reject: 'auth',
|
|
790
|
+
});
|
|
710
791
|
res.writeHead(401, JSON_HEADERS);
|
|
711
792
|
res.end(ERR_UNAUTH);
|
|
712
793
|
return;
|
|
@@ -790,6 +871,10 @@ export async function startProxy(opts = {}) {
|
|
|
790
871
|
}
|
|
791
872
|
catch (err) {
|
|
792
873
|
if (err instanceof QueueFullError) {
|
|
874
|
+
writeLogLine(logFileStream, {
|
|
875
|
+
ts: new Date().toISOString(), req: requestCount,
|
|
876
|
+
method: req.method ?? '', path: urlPath, status: 429, reject: 'queue-full',
|
|
877
|
+
});
|
|
793
878
|
res.writeHead(429, JSON_HEADERS);
|
|
794
879
|
res.end(JSON.stringify({
|
|
795
880
|
type: 'error',
|
|
@@ -801,6 +886,10 @@ export async function startProxy(opts = {}) {
|
|
|
801
886
|
return;
|
|
802
887
|
}
|
|
803
888
|
if (err instanceof QueueTimeoutError) {
|
|
889
|
+
writeLogLine(logFileStream, {
|
|
890
|
+
ts: new Date().toISOString(), req: requestCount,
|
|
891
|
+
method: req.method ?? '', path: urlPath, status: 504, reject: 'queue-timeout',
|
|
892
|
+
});
|
|
804
893
|
res.writeHead(504, JSON_HEADERS);
|
|
805
894
|
res.end(JSON.stringify({
|
|
806
895
|
type: 'error',
|
|
@@ -817,6 +906,13 @@ export async function startProxy(opts = {}) {
|
|
|
817
906
|
let upstreamTimeout = null;
|
|
818
907
|
let onClientClose = null;
|
|
819
908
|
let upstreamAbortReason = null;
|
|
909
|
+
// Hoisted so the catch can include them in the request log line. The
|
|
910
|
+
// body-parsing block below assigns these once the request is parsed;
|
|
911
|
+
// before that point they remain at their initial values, which is
|
|
912
|
+
// also exactly what we want to log on early-failure paths.
|
|
913
|
+
let requestModel = '';
|
|
914
|
+
let detectedClientForLog;
|
|
915
|
+
let preserveToolsEffective = Boolean(opts.preserveTools);
|
|
820
916
|
try {
|
|
821
917
|
// Pool mode: select an account by headroom. Single-account mode:
|
|
822
918
|
// fall through to getAccessToken() exactly as before. Request-path
|
|
@@ -912,7 +1008,9 @@ export async function startProxy(opts = {}) {
|
|
|
912
1008
|
// Parse body once, apply OpenAI translation, model override, and sanitization
|
|
913
1009
|
let finalBody = body.length > 0 ? body : undefined;
|
|
914
1010
|
let ccToolMap = null;
|
|
915
|
-
|
|
1011
|
+
// requestModel / detectedClientForLog / preserveToolsEffective are
|
|
1012
|
+
// declared at the outer try-scope above so the catch block can
|
|
1013
|
+
// include them in the request log line.
|
|
916
1014
|
// Session stickiness key — hash of the first user message in this
|
|
917
1015
|
// conversation. Populated inside the template-replay block below
|
|
918
1016
|
// after the first user message is extracted for the build tag, then
|
|
@@ -1000,13 +1098,17 @@ export async function startProxy(opts = {}) {
|
|
|
1000
1098
|
const bodyIdentity = poolAccount
|
|
1001
1099
|
? poolAccount.identity
|
|
1002
1100
|
: { deviceId: identity.deviceId, accountUuid: identity.accountUuid, sessionId: preBodySessionId };
|
|
1003
|
-
const { body: ccBody, toolMap, detectedClient } = buildCCRequest(r, billingTag, CACHE_EPHEMERAL, bodyIdentity, {
|
|
1101
|
+
const { body: ccBody, toolMap, detectedClient, unmappedTools } = buildCCRequest(r, billingTag, CACHE_EPHEMERAL, bodyIdentity, {
|
|
1004
1102
|
preserveTools: opts.preserveTools ?? false,
|
|
1005
1103
|
hybridTools: opts.hybridTools ?? false,
|
|
1104
|
+
mergeTools: opts.mergeTools ?? false,
|
|
1006
1105
|
noAutoDetect: opts.noAutoDetect ?? false,
|
|
1007
1106
|
effort: opts.effort,
|
|
1008
1107
|
maxTokens: opts.maxTokens,
|
|
1009
1108
|
});
|
|
1109
|
+
detectedClientForLog = detectedClient;
|
|
1110
|
+
preserveToolsEffective = Boolean(opts.preserveTools)
|
|
1111
|
+
|| (Boolean(detectedClient) && !opts.hybridTools && !opts.mergeTools);
|
|
1010
1112
|
// Log the auto-preserve-tools switch once per text-tool
|
|
1011
1113
|
// client family. Skip when the operator already opted into
|
|
1012
1114
|
// --preserve-tools or --hybrid-tools — they know what they
|
|
@@ -1019,6 +1121,28 @@ export async function startProxy(opts = {}) {
|
|
|
1019
1121
|
detectedClientsLogged.add(detectedClient);
|
|
1020
1122
|
console.log(`[dario] detected ${detectedClient}-style text-tool protocol — auto-enabling preserve-tools for this client (pass --hybrid-tools to override, --preserve-tools to silence)`);
|
|
1021
1123
|
}
|
|
1124
|
+
// Surface tool substitution. When a non-CC client routes
|
|
1125
|
+
// tools we don't have in TOOL_MAP and neither auto-detect
|
|
1126
|
+
// nor an explicit flag flipped us into preserve-tools, the
|
|
1127
|
+
// unmapped tools get distributed onto CC fallback slots —
|
|
1128
|
+
// the model upstream sees "Glob"/"Read"/etc. and the
|
|
1129
|
+
// client's own tool surface is silently rewritten on the
|
|
1130
|
+
// response path. That rewrite is correct for
|
|
1131
|
+
// schema-compatible cases but invisible to operators who
|
|
1132
|
+
// didn't expect it. One-line warn the first time we see a
|
|
1133
|
+
// non-empty unmapped set per (client family, mapping mode)
|
|
1134
|
+
// — same de-dupe key shape as the auto-detect line so a
|
|
1135
|
+
// mixed-traffic proxy doesn't spam.
|
|
1136
|
+
const subKey = `${detectedClient ?? 'unknown'}:${preserveToolsEffective ? 'preserve' : 'remap'}`;
|
|
1137
|
+
if (unmappedTools.length > 0
|
|
1138
|
+
&& !preserveToolsEffective
|
|
1139
|
+
&& !toolSubLogged.has(subKey)) {
|
|
1140
|
+
toolSubLogged.add(subKey);
|
|
1141
|
+
const totalTools = (Array.isArray(r.tools) ? r.tools.length : 0);
|
|
1142
|
+
const sample = unmappedTools.slice(0, 5).join(', ');
|
|
1143
|
+
const more = unmappedTools.length > 5 ? `, +${unmappedTools.length - 5} more` : '';
|
|
1144
|
+
console.log(`[dario] tool substitution: ${unmappedTools.length}/${totalTools} client tool${unmappedTools.length === 1 ? '' : 's'} not in TOOL_MAP — remapped onto CC fallback slots (${sample}${more}). Pass --preserve-tools to forward your schemas verbatim instead.`);
|
|
1145
|
+
}
|
|
1022
1146
|
// Store tool map for response reverse-mapping
|
|
1023
1147
|
ccToolMap = toolMap;
|
|
1024
1148
|
// Replace request body entirely with CC template
|
|
@@ -1074,6 +1198,19 @@ export async function startProxy(opts = {}) {
|
|
|
1074
1198
|
if (filtered)
|
|
1075
1199
|
beta += ',' + filtered;
|
|
1076
1200
|
}
|
|
1201
|
+
// Operator-pinned passthrough betas. Always forwarded — bypasses
|
|
1202
|
+
// the billable-beta filter, bypasses the "not in client's
|
|
1203
|
+
// request" gate. The per-account rejection cache below still
|
|
1204
|
+
// applies, so a pinned flag the upstream 400's gets dropped on
|
|
1205
|
+
// the retry rather than re-sent forever (the cache survives the
|
|
1206
|
+
// operator's pin because the upstream's no is final until a
|
|
1207
|
+
// tier change resets it).
|
|
1208
|
+
if (passthroughBetas.size > 0) {
|
|
1209
|
+
const baseSet = new Set(beta.split(','));
|
|
1210
|
+
const toAdd = [...passthroughBetas].filter((b) => !baseSet.has(b));
|
|
1211
|
+
if (toAdd.length > 0)
|
|
1212
|
+
beta += ',' + toAdd.join(',');
|
|
1213
|
+
}
|
|
1077
1214
|
// Strip any beta flags the upstream has previously rejected on this
|
|
1078
1215
|
// account so we don't re-pay the 400 round-trip (dario#42 afk-mode
|
|
1079
1216
|
// fallout: captured templates carry tier-gated flags whose availability
|
|
@@ -1596,6 +1733,20 @@ export async function startProxy(opts = {}) {
|
|
|
1596
1733
|
latencyMs: Date.now() - startTime, status: upstream.status, isStream: true, isOpenAI,
|
|
1597
1734
|
});
|
|
1598
1735
|
}
|
|
1736
|
+
writeLogLine(logFileStream, {
|
|
1737
|
+
ts: new Date().toISOString(), req: requestCount,
|
|
1738
|
+
method: req.method ?? '', path: urlPath,
|
|
1739
|
+
model: requestModel || undefined,
|
|
1740
|
+
status: upstream.status, latency_ms: Date.now() - startTime,
|
|
1741
|
+
in_tokens: streamInputTokens, out_tokens: streamOutputTokens,
|
|
1742
|
+
cache_read: streamCacheReadTokens, cache_create: streamCacheCreateTokens,
|
|
1743
|
+
claim: poolAccount?.rateLimit.claim,
|
|
1744
|
+
bucket: poolAccount ? billingBucketFromClaim(poolAccount.rateLimit.claim) : undefined,
|
|
1745
|
+
account: poolAccount?.alias,
|
|
1746
|
+
client: detectedClientForLog,
|
|
1747
|
+
preserve_tools: preserveToolsEffective,
|
|
1748
|
+
stream: true,
|
|
1749
|
+
});
|
|
1599
1750
|
}
|
|
1600
1751
|
else {
|
|
1601
1752
|
// Buffer and forward
|
|
@@ -1615,16 +1766,20 @@ export async function startProxy(opts = {}) {
|
|
|
1615
1766
|
else {
|
|
1616
1767
|
res.end(responseBody);
|
|
1617
1768
|
}
|
|
1618
|
-
|
|
1769
|
+
let bufferedUsage = null;
|
|
1770
|
+
try {
|
|
1771
|
+
const parsed = JSON.parse(responseBody);
|
|
1772
|
+
bufferedUsage = Analytics.parseUsage(parsed);
|
|
1773
|
+
}
|
|
1774
|
+
catch { /* malformed body — log without usage */ }
|
|
1775
|
+
if (analytics && poolAccount && bufferedUsage) {
|
|
1619
1776
|
try {
|
|
1620
|
-
const parsed = JSON.parse(responseBody);
|
|
1621
|
-
const usage = Analytics.parseUsage(parsed);
|
|
1622
1777
|
analytics.record({
|
|
1623
1778
|
timestamp: Date.now(), account: poolAccount.alias,
|
|
1624
|
-
model:
|
|
1625
|
-
inputTokens:
|
|
1626
|
-
cacheReadTokens:
|
|
1627
|
-
thinkingTokens:
|
|
1779
|
+
model: bufferedUsage.model || requestModel,
|
|
1780
|
+
inputTokens: bufferedUsage.inputTokens, outputTokens: bufferedUsage.outputTokens,
|
|
1781
|
+
cacheReadTokens: bufferedUsage.cacheReadTokens, cacheCreateTokens: bufferedUsage.cacheCreateTokens,
|
|
1782
|
+
thinkingTokens: bufferedUsage.thinkingTokens,
|
|
1628
1783
|
claim: poolAccount.rateLimit.claim, util5h: poolAccount.rateLimit.util5h,
|
|
1629
1784
|
util7d: poolAccount.rateLimit.util7d, overageUtil: poolAccount.rateLimit.overageUtil,
|
|
1630
1785
|
latencyMs: Date.now() - startTime, status: upstream.status, isStream: false, isOpenAI,
|
|
@@ -1632,6 +1787,20 @@ export async function startProxy(opts = {}) {
|
|
|
1632
1787
|
}
|
|
1633
1788
|
catch { /* don't let analytics errors break responses */ }
|
|
1634
1789
|
}
|
|
1790
|
+
writeLogLine(logFileStream, {
|
|
1791
|
+
ts: new Date().toISOString(), req: requestCount,
|
|
1792
|
+
method: req.method ?? '', path: urlPath,
|
|
1793
|
+
model: bufferedUsage?.model || requestModel || undefined,
|
|
1794
|
+
status: upstream.status, latency_ms: Date.now() - startTime,
|
|
1795
|
+
in_tokens: bufferedUsage?.inputTokens, out_tokens: bufferedUsage?.outputTokens,
|
|
1796
|
+
cache_read: bufferedUsage?.cacheReadTokens, cache_create: bufferedUsage?.cacheCreateTokens,
|
|
1797
|
+
claim: poolAccount?.rateLimit.claim,
|
|
1798
|
+
bucket: poolAccount ? billingBucketFromClaim(poolAccount.rateLimit.claim) : undefined,
|
|
1799
|
+
account: poolAccount?.alias,
|
|
1800
|
+
client: detectedClientForLog,
|
|
1801
|
+
preserve_tools: preserveToolsEffective,
|
|
1802
|
+
stream: false,
|
|
1803
|
+
});
|
|
1635
1804
|
if (verbose)
|
|
1636
1805
|
console.log(`[dario] #${requestCount} ${upstream.status}`);
|
|
1637
1806
|
}
|
|
@@ -1639,9 +1808,17 @@ export async function startProxy(opts = {}) {
|
|
|
1639
1808
|
catch (err) {
|
|
1640
1809
|
// Differentiate the three failure modes so each gets the right
|
|
1641
1810
|
// response (and so we don't spam logs when clients simply drop).
|
|
1811
|
+
const errLogBase = {
|
|
1812
|
+
ts: new Date().toISOString(), req: requestCount,
|
|
1813
|
+
method: req.method ?? '', path: urlPath,
|
|
1814
|
+
model: requestModel || undefined,
|
|
1815
|
+
client: detectedClientForLog,
|
|
1816
|
+
preserve_tools: preserveToolsEffective,
|
|
1817
|
+
};
|
|
1642
1818
|
if (upstreamAbortReason === 'client_closed') {
|
|
1643
1819
|
if (verbose)
|
|
1644
1820
|
console.log(`[dario] #${requestCount} aborted (client disconnected)`);
|
|
1821
|
+
writeLogLine(logFileStream, { ...errLogBase, reject: 'client-closed' });
|
|
1645
1822
|
}
|
|
1646
1823
|
else if (upstreamAbortReason === 'timeout') {
|
|
1647
1824
|
console.error(`[dario] #${requestCount} upstream timeout after ${UPSTREAM_TIMEOUT_MS / 1000}s`);
|
|
@@ -1652,6 +1829,7 @@ export async function startProxy(opts = {}) {
|
|
|
1652
1829
|
else if (!res.writableEnded) {
|
|
1653
1830
|
res.end();
|
|
1654
1831
|
}
|
|
1832
|
+
writeLogLine(logFileStream, { ...errLogBase, status: 504, error: 'upstream-timeout' });
|
|
1655
1833
|
}
|
|
1656
1834
|
else {
|
|
1657
1835
|
// Log full error server-side, return generic message to client
|
|
@@ -1663,6 +1841,7 @@ export async function startProxy(opts = {}) {
|
|
|
1663
1841
|
else if (!res.writableEnded) {
|
|
1664
1842
|
res.end();
|
|
1665
1843
|
}
|
|
1844
|
+
writeLogLine(logFileStream, { ...errLogBase, status: 502, error: sanitizeError(err) });
|
|
1666
1845
|
}
|
|
1667
1846
|
}
|
|
1668
1847
|
finally {
|
|
@@ -1823,6 +2002,8 @@ export async function startProxy(opts = {}) {
|
|
|
1823
2002
|
console.log('\n[dario] Shutting down...');
|
|
1824
2003
|
clearInterval(presenceInterval);
|
|
1825
2004
|
clearInterval(refreshInterval);
|
|
2005
|
+
if (logFileStream)
|
|
2006
|
+
logFileStream.end();
|
|
1826
2007
|
server.close(() => process.exit(0));
|
|
1827
2008
|
// Force exit after 5s if connections don't close
|
|
1828
2009
|
setTimeout(() => process.exit(0), 5000).unref();
|
|
@@ -75,3 +75,29 @@ export declare function classifyRuntimeFingerprint(runningUnderBun: boolean, ava
|
|
|
75
75
|
* directly with synthetic inputs.
|
|
76
76
|
*/
|
|
77
77
|
export declare function detectRuntimeFingerprint(): RuntimeFingerprint;
|
|
78
|
+
/**
|
|
79
|
+
* One-shot Bun installer. Used by `dario doctor --bun-bootstrap` to
|
|
80
|
+
* close the gap between "Bun warn surfaced" and "Bun on PATH" without
|
|
81
|
+
* making the user copy-paste an install line. Picks the platform-correct
|
|
82
|
+
* upstream installer:
|
|
83
|
+
*
|
|
84
|
+
* - Windows: `powershell -c "irm https://bun.sh/install.ps1 | iex"`
|
|
85
|
+
* - macOS / Linux: `curl -fsSL https://bun.sh/install | bash`
|
|
86
|
+
*
|
|
87
|
+
* Streams installer output to the parent stdio so the user sees what's
|
|
88
|
+
* happening (the install can take 10-30 s on a slow link). Returns the
|
|
89
|
+
* exit code; non-zero is surfaced by the caller as a fail row.
|
|
90
|
+
*
|
|
91
|
+
* Pure delegation to the upstream Bun installer — dario does not vendor
|
|
92
|
+
* or self-host the binary. If the user wants a pinned version or doesn't
|
|
93
|
+
* want to run a curl-to-shell installer, the doctor warn line still
|
|
94
|
+
* points at https://bun.sh for manual install.
|
|
95
|
+
*
|
|
96
|
+
* Pinned to bun.sh (not bun.com) because PowerShell's `irm` doesn't
|
|
97
|
+
* follow the bun.com → bun.sh 308 redirect; piping the redirect HTML
|
|
98
|
+
* to `iex` then fails parse. bun.sh serves the install script directly.
|
|
99
|
+
*/
|
|
100
|
+
export declare function bunBootstrap(): Promise<{
|
|
101
|
+
exitCode: number;
|
|
102
|
+
runner: string;
|
|
103
|
+
}>;
|
|
@@ -115,3 +115,46 @@ export function detectRuntimeFingerprint() {
|
|
|
115
115
|
const probed = probeBunVersion();
|
|
116
116
|
return classifyRuntimeFingerprint(false, probed, process.env);
|
|
117
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* One-shot Bun installer. Used by `dario doctor --bun-bootstrap` to
|
|
120
|
+
* close the gap between "Bun warn surfaced" and "Bun on PATH" without
|
|
121
|
+
* making the user copy-paste an install line. Picks the platform-correct
|
|
122
|
+
* upstream installer:
|
|
123
|
+
*
|
|
124
|
+
* - Windows: `powershell -c "irm https://bun.sh/install.ps1 | iex"`
|
|
125
|
+
* - macOS / Linux: `curl -fsSL https://bun.sh/install | bash`
|
|
126
|
+
*
|
|
127
|
+
* Streams installer output to the parent stdio so the user sees what's
|
|
128
|
+
* happening (the install can take 10-30 s on a slow link). Returns the
|
|
129
|
+
* exit code; non-zero is surfaced by the caller as a fail row.
|
|
130
|
+
*
|
|
131
|
+
* Pure delegation to the upstream Bun installer — dario does not vendor
|
|
132
|
+
* or self-host the binary. If the user wants a pinned version or doesn't
|
|
133
|
+
* want to run a curl-to-shell installer, the doctor warn line still
|
|
134
|
+
* points at https://bun.sh for manual install.
|
|
135
|
+
*
|
|
136
|
+
* Pinned to bun.sh (not bun.com) because PowerShell's `irm` doesn't
|
|
137
|
+
* follow the bun.com → bun.sh 308 redirect; piping the redirect HTML
|
|
138
|
+
* to `iex` then fails parse. bun.sh serves the install script directly.
|
|
139
|
+
*/
|
|
140
|
+
export async function bunBootstrap() {
|
|
141
|
+
const { spawn } = await import('node:child_process');
|
|
142
|
+
const isWindows = process.platform === 'win32';
|
|
143
|
+
const runner = isWindows
|
|
144
|
+
? 'powershell -NoProfile -ExecutionPolicy Bypass -c "irm https://bun.sh/install.ps1 | iex"'
|
|
145
|
+
: 'curl -fsSL https://bun.sh/install | bash';
|
|
146
|
+
return await new Promise((resolve) => {
|
|
147
|
+
// Single-shell invocation so the pipe stages execute the way the
|
|
148
|
+
// upstream installer expects. Avoids reimplementing the curl-pipe-bash
|
|
149
|
+
// sequencing in Node primitives.
|
|
150
|
+
const child = isWindows
|
|
151
|
+
? spawn('powershell', [
|
|
152
|
+
'-NoProfile',
|
|
153
|
+
'-ExecutionPolicy', 'Bypass',
|
|
154
|
+
'-Command', 'irm https://bun.sh/install.ps1 | iex',
|
|
155
|
+
], { stdio: 'inherit' })
|
|
156
|
+
: spawn('bash', ['-lc', 'curl -fsSL https://bun.sh/install | bash'], { stdio: 'inherit' });
|
|
157
|
+
child.on('error', () => resolve({ exitCode: 1, runner }));
|
|
158
|
+
child.on('exit', (code) => resolve({ exitCode: code ?? 1, runner }));
|
|
159
|
+
});
|
|
160
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.32.0",
|
|
4
4
|
"description": "A local LLM router. One endpoint, every provider — Claude subscriptions, OpenAI, OpenRouter, Groq, local LiteLLM, any OpenAI-compat endpoint — your tools don't need to change.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|