@juspay/neurolink 12.13.0 → 12.14.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.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Model IDs the proxy advertises when no routing config narrows the list.
3
3
  *
4
- * Two consumers need the same answer and must not drift apart:
4
+ * Three consumers need the same answer and must not drift apart:
5
5
  *
6
6
  * - `proxyTranslationEngine` serves them from `GET /v1/models`.
7
7
  * - The OpenCode client configurator writes them into `provider.neurolink.
@@ -9,6 +9,12 @@
9
9
  * It does not call `/v1/models`, so an empty map means every model id is
10
10
  * unknown and `opencode run` fails with `ProviderModelNotFoundError`
11
11
  * before a request is ever made.
12
+ * - The Grok Build configurator writes the same ids (plus any
13
+ * `routing.model-mappings` in the active proxy config — default
14
+ * `~/.neurolink/proxy-config.yaml`, or the path passed to `--config`)
15
+ * into `~/.grok/config.toml` as `[model.<id>]` entries, with
16
+ * `api_backend` and `context_window` so Grok's compaction matches the
17
+ * upstream. Built-in grok-* ids are skipped.
12
18
  *
13
19
  * Format matches the IDs used throughout `src/lib/models/` and
14
20
  * `src/lib/constants/` (e.g. `claude-3-5-haiku-20241022`, not
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Model IDs the proxy advertises when no routing config narrows the list.
3
3
  *
4
- * Two consumers need the same answer and must not drift apart:
4
+ * Three consumers need the same answer and must not drift apart:
5
5
  *
6
6
  * - `proxyTranslationEngine` serves them from `GET /v1/models`.
7
7
  * - The OpenCode client configurator writes them into `provider.neurolink.
@@ -9,6 +9,12 @@
9
9
  * It does not call `/v1/models`, so an empty map means every model id is
10
10
  * unknown and `opencode run` fails with `ProviderModelNotFoundError`
11
11
  * before a request is ever made.
12
+ * - The Grok Build configurator writes the same ids (plus any
13
+ * `routing.model-mappings` in the active proxy config — default
14
+ * `~/.neurolink/proxy-config.yaml`, or the path passed to `--config`)
15
+ * into `~/.grok/config.toml` as `[model.<id>]` entries, with
16
+ * `api_backend` and `context_window` so Grok's compaction matches the
17
+ * upstream. Built-in grok-* ids are skipped.
12
18
  *
13
19
  * Format matches the IDs used throughout `src/lib/models/` and
14
20
  * `src/lib/constants/` (e.g. `claude-3-5-haiku-20241022`, not
@@ -19,4 +19,4 @@ export declare function redactProxyHeadersForLogging(headers: Record<string, str
19
19
  * Process a capture in the worker and retain redacted output when artifact
20
20
  * persistence fails.
21
21
  */
22
- export declare function processProxyBodyCapture(entry: ProxyBodyCaptureEntry, logDir: string): Promise<ProcessedProxyBodyCapture>;
22
+ export declare function processProxyBodyCapture(entry: ProxyBodyCaptureEntry, logDir: string | null): Promise<ProcessedProxyBodyCapture>;
@@ -203,6 +203,16 @@ export function redactProxyHeadersForLogging(headers) {
203
203
  export async function processProxyBodyCapture(entry, logDir) {
204
204
  const headers = redactHeaders(entry.headers);
205
205
  const prepared = prepareRedactedBody(entry.body);
206
+ if (logDir === null) {
207
+ return {
208
+ headers,
209
+ stored: {
210
+ redactedBody: prepared.value,
211
+ redactedBodyBytes: prepared.bytes,
212
+ bodyTruncated: prepared.truncated,
213
+ },
214
+ };
215
+ }
206
216
  let stored;
207
217
  try {
208
218
  stored = await writeBodyArtifact(logDir, entry, headers, prepared.value, prepared.truncated);
@@ -1,7 +1,7 @@
1
1
  import type { ProcessedProxyBodyCapture, ProxyBodyCaptureEntry, ProxyBodyCaptureWorkerSnapshot } from "../types/index.js";
2
2
  export declare const PROXY_BODY_CAPTURE_DEADLINE_MS = 20000;
3
3
  /** Bounded bulk capture. Failures are indexed; never fall back to blocking work. */
4
- export declare function captureProxyBody(entry: ProxyBodyCaptureEntry, logDir: string, consume: (result: ProcessedProxyBodyCapture) => Promise<void>): Promise<void>;
4
+ export declare function captureProxyBody(entry: ProxyBodyCaptureEntry, logDir: string | null, consume: (result: ProcessedProxyBodyCapture) => Promise<void>): Promise<void>;
5
5
  /**
6
6
  * Return independent counters for processing, rejection, failure, and
7
7
  * retained publication work.
@@ -39,6 +39,9 @@ const CLIENT_PREFIXES = [
39
39
  // different token, and it is left unmapped until someone measures it rather
40
40
  // than pattern-matched on a guess.
41
41
  ["codex_exec/", "codex"],
42
+ // "grok-shell/1.0.30 (macos; aarch64)" — measured 2026-09-12 on
43
+ // POST /v1/messages from Grok Build 1.0.30.
44
+ ["grok-shell/", "grok"],
42
45
  ];
43
46
  /**
44
47
  * Deliberately NOT mapped, and why each was ruled out.
@@ -1,3 +1,4 @@
1
+ import { isProxyOtelOnly } from "./otelLogSink.js";
1
2
  import { Worker } from "node:worker_threads";
2
3
  import { withTimeout } from "../utils/async/withTimeout.js";
3
4
  import { logger } from "../utils/logger.js";
@@ -9,6 +10,9 @@ const WORKER_TERMINATION_TIMEOUT_MS = 5_000;
9
10
  * block active streams. Concurrent runs are coalesced into the active scan.
10
11
  */
11
12
  export function startProxyLogCleanupScheduler(params) {
13
+ if (isProxyOtelOnly()) {
14
+ return { trigger: () => false, stop: async () => { } };
15
+ }
12
16
  const maxAgeDays = params.maxAgeDays ?? 7;
13
17
  const maxSizeMb = params.maxSizeMb ?? 500;
14
18
  let activeWorker;
@@ -0,0 +1,34 @@
1
+ import { LoggerProvider } from "@opentelemetry/sdk-logs";
2
+ /** Explicit opt-in; configuration never silently falls back to file logging. */
3
+ export declare function isProxyOtelOnly(): boolean;
4
+ /** Initialize a log-only provider in every proxy process, including the supervisor. */
5
+ export declare function initializeProxyOtelLogs(role?: string): LoggerProvider | undefined;
6
+ /** Structured evidence without final-request dashboard fields on auxiliary events. */
7
+ export declare function emitProxyOtelEvent(kind: string, record: Record<string, unknown>): void;
8
+ /** Capture application console diagnostics only inside proxy service processes. */
9
+ export declare function routeProxyConsoleToOtel(): void;
10
+ /** Counters acknowledge collector transport only, never backend persistence. */
11
+ export declare function getProxyOtelLogSnapshot(): {
12
+ mode: string;
13
+ initialized: boolean;
14
+ deliveryGuarantee: string;
15
+ invalidRecords: number;
16
+ queues: {
17
+ attempted: number;
18
+ submitted: number;
19
+ transportAcknowledged: number;
20
+ exportUnconfirmed: number;
21
+ dropped: number;
22
+ outstanding: number;
23
+ lastAcknowledgedAt: string | undefined;
24
+ lastFailureAt: string | undefined;
25
+ kind: string;
26
+ capacity: number;
27
+ }[];
28
+ };
29
+ /** Bounded provider flush belongs after final request and lifecycle publication. */
30
+ export declare function flushProxyOtelLogs(): Promise<void>;
31
+ /** Release this process's exporter and restore console ownership. */
32
+ export declare function shutdownProxyOtelLogs(): Promise<void>;
33
+ /** Flush short-lived proxy command diagnostics on every normal return or exception. */
34
+ export declare function withProxyOtelLogShutdown<TArg>(handler: (arg: TArg) => Promise<void>): (arg: TArg) => Promise<void>;
@@ -0,0 +1,254 @@
1
+ /* eslint-disable no-console -- This proxy-only sink replaces console methods with OTLP emission. */
2
+ import { inspect } from "node:util";
3
+ import { SeverityNumber } from "@opentelemetry/api-logs";
4
+ import { ExportResultCode } from "@opentelemetry/core";
5
+ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
6
+ import { resourceFromAttributes } from "@opentelemetry/resources";
7
+ import { BatchLogRecordProcessor, LoggerProvider, } from "@opentelemetry/sdk-logs";
8
+ import { sanitizeForLog } from "../utils/logSanitize.js";
9
+ let provider;
10
+ let restoreConsole;
11
+ const queues = [];
12
+ /** Explicit opt-in; configuration never silently falls back to file logging. */
13
+ export function isProxyOtelOnly() {
14
+ return process.env.NEUROLINK_PROXY_LOG_SINK === "otel";
15
+ }
16
+ /** Reserve capacity including exports in flight, independently for metadata and bodies. */
17
+ function createTrackedProcessor(url, capacity) {
18
+ const state = {
19
+ attempted: 0,
20
+ submitted: 0,
21
+ transportAcknowledged: 0,
22
+ exportUnconfirmed: 0,
23
+ dropped: 0,
24
+ outstanding: 0,
25
+ lastAcknowledgedAt: undefined,
26
+ lastFailureAt: undefined,
27
+ };
28
+ const transport = new OTLPLogExporter({ url, timeoutMillis: 5000 });
29
+ const exporter = {
30
+ export(records, callback) {
31
+ let settled = false;
32
+ const settle = (result) => {
33
+ if (settled) {
34
+ return;
35
+ }
36
+ settled = true;
37
+ state.outstanding -= records.length;
38
+ if (result.code === ExportResultCode.SUCCESS) {
39
+ state.transportAcknowledged += records.length;
40
+ state.lastAcknowledgedAt = new Date().toISOString();
41
+ }
42
+ else {
43
+ state.exportUnconfirmed += records.length;
44
+ state.lastFailureAt = new Date().toISOString();
45
+ }
46
+ callback(result);
47
+ };
48
+ try {
49
+ transport.export(records, settle);
50
+ }
51
+ catch (error) {
52
+ settle({
53
+ code: ExportResultCode.FAILED,
54
+ error: error instanceof Error ? error : new Error(String(error)),
55
+ });
56
+ }
57
+ },
58
+ shutdown: () => transport.shutdown(),
59
+ };
60
+ const batch = new BatchLogRecordProcessor(exporter, {
61
+ maxQueueSize: capacity,
62
+ maxExportBatchSize: 64,
63
+ scheduledDelayMillis: 1000,
64
+ exportTimeoutMillis: 6000,
65
+ });
66
+ const processor = {
67
+ onEmit(record) {
68
+ state.attempted++;
69
+ if (state.outstanding >= capacity) {
70
+ state.dropped++;
71
+ return;
72
+ }
73
+ state.submitted++;
74
+ state.outstanding++;
75
+ batch.onEmit(record);
76
+ },
77
+ forceFlush: () => batch.forceFlush(),
78
+ shutdown: () => batch.shutdown(),
79
+ };
80
+ return { state, processor, capacity };
81
+ }
82
+ /** Initialize a log-only provider in every proxy process, including the supervisor. */
83
+ export function initializeProxyOtelLogs(role = "worker") {
84
+ if (!isProxyOtelOnly()) {
85
+ return undefined;
86
+ }
87
+ if (provider) {
88
+ return provider;
89
+ }
90
+ const endpoint = process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT ??
91
+ (process.env.OTEL_EXPORTER_OTLP_ENDPOINT
92
+ ? `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT.replace(/\/$/, "")}/v1/logs`
93
+ : undefined);
94
+ if (!endpoint) {
95
+ throw new Error("OTel-only proxy logging requires an OTLP endpoint");
96
+ }
97
+ const url = new URL(endpoint);
98
+ if (!["http:", "https:"].includes(url.protocol)) {
99
+ throw new Error("Proxy OTLP logs endpoint must use HTTP or HTTPS");
100
+ }
101
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
102
+ if (url.protocol === "http:" && !loopback) {
103
+ throw new Error("Proxy OTLP logs require HTTPS for non-loopback collectors");
104
+ }
105
+ const metadata = createTrackedProcessor(endpoint, 2048);
106
+ const bodies = createTrackedProcessor(endpoint, 256);
107
+ queues.push(metadata, bodies);
108
+ provider = new LoggerProvider({
109
+ resource: resourceFromAttributes({
110
+ "service.name": process.env.OTEL_SERVICE_NAME ?? "neurolink-proxy",
111
+ "service.instance.id": `${role}-${process.pid}`,
112
+ "process.pid": process.pid,
113
+ "proxy.process.role": role,
114
+ }),
115
+ processors: [
116
+ {
117
+ onEmit(record, context) {
118
+ (record.attributes?.["proxy.record_kind"] === "body"
119
+ ? bodies
120
+ : metadata).processor.onEmit(record, context);
121
+ },
122
+ forceFlush: async () => {
123
+ await Promise.all(queues.map((q) => q.processor.forceFlush()));
124
+ },
125
+ shutdown: async () => {
126
+ await Promise.all(queues.map((q) => q.processor.shutdown()));
127
+ },
128
+ },
129
+ ],
130
+ });
131
+ return provider;
132
+ }
133
+ /** Structured evidence without final-request dashboard fields on auxiliary events. */
134
+ export function emitProxyOtelEvent(kind, record) {
135
+ if (!isProxyOtelOnly()) {
136
+ return;
137
+ }
138
+ try {
139
+ initializeProxyOtelLogs()
140
+ ?.getLogger("neurolink-proxy-events")
141
+ .emit({
142
+ severityNumber: SeverityNumber.INFO,
143
+ severityText: "INFO",
144
+ body: JSON.stringify(record),
145
+ attributes: {
146
+ "proxy.record_kind": kind,
147
+ "event.name": `proxy.${kind}`,
148
+ ...(typeof record.requestId === "string"
149
+ ? { "request.id": record.requestId }
150
+ : {}),
151
+ ...(typeof record.event === "string"
152
+ ? { "proxy.lifecycle.event": record.event }
153
+ : {}),
154
+ },
155
+ });
156
+ }
157
+ catch {
158
+ // Telemetry must never fail a model request. Invalid records are observable.
159
+ invalidRecords++;
160
+ }
161
+ }
162
+ let invalidRecords = 0;
163
+ /** Capture application console diagnostics only inside proxy service processes. */
164
+ export function routeProxyConsoleToOtel() {
165
+ if (!isProxyOtelOnly() || restoreConsole) {
166
+ return;
167
+ }
168
+ initializeProxyOtelLogs();
169
+ const originals = {
170
+ log: console.log,
171
+ info: console.info,
172
+ warn: console.warn,
173
+ error: console.error,
174
+ debug: console.debug,
175
+ };
176
+ let emitting = false;
177
+ for (const level of Object.keys(originals)) {
178
+ console[level] = (...args) => {
179
+ if (emitting) {
180
+ return;
181
+ }
182
+ emitting = true;
183
+ try {
184
+ const body = args
185
+ .map((value) => typeof value === "string"
186
+ ? value
187
+ : inspect(value, {
188
+ depth: 4,
189
+ maxArrayLength: 30,
190
+ maxStringLength: 16000,
191
+ }))
192
+ .join(" ");
193
+ provider?.getLogger("neurolink-proxy-console").emit({
194
+ body: sanitizeForLog(body, 32000),
195
+ severityText: level.toUpperCase(),
196
+ severityNumber: level === "error"
197
+ ? SeverityNumber.ERROR
198
+ : level === "warn"
199
+ ? SeverityNumber.WARN
200
+ : level === "debug"
201
+ ? SeverityNumber.DEBUG
202
+ : SeverityNumber.INFO,
203
+ attributes: { "proxy.record_kind": "console" },
204
+ });
205
+ }
206
+ catch {
207
+ invalidRecords++;
208
+ }
209
+ finally {
210
+ emitting = false;
211
+ }
212
+ };
213
+ }
214
+ restoreConsole = () => Object.assign(console, originals);
215
+ }
216
+ /** Counters acknowledge collector transport only, never backend persistence. */
217
+ export function getProxyOtelLogSnapshot() {
218
+ return {
219
+ mode: isProxyOtelOnly() ? "otel" : "file-and-otel",
220
+ initialized: provider !== undefined,
221
+ deliveryGuarantee: "best-effort; HTTP success is not per-record acceptance or backend persistence",
222
+ invalidRecords,
223
+ queues: queues.map((q, index) => ({
224
+ kind: index === 0 ? "metadata" : "bodies",
225
+ capacity: q.capacity,
226
+ ...q.state,
227
+ })),
228
+ };
229
+ }
230
+ /** Bounded provider flush belongs after final request and lifecycle publication. */
231
+ export async function flushProxyOtelLogs() {
232
+ await provider?.forceFlush();
233
+ }
234
+ /** Release this process's exporter and restore console ownership. */
235
+ export async function shutdownProxyOtelLogs() {
236
+ restoreConsole?.();
237
+ restoreConsole = undefined;
238
+ await provider?.shutdown();
239
+ provider = undefined;
240
+ queues.length = 0;
241
+ invalidRecords = 0;
242
+ }
243
+ /** Flush short-lived proxy command diagnostics on every normal return or exception. */
244
+ export function withProxyOtelLogShutdown(handler) {
245
+ return async (arg) => {
246
+ try {
247
+ await handler(arg);
248
+ }
249
+ finally {
250
+ await flushProxyOtelLogs().catch(() => undefined);
251
+ await shutdownProxyOtelLogs().catch(() => undefined);
252
+ }
253
+ };
254
+ }
@@ -11,6 +11,10 @@
11
11
  * falls back to JSON.parse.
12
12
  */
13
13
  import type { LoadProxyConfigOptions, ProxyConfigFile } from "../types/index.js";
14
+ /** Default on-disk path used when `proxy start` is not given `--config`. */
15
+ export declare function defaultProxyConfigPath(): string;
16
+ /** Resolve `--config` the same way `proxy start` / `proxy install` do. */
17
+ export declare function resolveProxyConfigPath(explicit?: string): string;
14
18
  /**
15
19
  * Replace all `${VAR}` / `${VAR:-default}` references in a string.
16
20
  *
@@ -11,9 +11,21 @@
11
11
  * falls back to JSON.parse.
12
12
  */
13
13
  import { readFile } from "node:fs/promises";
14
- import { extname } from "node:path";
14
+ import { homedir } from "node:os";
15
+ import { extname, join, resolve } from "node:path";
15
16
  import { MAX_MAX_INFLIGHT_PER_ACCOUNT, MIN_MAX_INFLIGHT_PER_ACCOUNT, } from "./modelRouter.js";
16
17
  import { logger } from "../utils/logger.js";
18
+ /** Default on-disk path used when `proxy start` is not given `--config`. */
19
+ export function defaultProxyConfigPath() {
20
+ return join(homedir(), ".neurolink", "proxy-config.yaml");
21
+ }
22
+ /** Resolve `--config` the same way `proxy start` / `proxy install` do. */
23
+ export function resolveProxyConfigPath(explicit) {
24
+ const trimmed = explicit?.trim();
25
+ return trimmed && trimmed.length > 0
26
+ ? resolve(trimmed)
27
+ : defaultProxyConfigPath();
28
+ }
17
29
  // ---------------------------------------------------------------------------
18
30
  // Environment variable resolution
19
31
  // ---------------------------------------------------------------------------
@@ -1,3 +1,4 @@
1
+ import { emitProxyOtelEvent, isProxyOtelOnly, initializeProxyOtelLogs, } from "./otelLogSink.js";
1
2
  import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
2
3
  import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
4
  import { appendFile } from "node:fs/promises";
@@ -297,6 +298,25 @@ export function configureProxyLifecycleLogger(options) {
297
298
  batchSize = positiveInteger(options.batchSize, DEFAULT_BATCH_SIZE);
298
299
  maxWriteRetries = positiveInteger(options.maxWriteRetries, DEFAULT_MAX_WRITE_RETRIES);
299
300
  flushIntervalMs = positiveInteger(options.flushIntervalMs, DEFAULT_FLUSH_INTERVAL_MS);
301
+ if (options.enabled && isProxyOtelOnly()) {
302
+ initializeProxyOtelLogs(options.filePrefix === "proxy-supervisor" ? "supervisor" : "worker");
303
+ loggerEnabled = true;
304
+ sessionHashKey = process.env.NEUROLINK_PROXY_SESSION_SECRET
305
+ ? createHash("sha256")
306
+ .update(process.env.NEUROLINK_PROXY_SESSION_SECRET)
307
+ .digest()
308
+ : sessionHashKey;
309
+ stopRuntimeMetrics = startProxyRuntimeMetrics((runtimeSample) => {
310
+ logProxyLifecycleEvent({
311
+ event: "runtime_sample",
312
+ requestId: "-",
313
+ method: "-",
314
+ path: "-",
315
+ runtimeSample,
316
+ });
317
+ });
318
+ return;
319
+ }
300
320
  if (options.enabled && options.logDir) {
301
321
  try {
302
322
  mkdirSync(options.logDir, { recursive: true, mode: 0o700 });
@@ -336,7 +356,7 @@ export function logProxyLifecycleEvent(input) {
336
356
  * capacity is unavailable.
337
357
  */
338
358
  function enqueueLifecycleEvent(input, onPersisted) {
339
- if (!loggerEnabled || !lifecycleLogDir) {
359
+ if (!loggerEnabled || (!lifecycleLogDir && !isProxyOtelOnly())) {
340
360
  onPersisted?.(false);
341
361
  return;
342
362
  }
@@ -406,6 +426,10 @@ function enqueueLifecycleEvent(input, onPersisted) {
406
426
  : {}),
407
427
  ...(input.runtimeSample ? { runtimeSample: input.runtimeSample } : {}),
408
428
  };
429
+ if (isProxyOtelOnly()) {
430
+ emitProxyOtelEvent(filePrefix === "proxy-supervisor" ? "supervisor" : "lifecycle", record);
431
+ return;
432
+ }
409
433
  queue.push({
410
434
  filePrefix,
411
435
  logDir: lifecycleLogDir,
@@ -431,6 +455,10 @@ export async function persistProxyLifecycleAcceptance(input, timeoutMs = LIFECYC
431
455
  if (!loggerRequired) {
432
456
  return;
433
457
  }
458
+ if (isProxyOtelOnly()) {
459
+ enqueueLifecycleEvent({ ...input, event: "request_accepted" });
460
+ return;
461
+ }
434
462
  const confirmed = new Promise((resolve) => {
435
463
  enqueueLifecycleEvent({ ...input, event: "request_accepted" }, resolve);
436
464
  });
@@ -462,6 +490,8 @@ export async function flushProxyLifecycleEvents(timeoutMs = 5_000) {
462
490
  export function getProxyLifecycleLoggerSnapshot() {
463
491
  return {
464
492
  enabled: loggerEnabled,
493
+ sink: isProxyOtelOnly() ? "otel" : "file",
494
+ admissionPolicy: isProxyOtelOnly() ? "best-effort" : "durable-file",
465
495
  schemaVersion: SCHEMA_VERSION,
466
496
  processInstanceId,
467
497
  nextSequence,
@@ -5,6 +5,7 @@
5
5
  * when a LoggerProvider is configured via OpenTelemetry instrumentation.
6
6
  * Useful for debugging and auditing proxy traffic.
7
7
  */
8
+ import { emitProxyOtelEvent, getProxyOtelLogSnapshot, initializeProxyOtelLogs, isProxyOtelOnly, } from "./otelLogSink.js";
8
9
  import { join } from "path";
9
10
  import { homedir } from "os";
10
11
  import { logger } from "../utils/logger.js";
@@ -46,6 +47,8 @@ const metadataSinks = {
46
47
  export function getRequestLoggerSnapshot() {
47
48
  return {
48
49
  enabled: logEnabled,
50
+ diskEnabled: logEnabled && !isProxyOtelOnly(),
51
+ otel: getProxyOtelLogSnapshot(),
49
52
  requests: { ...metadataSinks.requests },
50
53
  attempts: { ...metadataSinks.attempts },
51
54
  debug: { ...metadataSinks.debug },
@@ -155,6 +158,12 @@ export function initRequestLogger(enabled = true, customLogsDir) {
155
158
  configureProxyLifecycleLogger({ enabled: false });
156
159
  return;
157
160
  }
161
+ if (isProxyOtelOnly()) {
162
+ initializeProxyOtelLogs();
163
+ logDir = null;
164
+ configureProxyLifecycleLogger({ enabled: true });
165
+ return;
166
+ }
158
167
  try {
159
168
  logDir = customLogsDir ?? join(homedir(), ".neurolink", "logs");
160
169
  if (!existsSync(logDir)) {
@@ -183,7 +192,7 @@ export async function logRequest(entry) {
183
192
  ? "handler_error"
184
193
  : "completed";
185
194
  notifyProxyFinalLog(entry);
186
- if (!logEnabled || !logDir) {
195
+ if (!logEnabled || (!logDir && !isProxyOtelOnly())) {
187
196
  return;
188
197
  }
189
198
  // Only use OtelBridge if traceId not already provided by caller.
@@ -197,6 +206,10 @@ export async function logRequest(entry) {
197
206
  entry.spanId = traceCtx.spanId;
198
207
  }
199
208
  }
209
+ if (isProxyOtelOnly()) {
210
+ await emitOtlpLogRecord(entry);
211
+ return;
212
+ }
200
213
  const logFile = join(logDir, `proxy-${new Date().toISOString().split("T")[0]}.jsonl`);
201
214
  const line = JSON.stringify(entry) + "\n";
202
215
  try {
@@ -206,7 +219,7 @@ export async function logRequest(entry) {
206
219
  // Non-fatal — don't crash proxy for logging failures
207
220
  }
208
221
  // Emit OTLP log record (additive — file logging is the primary sink)
209
- emitOtlpLogRecord(entry);
222
+ void emitOtlpLogRecord(entry);
210
223
  }
211
224
  /**
212
225
  * Log an upstream attempt separately from the final request outcome.
@@ -214,7 +227,7 @@ export async function logRequest(entry) {
214
227
  * or OTLP-derived dashboard panels.
215
228
  */
216
229
  export async function logRequestAttempt(entry) {
217
- if (!logEnabled || !logDir) {
230
+ if (!logEnabled || (!logDir && !isProxyOtelOnly())) {
218
231
  return;
219
232
  }
220
233
  if (!entry.traceId) {
@@ -225,6 +238,10 @@ export async function logRequestAttempt(entry) {
225
238
  entry.spanId = traceCtx.spanId;
226
239
  }
227
240
  }
241
+ if (isProxyOtelOnly()) {
242
+ emitProxyOtelEvent("attempt", entry);
243
+ return;
244
+ }
228
245
  const logFile = join(logDir, `proxy-attempts-${new Date().toISOString().split("T")[0]}.jsonl`);
229
246
  const line = JSON.stringify(entry) + "\n";
230
247
  try {
@@ -242,6 +259,9 @@ export async function logRequestAttempt(entry) {
242
259
  * where OTel initialization completes after the first log request.
243
260
  */
244
261
  async function resolveLoggerProvider() {
262
+ if (isProxyOtelOnly()) {
263
+ return initializeProxyOtelLogs();
264
+ }
245
265
  if (otelLoggerProvider === false) {
246
266
  return undefined;
247
267
  } // permanently unavailable
@@ -275,7 +295,7 @@ async function resolveLoggerProvider() {
275
295
  * Non-blocking, non-fatal — failures are silently swallowed.
276
296
  */
277
297
  function emitOtlpLogRecord(entry) {
278
- resolveLoggerProvider()
298
+ return resolveLoggerProvider()
279
299
  .then((provider) => {
280
300
  if (!provider) {
281
301
  return;
@@ -293,8 +313,11 @@ function emitOtlpLogRecord(entry) {
293
313
  otelLogger.emit({
294
314
  severityNumber,
295
315
  severityText,
296
- body: `${entry.method} ${entry.path} → ${entry.responseStatus} (${entry.responseTimeMs}ms)`,
316
+ body: isProxyOtelOnly()
317
+ ? JSON.stringify(entry)
318
+ : `${entry.method} ${entry.path} → ${entry.responseStatus} (${entry.responseTimeMs}ms)`,
297
319
  attributes: {
320
+ "proxy.record_kind": "request_final",
298
321
  // Core request fields
299
322
  "request.id": entry.requestId,
300
323
  "http.method": entry.method,
@@ -440,6 +463,7 @@ function emitOtlpBodyLogRecord(entry, stored) {
440
463
  body: chunk,
441
464
  attributes: {
442
465
  "event.name": "proxy.body_capture",
466
+ "proxy.record_kind": "body",
443
467
  "request.id": entry.requestId,
444
468
  "body.phase": entry.phase,
445
469
  "body.chunk_index": chunkIndex,
@@ -484,7 +508,7 @@ function emitOtlpBodyLogRecord(entry, stored) {
484
508
  }
485
509
  /** Capture an owned request body with bounded processing and tracked index/export publication. */
486
510
  export async function logBodyCapture(entry) {
487
- if (!logEnabled || !logDir) {
511
+ if (!logEnabled || (!logDir && !isProxyOtelOnly())) {
488
512
  return;
489
513
  }
490
514
  // Borrowed traffic is somebody else's conversation. Capturing it would leave
@@ -507,7 +531,9 @@ export async function logBodyCapture(entry) {
507
531
  const redactedHeaders = processed.headers;
508
532
  const stored = processed.stored;
509
533
  const dateStr = new Date(metadata.timestamp).toISOString().split("T")[0];
510
- const logFile = join(destination, `proxy-debug-${dateStr}.jsonl`);
534
+ const logFile = destination
535
+ ? join(destination, `proxy-debug-${dateStr}.jsonl`)
536
+ : undefined;
511
537
  const indexEntry = {
512
538
  timestamp: metadata.timestamp,
513
539
  type: "body_capture",
@@ -538,8 +564,13 @@ export async function logBodyCapture(entry) {
538
564
  indexEntry.traceId = traceCtx.traceId;
539
565
  indexEntry.spanId = traceCtx.spanId;
540
566
  }
567
+ if (isProxyOtelOnly()) {
568
+ emitProxyOtelEvent("body_capture_index", indexEntry);
569
+ }
541
570
  try {
542
- await appendMetadataRecord(logFile, JSON.stringify(indexEntry) + "\n", "debug", { waitForPersistence: true });
571
+ if (logFile) {
572
+ await appendMetadataRecord(logFile, JSON.stringify(indexEntry) + "\n", "debug", { waitForPersistence: true });
573
+ }
543
574
  }
544
575
  catch {
545
576
  // Non-fatal
@@ -596,12 +627,11 @@ export async function logFullRequestResponse(entry) {
596
627
  * These are invisible in normal request logs since the 200 was already recorded.
597
628
  */
598
629
  export async function logStreamError(entry) {
599
- if (!logEnabled || !logDir) {
630
+ if (!logEnabled || (!logDir && !isProxyOtelOnly())) {
600
631
  return;
601
632
  }
602
633
  const bridge = new OtelBridge();
603
634
  const traceCtx = bridge.getCurrentTraceContext();
604
- const logFile = join(logDir, `proxy-${new Date().toISOString().split("T")[0]}.jsonl`);
605
635
  const logEntry = {
606
636
  ...entry,
607
637
  responseStatus: 200,
@@ -614,6 +644,11 @@ export async function logStreamError(entry) {
614
644
  logEntry.traceId = traceCtx.traceId;
615
645
  logEntry.spanId = traceCtx.spanId;
616
646
  }
647
+ if (isProxyOtelOnly()) {
648
+ emitProxyOtelEvent("stream_error", logEntry);
649
+ return;
650
+ }
651
+ const logFile = join(logDir, `proxy-${new Date().toISOString().split("T")[0]}.jsonl`);
617
652
  try {
618
653
  await appendMetadataRecord(logFile, JSON.stringify(logEntry) + "\n", "requests");
619
654
  }