@uvl000/claude-trace 3.0.13-workflowexp.1
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 +477 -0
- package/README.zh-CN.md +477 -0
- package/dist/adapt/api-format.d.ts +41 -0
- package/dist/adapt/api-format.d.ts.map +1 -0
- package/dist/adapt/api-format.js +132 -0
- package/dist/adapt/api-format.js.map +1 -0
- package/dist/adapt/openai-adapter.d.ts +123 -0
- package/dist/adapt/openai-adapter.d.ts.map +1 -0
- package/dist/adapt/openai-adapter.js +628 -0
- package/dist/adapt/openai-adapter.js.map +1 -0
- package/dist/cli/cli-common.d.ts +73 -0
- package/dist/cli/cli-common.d.ts.map +1 -0
- package/dist/cli/cli-common.js +189 -0
- package/dist/cli/cli-common.js.map +1 -0
- package/dist/cli/cli.d.ts +23 -0
- package/dist/cli/cli.d.ts.map +1 -0
- package/dist/cli/cli.js +175 -0
- package/dist/cli/cli.js.map +1 -0
- package/dist/cli/codex-cli.d.ts +24 -0
- package/dist/cli/codex-cli.d.ts.map +1 -0
- package/dist/cli/codex-cli.js +146 -0
- package/dist/cli/codex-cli.js.map +1 -0
- package/dist/cli/opencode-cli.d.ts +23 -0
- package/dist/cli/opencode-cli.d.ts.map +1 -0
- package/dist/cli/opencode-cli.js +156 -0
- package/dist/cli/opencode-cli.js.map +1 -0
- package/dist/cli/trace-runner.d.ts +21 -0
- package/dist/cli/trace-runner.d.ts.map +1 -0
- package/dist/cli/trace-runner.js +282 -0
- package/dist/cli/trace-runner.js.map +1 -0
- package/dist/cli/validate-trace.d.ts +30 -0
- package/dist/cli/validate-trace.d.ts.map +1 -0
- package/dist/cli/validate-trace.js +230 -0
- package/dist/cli/validate-trace.js.map +1 -0
- package/dist/cli/verify-offline-bundle.d.ts +13 -0
- package/dist/cli/verify-offline-bundle.d.ts.map +1 -0
- package/dist/cli/verify-offline-bundle.js +89 -0
- package/dist/cli/verify-offline-bundle.js.map +1 -0
- package/dist/cli/vibe-coding-proxy-cli.d.ts +12 -0
- package/dist/cli/vibe-coding-proxy-cli.d.ts.map +1 -0
- package/dist/cli/vibe-coding-proxy-cli.js +218 -0
- package/dist/cli/vibe-coding-proxy-cli.js.map +1 -0
- package/dist/config/claude-config-overlay.d.ts +69 -0
- package/dist/config/claude-config-overlay.d.ts.map +1 -0
- package/dist/config/claude-config-overlay.js +229 -0
- package/dist/config/claude-config-overlay.js.map +1 -0
- package/dist/config/codex-config-overlay.d.ts +122 -0
- package/dist/config/codex-config-overlay.d.ts.map +1 -0
- package/dist/config/codex-config-overlay.js +352 -0
- package/dist/config/codex-config-overlay.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/intercept/forward-proxy.d.ts +77 -0
- package/dist/intercept/forward-proxy.d.ts.map +1 -0
- package/dist/intercept/forward-proxy.js +368 -0
- package/dist/intercept/forward-proxy.js.map +1 -0
- package/dist/intercept/header-redaction.d.ts +7 -0
- package/dist/intercept/header-redaction.d.ts.map +1 -0
- package/dist/intercept/header-redaction.js +28 -0
- package/dist/intercept/header-redaction.js.map +1 -0
- package/dist/intercept/interceptor-loader.js +26 -0
- package/dist/intercept/interceptor.d.ts +164 -0
- package/dist/intercept/interceptor.d.ts.map +1 -0
- package/dist/intercept/interceptor.js +496 -0
- package/dist/intercept/interceptor.js.map +1 -0
- package/dist/intercept/mitm-cert.d.ts +53 -0
- package/dist/intercept/mitm-cert.d.ts.map +1 -0
- package/dist/intercept/mitm-cert.js +259 -0
- package/dist/intercept/mitm-cert.js.map +1 -0
- package/dist/intercept/proxy-log-writer.d.ts +77 -0
- package/dist/intercept/proxy-log-writer.d.ts.map +1 -0
- package/dist/intercept/proxy-log-writer.js +323 -0
- package/dist/intercept/proxy-log-writer.js.map +1 -0
- package/dist/intercept/proxy-targets.d.ts +69 -0
- package/dist/intercept/proxy-targets.d.ts.map +1 -0
- package/dist/intercept/proxy-targets.js +129 -0
- package/dist/intercept/proxy-targets.js.map +1 -0
- package/dist/intercept/reverse-proxy.d.ts +191 -0
- package/dist/intercept/reverse-proxy.d.ts.map +1 -0
- package/dist/intercept/reverse-proxy.js +807 -0
- package/dist/intercept/reverse-proxy.js.map +1 -0
- package/dist/intercept/token-extractor.js +28 -0
- package/dist/report/html-generator.d.ts +96 -0
- package/dist/report/html-generator.d.ts.map +1 -0
- package/dist/report/html-generator.js +225 -0
- package/dist/report/html-generator.js.map +1 -0
- package/dist/report/index-generator.d.ts +89 -0
- package/dist/report/index-generator.d.ts.map +1 -0
- package/dist/report/index-generator.js +319 -0
- package/dist/report/index-generator.js.map +1 -0
- package/dist/report/shared-conversation-processor.d.ts +200 -0
- package/dist/report/shared-conversation-processor.d.ts.map +1 -0
- package/dist/report/shared-conversation-processor.js +720 -0
- package/dist/report/shared-conversation-processor.js.map +1 -0
- package/dist/routing/codex-routing.d.ts +40 -0
- package/dist/routing/codex-routing.d.ts.map +1 -0
- package/dist/routing/codex-routing.js +96 -0
- package/dist/routing/codex-routing.js.map +1 -0
- package/dist/routing/proxy-routing.d.ts +48 -0
- package/dist/routing/proxy-routing.d.ts.map +1 -0
- package/dist/routing/proxy-routing.js +113 -0
- package/dist/routing/proxy-routing.js.map +1 -0
- package/dist/tools/binary-utils.d.ts +29 -0
- package/dist/tools/binary-utils.d.ts.map +1 -0
- package/dist/tools/binary-utils.js +134 -0
- package/dist/tools/binary-utils.js.map +1 -0
- package/dist/tools/claude.d.ts +61 -0
- package/dist/tools/claude.d.ts.map +1 -0
- package/dist/tools/claude.js +449 -0
- package/dist/tools/claude.js.map +1 -0
- package/dist/tools/codex.d.ts +70 -0
- package/dist/tools/codex.d.ts.map +1 -0
- package/dist/tools/codex.js +492 -0
- package/dist/tools/codex.js.map +1 -0
- package/dist/tools/opencode.d.ts +85 -0
- package/dist/tools/opencode.d.ts.map +1 -0
- package/dist/tools/opencode.js +401 -0
- package/dist/tools/opencode.js.map +1 -0
- package/dist/tools/types.d.ts +91 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +11 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/types.d.ts +158 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/frontend/dist/index.global.js +1551 -0
- package/frontend/dist/styles.css +1027 -0
- package/frontend/template.html +19 -0
- package/node_modules/agent-base/LICENSE +22 -0
- package/node_modules/agent-base/README.md +69 -0
- package/node_modules/agent-base/dist/helpers.d.ts +15 -0
- package/node_modules/agent-base/dist/helpers.d.ts.map +1 -0
- package/node_modules/agent-base/dist/helpers.js +66 -0
- package/node_modules/agent-base/dist/helpers.js.map +1 -0
- package/node_modules/agent-base/dist/index.d.ts +41 -0
- package/node_modules/agent-base/dist/index.d.ts.map +1 -0
- package/node_modules/agent-base/dist/index.js +178 -0
- package/node_modules/agent-base/dist/index.js.map +1 -0
- package/node_modules/agent-base/package.json +46 -0
- package/node_modules/debug/LICENSE +20 -0
- package/node_modules/debug/README.md +481 -0
- package/node_modules/debug/package.json +64 -0
- package/node_modules/debug/src/browser.js +272 -0
- package/node_modules/debug/src/common.js +292 -0
- package/node_modules/debug/src/index.js +10 -0
- package/node_modules/debug/src/node.js +263 -0
- package/node_modules/http-proxy-agent/LICENSE +22 -0
- package/node_modules/http-proxy-agent/README.md +44 -0
- package/node_modules/http-proxy-agent/dist/index.d.ts +44 -0
- package/node_modules/http-proxy-agent/dist/index.d.ts.map +1 -0
- package/node_modules/http-proxy-agent/dist/index.js +148 -0
- package/node_modules/http-proxy-agent/dist/index.js.map +1 -0
- package/node_modules/http-proxy-agent/package.json +47 -0
- package/node_modules/https-proxy-agent/LICENSE +22 -0
- package/node_modules/https-proxy-agent/README.md +70 -0
- package/node_modules/https-proxy-agent/dist/index.d.ts +47 -0
- package/node_modules/https-proxy-agent/dist/index.d.ts.map +1 -0
- package/node_modules/https-proxy-agent/dist/index.js +180 -0
- package/node_modules/https-proxy-agent/dist/index.js.map +1 -0
- package/node_modules/https-proxy-agent/dist/parse-proxy-response.d.ts +15 -0
- package/node_modules/https-proxy-agent/dist/parse-proxy-response.d.ts.map +1 -0
- package/node_modules/https-proxy-agent/dist/parse-proxy-response.js +101 -0
- package/node_modules/https-proxy-agent/dist/parse-proxy-response.js.map +1 -0
- package/node_modules/https-proxy-agent/package.json +50 -0
- package/node_modules/ms/index.js +162 -0
- package/node_modules/ms/license.md +21 -0
- package/node_modules/ms/package.json +38 -0
- package/node_modules/ms/readme.md +59 -0
- package/node_modules/node-forge/LICENSE +331 -0
- package/node_modules/node-forge/README.md +2071 -0
- package/node_modules/node-forge/dist/forge.all.min.js +2 -0
- package/node_modules/node-forge/dist/forge.all.min.js.map +1 -0
- package/node_modules/node-forge/dist/forge.min.js +2 -0
- package/node_modules/node-forge/dist/forge.min.js.map +1 -0
- package/node_modules/node-forge/dist/prime.worker.min.js +2 -0
- package/node_modules/node-forge/dist/prime.worker.min.js.map +1 -0
- package/node_modules/node-forge/flash/swf/SocketPool.swf +0 -0
- package/node_modules/node-forge/lib/aes.js +1091 -0
- package/node_modules/node-forge/lib/aesCipherSuites.js +282 -0
- package/node_modules/node-forge/lib/asn1-validator.js +91 -0
- package/node_modules/node-forge/lib/asn1.js +1503 -0
- package/node_modules/node-forge/lib/baseN.js +186 -0
- package/node_modules/node-forge/lib/cipher.js +230 -0
- package/node_modules/node-forge/lib/cipherModes.js +999 -0
- package/node_modules/node-forge/lib/des.js +496 -0
- package/node_modules/node-forge/lib/ed25519.js +1091 -0
- package/node_modules/node-forge/lib/forge.js +13 -0
- package/node_modules/node-forge/lib/form.js +149 -0
- package/node_modules/node-forge/lib/hmac.js +146 -0
- package/node_modules/node-forge/lib/http.js +1346 -0
- package/node_modules/node-forge/lib/index.all.js +16 -0
- package/node_modules/node-forge/lib/index.js +33 -0
- package/node_modules/node-forge/lib/jsbn.js +1294 -0
- package/node_modules/node-forge/lib/kem.js +168 -0
- package/node_modules/node-forge/lib/log.js +319 -0
- package/node_modules/node-forge/lib/md.all.js +13 -0
- package/node_modules/node-forge/lib/md.js +11 -0
- package/node_modules/node-forge/lib/md5.js +289 -0
- package/node_modules/node-forge/lib/mgf.js +12 -0
- package/node_modules/node-forge/lib/mgf1.js +57 -0
- package/node_modules/node-forge/lib/oids.js +180 -0
- package/node_modules/node-forge/lib/pbe.js +1023 -0
- package/node_modules/node-forge/lib/pbkdf2.js +211 -0
- package/node_modules/node-forge/lib/pem.js +237 -0
- package/node_modules/node-forge/lib/pkcs1.js +276 -0
- package/node_modules/node-forge/lib/pkcs12.js +1078 -0
- package/node_modules/node-forge/lib/pkcs7.js +1260 -0
- package/node_modules/node-forge/lib/pkcs7asn1.js +410 -0
- package/node_modules/node-forge/lib/pki.js +102 -0
- package/node_modules/node-forge/lib/prime.js +297 -0
- package/node_modules/node-forge/lib/prime.worker.js +168 -0
- package/node_modules/node-forge/lib/prng.js +419 -0
- package/node_modules/node-forge/lib/pss.js +241 -0
- package/node_modules/node-forge/lib/random.js +191 -0
- package/node_modules/node-forge/lib/rc2.js +410 -0
- package/node_modules/node-forge/lib/rsa.js +1968 -0
- package/node_modules/node-forge/lib/sha1.js +319 -0
- package/node_modules/node-forge/lib/sha256.js +327 -0
- package/node_modules/node-forge/lib/sha512.js +561 -0
- package/node_modules/node-forge/lib/socket.js +287 -0
- package/node_modules/node-forge/lib/ssh.js +236 -0
- package/node_modules/node-forge/lib/tls.js +4282 -0
- package/node_modules/node-forge/lib/tlssocket.js +249 -0
- package/node_modules/node-forge/lib/util.js +2652 -0
- package/node_modules/node-forge/lib/x509.js +3251 -0
- package/node_modules/node-forge/lib/xhr.js +738 -0
- package/node_modules/node-forge/package.json +123 -0
- package/node_modules/smol-toml/LICENSE +24 -0
- package/node_modules/smol-toml/README.md +239 -0
- package/node_modules/smol-toml/dist/date.d.ts +41 -0
- package/node_modules/smol-toml/dist/date.js +127 -0
- package/node_modules/smol-toml/dist/error.d.ts +38 -0
- package/node_modules/smol-toml/dist/error.js +63 -0
- package/node_modules/smol-toml/dist/extract.d.ts +30 -0
- package/node_modules/smol-toml/dist/extract.js +100 -0
- package/node_modules/smol-toml/dist/index.cjs +897 -0
- package/node_modules/smol-toml/dist/index.d.ts +43 -0
- package/node_modules/smol-toml/dist/index.js +33 -0
- package/node_modules/smol-toml/dist/parse.d.ts +37 -0
- package/node_modules/smol-toml/dist/parse.js +148 -0
- package/node_modules/smol-toml/dist/primitive.d.ts +31 -0
- package/node_modules/smol-toml/dist/primitive.js +179 -0
- package/node_modules/smol-toml/dist/stringify.d.ts +31 -0
- package/node_modules/smol-toml/dist/stringify.js +167 -0
- package/node_modules/smol-toml/dist/struct.d.ts +32 -0
- package/node_modules/smol-toml/dist/struct.js +184 -0
- package/node_modules/smol-toml/dist/util.d.ts +42 -0
- package/node_modules/smol-toml/dist/util.js +111 -0
- package/node_modules/smol-toml/package.json +54 -0
- package/offline/bin/claude-trace +6 -0
- package/offline/bin/claude-wrapper +13 -0
- package/offline/bin/install-agent +14 -0
- package/offline/bin/validate-trace +6 -0
- package/offline/bin/verify-bundle +7 -0
- package/package.json +103 -0
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @file Local HTTP reverse proxy for intercepting LLM API traffic from native binaries.
|
|
4
|
+
*
|
|
5
|
+
* Used when the target coding agent (Claude Code V2+, OpenCode, Codex CLI) is a native
|
|
6
|
+
* binary that cannot be launched with Node's `--require` interceptor hook. The proxy
|
|
7
|
+
* listens on 127.0.0.1, rewrites upstream URLs based on tool-specific routing rules,
|
|
8
|
+
* forwards requests transparently, and logs request/response pairs to JSONL/JSON/HTML.
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.ReverseProxyServer = void 0;
|
|
45
|
+
exports.supportsZstd = supportsZstd;
|
|
46
|
+
exports.normalizeAcceptEncoding = normalizeAcceptEncoding;
|
|
47
|
+
exports.resolveOutboundProxy = resolveOutboundProxy;
|
|
48
|
+
exports.decodeResponseBody = decodeResponseBody;
|
|
49
|
+
const https = __importStar(require("https"));
|
|
50
|
+
const http = __importStar(require("http"));
|
|
51
|
+
const fs = __importStar(require("fs"));
|
|
52
|
+
const path = __importStar(require("path"));
|
|
53
|
+
const zlib = __importStar(require("zlib"));
|
|
54
|
+
const child_process_1 = require("child_process");
|
|
55
|
+
const http_proxy_agent_1 = require("http-proxy-agent");
|
|
56
|
+
const https_proxy_agent_1 = require("https-proxy-agent");
|
|
57
|
+
const cli_common_1 = require("../cli/cli-common");
|
|
58
|
+
const header_redaction_1 = require("./header-redaction");
|
|
59
|
+
const html_generator_1 = require("../report/html-generator");
|
|
60
|
+
const shared_conversation_processor_1 = require("../report/shared-conversation-processor");
|
|
61
|
+
const openai_adapter_1 = require("../adapt/openai-adapter");
|
|
62
|
+
const api_format_1 = require("../adapt/api-format");
|
|
63
|
+
const codex_routing_1 = require("../routing/codex-routing");
|
|
64
|
+
const proxy_routing_1 = require("../routing/proxy-routing");
|
|
65
|
+
/** Whether this Node runtime can decode zstd-compressed API responses. */
|
|
66
|
+
function supportsZstd() {
|
|
67
|
+
return typeof zlib.zstdDecompressSync === "function";
|
|
68
|
+
}
|
|
69
|
+
/** Remove zstd from Accept-Encoding when the current Node cannot decode it. */
|
|
70
|
+
function normalizeAcceptEncoding(value, zstdSupported = supportsZstd()) {
|
|
71
|
+
if (value === undefined || zstdSupported)
|
|
72
|
+
return Array.isArray(value) ? value.join(", ") : value;
|
|
73
|
+
const encodings = (Array.isArray(value) ? value.join(",") : value)
|
|
74
|
+
.split(",")
|
|
75
|
+
.map((item) => item.trim())
|
|
76
|
+
.filter((item) => item && item.split(";", 1)[0].toLowerCase() !== "zstd");
|
|
77
|
+
return encodings.length > 0 ? encodings.join(", ") : undefined;
|
|
78
|
+
}
|
|
79
|
+
function noProxyMatches(targetUrl, noProxy) {
|
|
80
|
+
const hostname = targetUrl.hostname.toLowerCase();
|
|
81
|
+
const port = targetUrl.port || (targetUrl.protocol === "https:" ? "443" : "80");
|
|
82
|
+
for (const rawEntry of noProxy.split(",")) {
|
|
83
|
+
let entry = rawEntry.trim().toLowerCase();
|
|
84
|
+
if (!entry)
|
|
85
|
+
continue;
|
|
86
|
+
if (entry === "*")
|
|
87
|
+
return true;
|
|
88
|
+
let entryPort = "";
|
|
89
|
+
const portIndex = entry.lastIndexOf(":");
|
|
90
|
+
if (portIndex > 0 && /^\d+$/.test(entry.slice(portIndex + 1))) {
|
|
91
|
+
entryPort = entry.slice(portIndex + 1);
|
|
92
|
+
entry = entry.slice(0, portIndex);
|
|
93
|
+
}
|
|
94
|
+
if (entryPort && entryPort !== port)
|
|
95
|
+
continue;
|
|
96
|
+
entry = entry.replace(/^\*\./, ".");
|
|
97
|
+
if (entry.startsWith(".")) {
|
|
98
|
+
if (hostname === entry.slice(1) || hostname.endsWith(entry))
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
else if (hostname === entry || hostname.endsWith(`.${entry}`)) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
/** Resolve the process-level outbound proxy for one upstream URL, respecting NO_PROXY. */
|
|
108
|
+
function resolveOutboundProxy(targetUrl, environment = process.env) {
|
|
109
|
+
const parsed = new URL(targetUrl);
|
|
110
|
+
const noProxy = environment.NO_PROXY || environment.no_proxy || "";
|
|
111
|
+
if (noProxyMatches(parsed, noProxy))
|
|
112
|
+
return undefined;
|
|
113
|
+
if (parsed.protocol === "https:") {
|
|
114
|
+
return (environment.HTTPS_PROXY ||
|
|
115
|
+
environment.https_proxy ||
|
|
116
|
+
environment.HTTP_PROXY ||
|
|
117
|
+
environment.http_proxy ||
|
|
118
|
+
environment.ALL_PROXY ||
|
|
119
|
+
environment.all_proxy);
|
|
120
|
+
}
|
|
121
|
+
return (environment.HTTP_PROXY ||
|
|
122
|
+
environment.http_proxy ||
|
|
123
|
+
environment.ALL_PROXY ||
|
|
124
|
+
environment.all_proxy);
|
|
125
|
+
}
|
|
126
|
+
function createOutboundAgent(targetUrl) {
|
|
127
|
+
const proxyUrl = resolveOutboundProxy(targetUrl);
|
|
128
|
+
if (!proxyUrl)
|
|
129
|
+
return undefined;
|
|
130
|
+
return new URL(targetUrl).protocol === "https:"
|
|
131
|
+
? new https_proxy_agent_1.HttpsProxyAgent(proxyUrl)
|
|
132
|
+
: new http_proxy_agent_1.HttpProxyAgent(proxyUrl);
|
|
133
|
+
}
|
|
134
|
+
/** Decode a buffered upstream response for structured JSON/SSE logging. */
|
|
135
|
+
function decodeResponseBody(buffer, contentEncoding) {
|
|
136
|
+
try {
|
|
137
|
+
if (contentEncoding === "zstd") {
|
|
138
|
+
const decompress = zlib.zstdDecompressSync;
|
|
139
|
+
if (decompress)
|
|
140
|
+
return decompress(buffer).toString("utf-8");
|
|
141
|
+
}
|
|
142
|
+
if (contentEncoding === "gzip")
|
|
143
|
+
return zlib.gunzipSync(buffer).toString("utf-8");
|
|
144
|
+
if (contentEncoding === "br")
|
|
145
|
+
return zlib.brotliDecompressSync(buffer).toString("utf-8");
|
|
146
|
+
if (contentEncoding === "deflate")
|
|
147
|
+
return zlib.inflateSync(buffer).toString("utf-8");
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Preserve the raw body below so validation can reject an unparseable exchange.
|
|
151
|
+
}
|
|
152
|
+
return buffer.toString("utf-8");
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Parses a base URL string into host, port, protocol, and path prefix.
|
|
156
|
+
*
|
|
157
|
+
* @param targetBaseUrl - Full base URL (e.g. `https://api.anthropic.com/v1`).
|
|
158
|
+
* @returns Parsed target suitable for constructing upstream requests.
|
|
159
|
+
*/
|
|
160
|
+
function parseTargetBaseUrl(targetBaseUrl) {
|
|
161
|
+
const parsed = new URL(targetBaseUrl || "https://api.anthropic.com");
|
|
162
|
+
// Strip trailing slash from pathname so path concatenation stays predictable.
|
|
163
|
+
const pathPrefix = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, "");
|
|
164
|
+
return {
|
|
165
|
+
protocol: parsed.protocol,
|
|
166
|
+
targetHost: parsed.hostname,
|
|
167
|
+
targetPort: parsed.port
|
|
168
|
+
? parseInt(parsed.port, 10)
|
|
169
|
+
: parsed.protocol === "https:" ? 443 : 80,
|
|
170
|
+
pathPrefix,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* URL path segments that identify LLM API endpoints worth logging.
|
|
175
|
+
* Matched via substring check so query strings and provider prefixes still match.
|
|
176
|
+
*/
|
|
177
|
+
const LLM_API_PATHS = [
|
|
178
|
+
"/v1/messages",
|
|
179
|
+
"/messages",
|
|
180
|
+
"/v1/chat/completions",
|
|
181
|
+
"/chat/completions",
|
|
182
|
+
"/v1/responses",
|
|
183
|
+
"/responses",
|
|
184
|
+
"/responses/compact",
|
|
185
|
+
"/backend-api/codex/responses",
|
|
186
|
+
];
|
|
187
|
+
/**
|
|
188
|
+
* Returns true when the request path targets a known LLM API endpoint.
|
|
189
|
+
*
|
|
190
|
+
* @param urlPath - Raw request URL path (may include query string).
|
|
191
|
+
*/
|
|
192
|
+
function isLlmApiPath(urlPath) {
|
|
193
|
+
if (!urlPath) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
return LLM_API_PATHS.some((segment) => urlPath.includes(segment));
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Resolves upstream target for OpenCode provider-prefixed paths (`/p/{providerId}/...`).
|
|
200
|
+
*
|
|
201
|
+
* When the path does not match the `/p/` convention or the provider is unknown,
|
|
202
|
+
* falls back to the default target and preserves the original path.
|
|
203
|
+
*
|
|
204
|
+
* @param reqUrl - Incoming request URL from the client.
|
|
205
|
+
* @param routes - Provider-id → upstream base URL map.
|
|
206
|
+
* @param fallback - Default upstream when routing does not apply.
|
|
207
|
+
* @returns Parsed upstream target plus the full path used for logging and forwarding.
|
|
208
|
+
*/
|
|
209
|
+
function resolveRouteTarget(reqUrl, routes, fallback) {
|
|
210
|
+
if (!reqUrl || !routes || Object.keys(routes).length === 0) {
|
|
211
|
+
const upstreamDisplayPath = `${fallback.pathPrefix}${reqUrl || "/"}`;
|
|
212
|
+
return { ...fallback, upstreamDisplayPath };
|
|
213
|
+
}
|
|
214
|
+
// OpenCode encodes provider selection in the path: /p/{providerId}/remainder
|
|
215
|
+
const match = reqUrl.match(/^\/p\/([^/]+)(\/.*)?$/);
|
|
216
|
+
if (!match) {
|
|
217
|
+
const upstreamDisplayPath = `${fallback.pathPrefix}${reqUrl}`;
|
|
218
|
+
return { ...fallback, upstreamDisplayPath };
|
|
219
|
+
}
|
|
220
|
+
const providerId = decodeURIComponent(match[1]);
|
|
221
|
+
const remainder = match[2] || "/";
|
|
222
|
+
const upstreamBaseUrl = routes[providerId];
|
|
223
|
+
if (!upstreamBaseUrl) {
|
|
224
|
+
const upstreamDisplayPath = `${fallback.pathPrefix}${reqUrl}`;
|
|
225
|
+
return { ...fallback, upstreamDisplayPath };
|
|
226
|
+
}
|
|
227
|
+
const target = parseTargetBaseUrl(upstreamBaseUrl);
|
|
228
|
+
const upstreamDisplayPath = `${target.pathPrefix}${remainder}`;
|
|
229
|
+
return {
|
|
230
|
+
...target,
|
|
231
|
+
upstreamDisplayPath,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Returns the request `Content-Encoding` header normalized to lowercase.
|
|
236
|
+
*
|
|
237
|
+
* @param headers - Incoming HTTP headers from the client request.
|
|
238
|
+
*/
|
|
239
|
+
function getRequestContentEncoding(headers) {
|
|
240
|
+
return (headers["content-encoding"] || "").toLowerCase();
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Decompresses a request body buffer when Codex (or other clients) send compressed JSON.
|
|
244
|
+
*
|
|
245
|
+
* Supports zstd (Codex ChatGPT OAuth), gzip, br, and deflate. Returns the original
|
|
246
|
+
* buffer when encoding is absent or decompression is unavailable/fails.
|
|
247
|
+
*
|
|
248
|
+
* @param buffer - Raw request body bytes from the client.
|
|
249
|
+
* @param contentEncoding - Lowercase `Content-Encoding` header value.
|
|
250
|
+
*/
|
|
251
|
+
function decompressRequestBodyBuffer(buffer, contentEncoding) {
|
|
252
|
+
if (!buffer.length || !contentEncoding) {
|
|
253
|
+
return buffer;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
if (contentEncoding === "zstd") {
|
|
257
|
+
const zstdDecompressSync = zlib.zstdDecompressSync;
|
|
258
|
+
if (zstdDecompressSync) {
|
|
259
|
+
return zstdDecompressSync(buffer);
|
|
260
|
+
}
|
|
261
|
+
return buffer;
|
|
262
|
+
}
|
|
263
|
+
if (contentEncoding === "gzip") {
|
|
264
|
+
return zlib.gunzipSync(buffer);
|
|
265
|
+
}
|
|
266
|
+
if (contentEncoding === "br") {
|
|
267
|
+
return zlib.brotliDecompressSync(buffer);
|
|
268
|
+
}
|
|
269
|
+
if (contentEncoding === "deflate") {
|
|
270
|
+
return zlib.inflateSync(buffer);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return buffer;
|
|
275
|
+
}
|
|
276
|
+
return buffer;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Decodes a request body for JSON parsing and logs.
|
|
280
|
+
*
|
|
281
|
+
* @param headers - Incoming HTTP headers (for `Content-Encoding`).
|
|
282
|
+
* @param buffer - Raw request body bytes.
|
|
283
|
+
* @returns UTF-8 text after decompression, or empty string when buffer is empty.
|
|
284
|
+
*/
|
|
285
|
+
function decodeRequestBodyText(headers, buffer) {
|
|
286
|
+
if (!buffer.length) {
|
|
287
|
+
return "";
|
|
288
|
+
}
|
|
289
|
+
const decoded = decompressRequestBodyBuffer(buffer, getRequestContentEncoding(headers));
|
|
290
|
+
return decoded.toString("utf-8");
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Parses a request body for structured logging.
|
|
294
|
+
*
|
|
295
|
+
* Binary or compressed bodies are decompressed when possible; otherwise a short
|
|
296
|
+
* metadata placeholder is stored instead of corrupted binary text.
|
|
297
|
+
*
|
|
298
|
+
* @param headers - Incoming HTTP headers.
|
|
299
|
+
* @param buffer - Raw request body bytes.
|
|
300
|
+
*/
|
|
301
|
+
function parseRequestBodyForLog(headers, buffer) {
|
|
302
|
+
if (!buffer.length) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
const encoding = getRequestContentEncoding(headers);
|
|
306
|
+
const decoded = decompressRequestBodyBuffer(buffer, encoding);
|
|
307
|
+
if (encoding === "zstd" && decoded === buffer && !zlib.zstdDecompressSync) {
|
|
308
|
+
return { _note: "zstd compressed request body", byteLength: buffer.length };
|
|
309
|
+
}
|
|
310
|
+
const text = decoded.toString("utf-8");
|
|
311
|
+
try {
|
|
312
|
+
return JSON.parse(text);
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
if (encoding && decoded !== buffer) {
|
|
316
|
+
return text;
|
|
317
|
+
}
|
|
318
|
+
if (encoding) {
|
|
319
|
+
return { _note: `${encoding} compressed request body`, byteLength: buffer.length };
|
|
320
|
+
}
|
|
321
|
+
return text;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Extracts the `model` field from a JSON request body.
|
|
326
|
+
*
|
|
327
|
+
* @param requestBody - Raw request body string.
|
|
328
|
+
* @returns Model identifier or `null` when absent or unparseable.
|
|
329
|
+
*/
|
|
330
|
+
function extractModelFromBody(requestBody) {
|
|
331
|
+
if (!requestBody) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
const parsed = JSON.parse(requestBody);
|
|
336
|
+
return typeof parsed.model === "string" ? parsed.model : null;
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Resolves upstream target based on the `model` field in the request body.
|
|
344
|
+
*
|
|
345
|
+
* Used by OpenCode when `modelRoutes` is configured. Returns `null` when the
|
|
346
|
+
* model cannot be extracted or is not registered in the route table.
|
|
347
|
+
*
|
|
348
|
+
* @param reqUrl - Incoming request URL (path may be normalized for the provider).
|
|
349
|
+
* @param requestBody - Raw JSON request body.
|
|
350
|
+
* @param modelRoutes - Model name → route metadata map.
|
|
351
|
+
* @param fallback - Default upstream when model routing fails.
|
|
352
|
+
* @returns Routed target with optional {@link ModelRoute} metadata, or `null`.
|
|
353
|
+
*/
|
|
354
|
+
function resolveModelTarget(reqUrl, requestBody, modelRoutes, fallback) {
|
|
355
|
+
const model = extractModelFromBody(requestBody);
|
|
356
|
+
if (!model) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
const modelRoute = (0, proxy_routing_1.resolveModelRoute)(model, modelRoutes);
|
|
360
|
+
if (!modelRoute) {
|
|
361
|
+
(0, cli_common_1.traceDebug)(`opencode-trace: unknown model "${model}", cannot route request`);
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
const target = parseTargetBaseUrl(modelRoute.upstreamBaseUrl);
|
|
365
|
+
const [rawPath, query = ""] = (reqUrl || "/").split("?");
|
|
366
|
+
// Adjust path for provider-specific API conventions (e.g. /messages → /v1/messages).
|
|
367
|
+
const urlPath = (0, proxy_routing_1.normalizeUpstreamPath)(rawPath, modelRoute, target.pathPrefix);
|
|
368
|
+
const upstreamDisplayPath = `${target.pathPrefix}${urlPath}${query ? `?${query}` : ""}`;
|
|
369
|
+
(0, cli_common_1.traceDebug)(`opencode-trace: ${reqUrl || "/"} | model=${model} → provider=${modelRoute.providerId} (${modelRoute.apiFormat}) → ${modelRoute.upstreamBaseUrl}`);
|
|
370
|
+
return {
|
|
371
|
+
...target,
|
|
372
|
+
upstreamDisplayPath,
|
|
373
|
+
modelRoute,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* HTTP reverse proxy that intercepts, logs, and forwards LLM API traffic.
|
|
378
|
+
*
|
|
379
|
+
* Supports three routing modes:
|
|
380
|
+
* - **Default**: single `targetBaseUrl` (Claude Code V2+).
|
|
381
|
+
* - **OpenCode**: `modelRoutes` and/or `routes` for multi-provider routing.
|
|
382
|
+
* - **Codex**: `providerRoutes` for path-based upstream selection.
|
|
383
|
+
*/
|
|
384
|
+
class ReverseProxyServer {
|
|
385
|
+
/**
|
|
386
|
+
* Creates a new reverse proxy instance and initializes empty log files.
|
|
387
|
+
*
|
|
388
|
+
* @param config - Proxy and logging configuration.
|
|
389
|
+
*/
|
|
390
|
+
constructor(config = {}) {
|
|
391
|
+
/** Active Node HTTP server, or `null` after {@link stop}. */
|
|
392
|
+
this.server = null;
|
|
393
|
+
/** In-memory aggregate of all logged request/response pairs. */
|
|
394
|
+
this.pairs = [];
|
|
395
|
+
/** Guards against duplicate cleanup when {@link stop} is called more than once. */
|
|
396
|
+
this.stopped = false;
|
|
397
|
+
const target = parseTargetBaseUrl(config.targetBaseUrl);
|
|
398
|
+
this.targetProtocol = target.protocol;
|
|
399
|
+
this.targetHost = target.targetHost;
|
|
400
|
+
this.targetPort = target.targetPort;
|
|
401
|
+
this.pathPrefix = target.pathPrefix;
|
|
402
|
+
this.useHttps = target.protocol === "https:";
|
|
403
|
+
this.tool = config.tool;
|
|
404
|
+
this.routes = config.routes;
|
|
405
|
+
this.modelRoutes = config.modelRoutes;
|
|
406
|
+
this.providerRoutes = config.providerRoutes;
|
|
407
|
+
this.config = {
|
|
408
|
+
port: config.port || 0,
|
|
409
|
+
logDirectory: config.logDirectory || ".claude-trace",
|
|
410
|
+
logBaseName: config.logBaseName || "",
|
|
411
|
+
includeAllRequests: config.includeAllRequests || false,
|
|
412
|
+
openBrowser: config.openBrowser || false,
|
|
413
|
+
logSensitiveHeaders: config.logSensitiveHeaders || false,
|
|
414
|
+
};
|
|
415
|
+
if (!fs.existsSync(this.config.logDirectory)) {
|
|
416
|
+
fs.mkdirSync(this.config.logDirectory, { recursive: true });
|
|
417
|
+
}
|
|
418
|
+
const fileBaseName = this.config.logBaseName ||
|
|
419
|
+
`log-${new Date().toISOString().replace(/[:.]/g, "-").replace("T", "-").slice(0, -5)}`;
|
|
420
|
+
this.logFile = path.join(this.config.logDirectory, `${fileBaseName}.jsonl`);
|
|
421
|
+
this.jsonFile = path.join(this.config.logDirectory, `${fileBaseName}.json`);
|
|
422
|
+
this.htmlFile = path.join(this.config.logDirectory, `${fileBaseName}.html`);
|
|
423
|
+
fs.writeFileSync(this.logFile, "");
|
|
424
|
+
fs.writeFileSync(this.jsonFile, "[]");
|
|
425
|
+
this.htmlGenerator = new html_generator_1.HTMLGenerator();
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Converts Node HTTP headers to a flat string map, optionally redacting secrets.
|
|
429
|
+
*
|
|
430
|
+
* Sensitive keys (authorization, cookies, etc.) are truncated to show only
|
|
431
|
+
* prefix/suffix unless `logSensitiveHeaders` is enabled.
|
|
432
|
+
*
|
|
433
|
+
* @param headers - Raw Node `IncomingHttpHeaders` from request or response.
|
|
434
|
+
* @returns Flat string map safe for JSON serialization.
|
|
435
|
+
*/
|
|
436
|
+
processHeaders(headers) {
|
|
437
|
+
return (0, header_redaction_1.processHeaders)(headers, this.config.logSensitiveHeaders);
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Parses a Server-Sent Events (SSE) response body into structured events.
|
|
441
|
+
*
|
|
442
|
+
* Handles both Anthropic-style (`event:` + `data:`) and OpenAI-style
|
|
443
|
+
* (type embedded in JSON data) event formats.
|
|
444
|
+
*
|
|
445
|
+
* @param body - Decompressed SSE text from the upstream response.
|
|
446
|
+
* @returns Ordered list of parsed events with ISO timestamps.
|
|
447
|
+
*/
|
|
448
|
+
parseSSEEvents(body) {
|
|
449
|
+
const events = [];
|
|
450
|
+
const lines = body.split("\n");
|
|
451
|
+
let currentEvent = "";
|
|
452
|
+
for (const line of lines) {
|
|
453
|
+
if (line.startsWith("event: ")) {
|
|
454
|
+
currentEvent = line.substring(7).trim();
|
|
455
|
+
}
|
|
456
|
+
else if (line.startsWith("data: ")) {
|
|
457
|
+
const data = line.substring(6).trim();
|
|
458
|
+
if (data === "[DONE]")
|
|
459
|
+
break;
|
|
460
|
+
try {
|
|
461
|
+
const parsed = JSON.parse(data);
|
|
462
|
+
// Fall back to JSON `type` field when no explicit `event:` line was seen.
|
|
463
|
+
const eventType = currentEvent ||
|
|
464
|
+
(typeof parsed === "object" && parsed !== null && "type" in parsed
|
|
465
|
+
? String(parsed.type)
|
|
466
|
+
: "unknown");
|
|
467
|
+
events.push({
|
|
468
|
+
event: eventType,
|
|
469
|
+
data: parsed,
|
|
470
|
+
timestamp: new Date().toISOString(),
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
// Skip unparseable SSE data lines rather than failing the whole response log.
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return events;
|
|
479
|
+
}
|
|
480
|
+
/** Appends a request/response pair to JSONL and rewrites the aggregate JSON file. */
|
|
481
|
+
writePairToLog(pair) {
|
|
482
|
+
try {
|
|
483
|
+
const jsonLine = JSON.stringify(pair) + "\n";
|
|
484
|
+
fs.appendFileSync(this.logFile, jsonLine);
|
|
485
|
+
fs.writeFileSync(this.jsonFile, JSON.stringify(this.pairs, null, 2));
|
|
486
|
+
}
|
|
487
|
+
catch (err) {
|
|
488
|
+
(0, cli_common_1.traceRuntimeError)(`Failed to write log: ${err}`, this.config.logDirectory);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
/** Regenerates the self-contained HTML report from all logged pairs. */
|
|
492
|
+
async generateHTML() {
|
|
493
|
+
try {
|
|
494
|
+
await this.htmlGenerator.generateHTML(this.pairs, this.htmlFile, {
|
|
495
|
+
timestamp: new Date().toISOString().replace("T", " ").slice(0, -5),
|
|
496
|
+
includeAllRequests: this.config.includeAllRequests,
|
|
497
|
+
tool: this.tool,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
catch (err) {
|
|
501
|
+
(0, cli_common_1.traceRuntimeError)(`Failed to generate HTML: ${err}`, this.config.logDirectory);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Starts the proxy server on 127.0.0.1.
|
|
506
|
+
*
|
|
507
|
+
* @returns Resolved listen port and base URL (`http://127.0.0.1:{port}`).
|
|
508
|
+
*/
|
|
509
|
+
async start() {
|
|
510
|
+
return new Promise((resolve, reject) => {
|
|
511
|
+
const httpServer = http.createServer((req, res) => {
|
|
512
|
+
this.handleRequest(req, res);
|
|
513
|
+
});
|
|
514
|
+
this.server = httpServer;
|
|
515
|
+
httpServer.on("error", (err) => {
|
|
516
|
+
reject(err);
|
|
517
|
+
});
|
|
518
|
+
// Bind to loopback only — the proxy must not be reachable from the network.
|
|
519
|
+
httpServer.listen(this.config.port, "127.0.0.1", () => {
|
|
520
|
+
const address = httpServer.address();
|
|
521
|
+
if (address && typeof address === "object") {
|
|
522
|
+
const port = address.port;
|
|
523
|
+
const url = `http://127.0.0.1:${port}`;
|
|
524
|
+
console.error(`Logs will be written to:`);
|
|
525
|
+
console.error(` JSONL: ${path.resolve(this.logFile)}`);
|
|
526
|
+
console.error(` JSON: ${path.resolve(this.jsonFile)}`);
|
|
527
|
+
console.error(` HTML: ${path.resolve(this.htmlFile)}`);
|
|
528
|
+
resolve({ port, url });
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
reject(new Error("Failed to get server address"));
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Handles a single incoming HTTP request: route, forward, stream response, and log.
|
|
538
|
+
*
|
|
539
|
+
* Routing priority:
|
|
540
|
+
* 1. Model-based (OpenCode `modelRoutes`) — fails with 502 if model is unresolvable.
|
|
541
|
+
* 2. Provider-prefixed (`/p/{id}/...`) or default target.
|
|
542
|
+
* 3. Codex path-based (`providerRoutes`) overrides when `tool === "codex"`.
|
|
543
|
+
*
|
|
544
|
+
* @param req - Incoming client request from the coding agent.
|
|
545
|
+
* @param res - Response object streamed back to the client.
|
|
546
|
+
*/
|
|
547
|
+
handleRequest(req, res) {
|
|
548
|
+
const requestTimestamp = Date.now();
|
|
549
|
+
const requestBodyChunks = [];
|
|
550
|
+
req.on("data", (chunk) => {
|
|
551
|
+
requestBodyChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
552
|
+
});
|
|
553
|
+
req.on("end", () => {
|
|
554
|
+
const requestBodyBuffer = Buffer.concat(requestBodyChunks);
|
|
555
|
+
const requestBodyText = decodeRequestBodyText(req.headers, requestBodyBuffer);
|
|
556
|
+
const fallback = {
|
|
557
|
+
protocol: this.targetProtocol,
|
|
558
|
+
targetHost: this.targetHost,
|
|
559
|
+
targetPort: this.targetPort,
|
|
560
|
+
pathPrefix: this.pathPrefix,
|
|
561
|
+
};
|
|
562
|
+
const modelTarget = this.modelRoutes && Object.keys(this.modelRoutes).length > 0
|
|
563
|
+
? resolveModelTarget(req.url, requestBodyText, this.modelRoutes, fallback)
|
|
564
|
+
: null;
|
|
565
|
+
// OpenCode requires every request to resolve to a known model when modelRoutes is set.
|
|
566
|
+
if (this.modelRoutes && Object.keys(this.modelRoutes).length > 0 && !modelTarget) {
|
|
567
|
+
res.writeHead(502);
|
|
568
|
+
res.end("opencode-trace: could not resolve model from request body");
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
let routeTarget = modelTarget ??
|
|
572
|
+
resolveRouteTarget(req.url, this.routes, fallback);
|
|
573
|
+
// Codex uses path-prefix routing instead of model-based routing.
|
|
574
|
+
if (this.tool === "codex" && this.providerRoutes?.length) {
|
|
575
|
+
routeTarget = (0, codex_routing_1.resolveCodexRouteTarget)(req.url, this.providerRoutes, fallback);
|
|
576
|
+
}
|
|
577
|
+
const upstreamPath = routeTarget.upstreamDisplayPath;
|
|
578
|
+
const upstreamUrl = `${routeTarget.protocol}//${routeTarget.targetHost}${upstreamPath}`;
|
|
579
|
+
const useHttps = routeTarget.protocol === "https:";
|
|
580
|
+
const upstreamHeaders = {
|
|
581
|
+
...req.headers,
|
|
582
|
+
// Override Host so upstream TLS SNI and virtual-host routing match the target.
|
|
583
|
+
host: routeTarget.targetHost,
|
|
584
|
+
};
|
|
585
|
+
const acceptEncodingKey = Object.keys(upstreamHeaders).find((key) => key.toLowerCase() === "accept-encoding");
|
|
586
|
+
if (acceptEncodingKey) {
|
|
587
|
+
const normalized = normalizeAcceptEncoding(upstreamHeaders[acceptEncodingKey]);
|
|
588
|
+
if (normalized)
|
|
589
|
+
upstreamHeaders[acceptEncodingKey] = normalized;
|
|
590
|
+
else
|
|
591
|
+
delete upstreamHeaders[acceptEncodingKey];
|
|
592
|
+
}
|
|
593
|
+
const options = {
|
|
594
|
+
hostname: routeTarget.targetHost,
|
|
595
|
+
port: routeTarget.targetPort,
|
|
596
|
+
path: upstreamPath,
|
|
597
|
+
method: req.method,
|
|
598
|
+
headers: upstreamHeaders,
|
|
599
|
+
agent: createOutboundAgent(upstreamUrl),
|
|
600
|
+
};
|
|
601
|
+
const requestModule = useHttps ? https : http;
|
|
602
|
+
const proxyReq = requestModule.request(options, (proxyRes) => {
|
|
603
|
+
const responseTimestamp = Date.now();
|
|
604
|
+
const responseChunks = [];
|
|
605
|
+
// Stream response to client immediately while buffering for logging.
|
|
606
|
+
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
|
607
|
+
proxyRes.on("data", (chunk) => {
|
|
608
|
+
responseChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
609
|
+
res.write(chunk);
|
|
610
|
+
});
|
|
611
|
+
proxyRes.on("end", () => {
|
|
612
|
+
void this.logResponse(req, proxyRes, requestTimestamp, responseTimestamp, requestBodyBuffer, responseChunks, upstreamUrl, modelTarget !== null, modelTarget?.modelRoute);
|
|
613
|
+
res.end();
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
proxyReq.on("error", (err) => {
|
|
617
|
+
(0, cli_common_1.traceRuntimeError)(`Proxy request error: ${err.message} (upstream: ${upstreamUrl})`, this.config.logDirectory);
|
|
618
|
+
res.writeHead(502);
|
|
619
|
+
res.end(`Proxy error: ${err.message}`);
|
|
620
|
+
});
|
|
621
|
+
if (requestBodyBuffer.length > 0) {
|
|
622
|
+
proxyReq.write(requestBodyBuffer);
|
|
623
|
+
}
|
|
624
|
+
proxyReq.end();
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Parses, normalizes, and persists a completed request/response exchange.
|
|
629
|
+
*
|
|
630
|
+
* Decompresses gzip/brotli/deflate bodies before parsing. Converts OpenAI and
|
|
631
|
+
* Anthropic streaming responses into structured message objects when possible.
|
|
632
|
+
*
|
|
633
|
+
* @param req - Original client request (used for URL and method).
|
|
634
|
+
* @param proxyRes - Upstream response headers and status.
|
|
635
|
+
* @param requestTimestamp - Client request start time in milliseconds.
|
|
636
|
+
* @param responseTimestamp - Upstream response completion time in milliseconds.
|
|
637
|
+
* @param requestBody - Buffered raw request body bytes.
|
|
638
|
+
* @param responseChunks - Buffered raw response body chunks.
|
|
639
|
+
* @param upstreamUrl - Fully resolved upstream URL for logging.
|
|
640
|
+
* @param modelRouted - Whether model-based routing was used (forces logging).
|
|
641
|
+
* @param modelRoute - Resolved model route metadata, if any.
|
|
642
|
+
*/
|
|
643
|
+
async logResponse(req, proxyRes, requestTimestamp, responseTimestamp, requestBody, responseChunks, upstreamUrl, modelRouted = false, modelRoute) {
|
|
644
|
+
const shouldLog = this.config.includeAllRequests || isLlmApiPath(req.url) || modelRouted;
|
|
645
|
+
if (!shouldLog) {
|
|
646
|
+
(0, cli_common_1.traceDebug)(`opencode-trace: skipped logging for ${req.method} ${req.url}`);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
try {
|
|
650
|
+
const parsedRequestBody = parseRequestBodyForLog(req.headers, requestBody);
|
|
651
|
+
const rawBuffer = Buffer.concat(responseChunks);
|
|
652
|
+
const contentEncoding = (proxyRes.headers["content-encoding"] || "").toLowerCase();
|
|
653
|
+
const responseBody = decodeResponseBody(rawBuffer, contentEncoding);
|
|
654
|
+
const apiFormat = this.resolveResponseApiFormat(req.url, upstreamUrl, modelRoute);
|
|
655
|
+
const parsedResponseBody = this.parseResponseBody(proxyRes, responseBody, apiFormat, parsedRequestBody);
|
|
656
|
+
const pair = {
|
|
657
|
+
request: {
|
|
658
|
+
timestamp: requestTimestamp / 1000,
|
|
659
|
+
method: req.method || "GET",
|
|
660
|
+
url: upstreamUrl,
|
|
661
|
+
headers: this.processHeaders(req.headers),
|
|
662
|
+
body: parsedRequestBody,
|
|
663
|
+
},
|
|
664
|
+
response: {
|
|
665
|
+
timestamp: responseTimestamp / 1000,
|
|
666
|
+
status_code: proxyRes.statusCode || 0,
|
|
667
|
+
headers: this.processHeaders(proxyRes.headers),
|
|
668
|
+
api_format: apiFormat !== "unknown" ? apiFormat : undefined,
|
|
669
|
+
...parsedResponseBody,
|
|
670
|
+
},
|
|
671
|
+
logged_at: new Date().toISOString(),
|
|
672
|
+
};
|
|
673
|
+
this.pairs.push(pair);
|
|
674
|
+
this.writePairToLog(pair);
|
|
675
|
+
await this.generateHTML();
|
|
676
|
+
}
|
|
677
|
+
catch (err) {
|
|
678
|
+
(0, cli_common_1.traceRuntimeError)(`opencode-trace: failed to log response: ${err}`, this.config.logDirectory);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Determines the API format for response parsing.
|
|
683
|
+
*
|
|
684
|
+
* Priority: explicit model route → request path heuristics → upstream URL heuristics.
|
|
685
|
+
*
|
|
686
|
+
* @param reqUrl - Client-facing request URL path.
|
|
687
|
+
* @param upstreamUrl - Resolved upstream URL after routing.
|
|
688
|
+
* @param modelRoute - Optional model route with explicit `apiFormat`.
|
|
689
|
+
* @returns Best-effort API format for {@link parseResponseBody}.
|
|
690
|
+
*/
|
|
691
|
+
resolveResponseApiFormat(reqUrl, upstreamUrl, modelRoute) {
|
|
692
|
+
if (modelRoute?.apiFormat && modelRoute.apiFormat !== "unknown") {
|
|
693
|
+
return modelRoute.apiFormat;
|
|
694
|
+
}
|
|
695
|
+
const fromReq = (0, proxy_routing_1.inferApiFormatFromPath)(reqUrl);
|
|
696
|
+
if (fromReq !== "unknown") {
|
|
697
|
+
return fromReq;
|
|
698
|
+
}
|
|
699
|
+
return (0, api_format_1.inferApiFormatFromUrl)(upstreamUrl);
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Parses a response body into structured log fields based on content type and API format.
|
|
703
|
+
*
|
|
704
|
+
* For SSE streams, attempts to reconstruct complete message objects from event
|
|
705
|
+
* sequences; falls back to raw body text when parsing fails.
|
|
706
|
+
*
|
|
707
|
+
* @param proxyRes - Upstream response (for `content-type` header).
|
|
708
|
+
* @param responseBody - Decompressed response text.
|
|
709
|
+
* @param apiFormat - Resolved format from {@link resolveResponseApiFormat}.
|
|
710
|
+
* @param requestBody - Parsed request body (used to extract model name).
|
|
711
|
+
* @returns Subset of {@link RawPair.response} fields (`body`, `body_raw`, `events`).
|
|
712
|
+
*/
|
|
713
|
+
parseResponseBody(proxyRes, responseBody, apiFormat, requestBody) {
|
|
714
|
+
const contentType = proxyRes.headers["content-type"] || "";
|
|
715
|
+
const model = requestBody && typeof requestBody === "object" && "model" in requestBody
|
|
716
|
+
? String(requestBody.model)
|
|
717
|
+
: "unknown";
|
|
718
|
+
try {
|
|
719
|
+
if (contentType.includes("application/json")) {
|
|
720
|
+
const parsed = JSON.parse(responseBody);
|
|
721
|
+
if (parsed && typeof parsed === "object" && "error" in parsed) {
|
|
722
|
+
return { body: parsed };
|
|
723
|
+
}
|
|
724
|
+
if (apiFormat === "openai" || ("choices" in parsed)) {
|
|
725
|
+
return { body: (0, openai_adapter_1.parseOpenAIChatCompletionBody)(parsed, model) };
|
|
726
|
+
}
|
|
727
|
+
if (apiFormat === "openai-responses" || ("output" in parsed)) {
|
|
728
|
+
return { body: (0, openai_adapter_1.parseOpenAIResponsesBody)(parsed, model) };
|
|
729
|
+
}
|
|
730
|
+
return { body: parsed };
|
|
731
|
+
}
|
|
732
|
+
if (contentType.includes("text/event-stream")) {
|
|
733
|
+
const events = this.parseSSEEvents(responseBody);
|
|
734
|
+
if (apiFormat === "openai") {
|
|
735
|
+
try {
|
|
736
|
+
const completion = (0, openai_adapter_1.buildOpenAIChatCompletionFromSSE)(responseBody, model);
|
|
737
|
+
return { body: (0, openai_adapter_1.parseOpenAIChatCompletionBody)(completion, model), events };
|
|
738
|
+
}
|
|
739
|
+
catch {
|
|
740
|
+
return { body_raw: responseBody, events };
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (apiFormat === "openai-responses") {
|
|
744
|
+
try {
|
|
745
|
+
const responsesBody = (0, openai_adapter_1.buildOpenAIResponsesFromSSE)(responseBody, model);
|
|
746
|
+
return { body: (0, openai_adapter_1.parseOpenAIResponsesBody)(responsesBody, model), events };
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
return { body_raw: responseBody, events };
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
// Default SSE path: Anthropic Messages API streaming format.
|
|
753
|
+
const processor = new shared_conversation_processor_1.SharedConversationProcessor();
|
|
754
|
+
try {
|
|
755
|
+
const message = processor.parseStreamingResponse(responseBody);
|
|
756
|
+
return { body: message, events };
|
|
757
|
+
}
|
|
758
|
+
catch {
|
|
759
|
+
return { body_raw: responseBody, events };
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return { body_raw: responseBody };
|
|
763
|
+
}
|
|
764
|
+
catch {
|
|
765
|
+
return { body_raw: responseBody };
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Stops the proxy server, prints summary stats, and optionally opens the HTML report.
|
|
770
|
+
*/
|
|
771
|
+
stop() {
|
|
772
|
+
if (this.stopped) {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
this.stopped = true;
|
|
776
|
+
if (this.server) {
|
|
777
|
+
this.server.close();
|
|
778
|
+
this.server = null;
|
|
779
|
+
}
|
|
780
|
+
console.error(`Logged ${this.pairs.length} request/response pairs`);
|
|
781
|
+
if (this.config.openBrowser && fs.existsSync(this.htmlFile)) {
|
|
782
|
+
this.openHtmlInBrowser(this.htmlFile);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
/** Returns the absolute path to the generated HTML report file. */
|
|
786
|
+
getHtmlFile() {
|
|
787
|
+
return this.htmlFile;
|
|
788
|
+
}
|
|
789
|
+
/** Opens an HTML file in the platform default browser (Windows/macOS/Linux). */
|
|
790
|
+
openHtmlInBrowser(htmlFile) {
|
|
791
|
+
try {
|
|
792
|
+
if (process.platform === "win32") {
|
|
793
|
+
(0, child_process_1.spawn)("cmd", ["/c", "start", "", htmlFile], { detached: true, stdio: "ignore" }).unref();
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
const cmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
797
|
+
(0, child_process_1.spawn)(cmd, [htmlFile], { detached: true, stdio: "ignore" }).unref();
|
|
798
|
+
}
|
|
799
|
+
console.error(`Opening ${htmlFile} in browser`);
|
|
800
|
+
}
|
|
801
|
+
catch (err) {
|
|
802
|
+
console.error(`Failed to open browser: ${err}`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
exports.ReverseProxyServer = ReverseProxyServer;
|
|
807
|
+
//# sourceMappingURL=reverse-proxy.js.map
|