qwenproxy-cli 1.0.26 → 1.0.27

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwenproxy-cli",
3
- "version": "1.0.26",
3
+ "version": "1.0.27",
4
4
  "description": "High-performance OpenAI & Anthropic compatible API gateway for Qwen with multi-account rotation, interactive TUI, and resilient tool calling.",
5
5
  "main": "src/index.ts",
6
6
  "bin": {
@@ -24,8 +24,8 @@
24
24
  "reset": "npx tsx src/reset-cooldowns.ts",
25
25
  "purge": "npx tsx src/delete-chats.ts",
26
26
  "test": "npm run test:mock && npm run test:live",
27
- "test:mock": "tsx --test --test-concurrency=1 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/personalization-deadline.test.ts",
28
- "test:mock:fast": "tsx --test --test-concurrency=2 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/personalization-deadline.test.ts",
27
+ "test:mock": "tsx --test --test-concurrency=1 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts",
28
+ "test:mock:fast": "tsx --test --test-concurrency=2 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts",
29
29
  "test:live": "tsx --test src/tests/concurrentChat.test.ts src/tests/agenticStress.test.ts",
30
30
  "benchmark:proxy": "npx tsx src/benchmarks/proxy-baseline.ts",
31
31
  "typecheck": "tsc --noEmit"
package/src/api/server.ts CHANGED
@@ -22,6 +22,11 @@ import { AuthError, NotFoundError } from "../core/errors.js";
22
22
  import type { QwenAccount } from "../core/accounts.js";
23
23
  import { isAuthMockEnabled } from "../services/auth-playwright.js";
24
24
 
25
+ import {
26
+ hookServerConsoleForLogging,
27
+ getServerLogHistory,
28
+ subscribeServerLogStream,
29
+ } from "../core/server-log-buffer.ts";
25
30
  // Module-level state (initialized in startServer)
26
31
  let cache: MemoryCache | undefined;
27
32
  let watchdog: Watchdog | undefined;
@@ -131,12 +136,23 @@ app.use("*", async (c, next) => {
131
136
  String(Math.max(0, ratelimit.tokens - 1)),
132
137
  );
133
138
  c.header("x-ratelimit-reset-tokens", "0");
134
-
135
- metrics.increment("requests.total");
139
+ const isProbe =
140
+ c.req.path === "/health" ||
141
+ c.req.path === "/metrics" ||
142
+ c.req.path === "/logs" ||
143
+ c.req.path.startsWith("/logs") ||
144
+ c.req.path.startsWith("/diagnostics") ||
145
+ c.req.path === "/favicon.ico";
146
+
147
+ if (!isProbe) {
148
+ metrics.increment("requests.total");
149
+ }
136
150
  const start = Date.now();
137
151
  await next();
138
152
  const duration = Date.now() - start;
139
- metrics.histogram("latency.request", duration);
153
+ if (!isProbe) {
154
+ metrics.histogram("latency.request", duration);
155
+ }
140
156
  c.header("X-Response-Time", `${duration}ms`);
141
157
  c.header("openai-processing-ms", String(duration));
142
158
  });
@@ -272,6 +288,22 @@ app.get("/health", async (c) => {
272
288
  activeAccounts: (await import("../services/playwright.js")).getActivePlaywrightAccountIds(),
273
289
  metrics: {
274
290
  cache: await cache?.getStats(),
291
+ requestsTotal: Number(metrics.get("requests.total")?.value ?? 0),
292
+ requestsErrors: Number(metrics.get("requests.errors")?.value ?? 0),
293
+ latencyAvgMs: (() => {
294
+ const hist = metrics.get("latency.request")?.value;
295
+ if (hist && typeof hist === "object" && (hist as any).count > 0) {
296
+ return Math.round((hist as any).sum / (hist as any).count);
297
+ }
298
+ return 0;
299
+ })(),
300
+ deltasCount: Number(metrics.get("requests.delta")?.value ?? 0),
301
+ fullReplaysCount: Number(metrics.get("requests.full")?.value ?? 0),
302
+ toolCallsCount: Number(metrics.get("toolcalls.total")?.value ?? 0),
303
+ toolCallsRecovered: Number(metrics.get("toolcalls.recovered")?.value ?? 0),
304
+ captchasDetected: Number(metrics.get("captcha.challenges.detected")?.value ?? 0),
305
+ captchasSolved: Number(metrics.get("captcha.solves.succeeded")?.value ?? 0),
306
+ chatsCleaned: Number(metrics.get("chats.cleaned")?.value ?? 0),
275
307
  },
276
308
  });
277
309
  });
@@ -305,9 +337,54 @@ app.get("/metrics", (c) => {
305
337
  });
306
338
  });
307
339
 
340
+ app.get("/logs", (c) => {
341
+ return c.json(getServerLogHistory());
342
+ });
343
+
344
+ app.get("/logs/live", (c) => {
345
+ const encoder = new TextEncoder();
346
+ return c.body(
347
+ new ReadableStream({
348
+ start(controller) {
349
+ const past = getServerLogHistory();
350
+ for (const entry of past) {
351
+ try {
352
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(entry)}\n\n`));
353
+ } catch {}
354
+ }
355
+
356
+ const unsubscribe = subscribeServerLogStream((entry) => {
357
+ try {
358
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(entry)}\n\n`));
359
+ } catch {
360
+ unsubscribe();
361
+ }
362
+ });
363
+
364
+ c.req.raw.signal.addEventListener("abort", () => {
365
+ unsubscribe();
366
+ });
367
+ },
368
+ }),
369
+ 200,
370
+ {
371
+ "Content-Type": "text/event-stream",
372
+ "Cache-Control": "no-cache",
373
+ Connection: "keep-alive",
374
+ },
375
+ );
376
+ });
377
+
308
378
  app.onError((err, c) => {
309
379
  const requestId = c.req.header("X-Request-Id") || "unknown";
310
- metrics.increment("requests.errors");
380
+ const isProbe =
381
+ c.req.path === "/health" ||
382
+ c.req.path === "/metrics" ||
383
+ c.req.path.startsWith("/diagnostics") ||
384
+ c.req.path === "/favicon.ico";
385
+ if (!isProbe) {
386
+ metrics.increment("requests.errors");
387
+ }
311
388
  logger.error("API Error", {
312
389
  requestId,
313
390
  error: err instanceof Error ? err.message : String(err),
@@ -599,6 +676,7 @@ export async function startServer(options?: {
599
676
  installSignalHandlers?: boolean;
600
677
  showBanner?: boolean;
601
678
  }): Promise<StartedServerInfo> {
679
+ hookServerConsoleForLogging();
602
680
  if (server) {
603
681
  if (options?.installSignalHandlers !== false) installSignalHandlers();
604
682
  return buildStartedServerInfo();
@@ -32,8 +32,13 @@ export class Metrics extends EventEmitter {
32
32
  // Core request metrics
33
33
  ["requests.total", "counter", "Total requests processed"],
34
34
  ["requests.errors", "counter", "Total request errors"],
35
+ ["requests.delta", "counter", "Thread-native delta requests"],
36
+ ["requests.full", "counter", "Full context replay or initial requests"],
35
37
  ["latency.request", "histogram", "Request latency (ms)"],
36
-
38
+ ["toolcalls.total", "counter", "Total tool calls processed"],
39
+ ["toolcalls.malformed", "counter", "Malformed tool calls detected"],
40
+ ["toolcalls.recovered", "counter", "Malformed tool calls recovered/healed"],
41
+ ["chats.cleaned", "counter", "Remote chats automatically cleaned"],
37
42
  // Stream metrics
38
43
  ["streams.active", "gauge", "Active SSE streams"],
39
44
  ["streams.errors", "counter", "Stream errors"],
package/src/core/paths.ts CHANGED
@@ -170,6 +170,13 @@ export function getSyncStatePath(customDataDir?: string): string {
170
170
  return path.join(customDataDir || getDataDir(), "sync-state.json");
171
171
  }
172
172
 
173
+ /**
174
+ * Path to TUI user preferences file (model, effort, last tab, etc.).
175
+ */
176
+ export function getTuiSettingsPath(customDataDir?: string): string {
177
+ return path.join(customDataDir || getDataDir(), "tui-settings.json");
178
+ }
179
+
173
180
  /**
174
181
  * Path to user configuration .env file (either local or in global data dir).
175
182
  */
@@ -0,0 +1,105 @@
1
+ /**
2
+ * QwenProxy - Server Log Circular Buffer & Live SSE Broadcast
3
+ * Captures server logs and streams them to connected TUI instances.
4
+ */
5
+
6
+ import { stripAnsi } from "../tui/theme.ts";
7
+
8
+ export interface ServerLogMessage {
9
+ time: string;
10
+ level: "INFO" | "WARN" | "ERROR";
11
+ message: string;
12
+ }
13
+
14
+ const MAX_LOG_HISTORY = 400;
15
+ const logHistory: ServerLogMessage[] = [];
16
+ const subscribers = new Set<(entry: ServerLogMessage) => void>();
17
+ let consoleHooked = false;
18
+
19
+ export function recordServerLog(level: "INFO" | "WARN" | "ERROR", text: string): void {
20
+ if (!text) return;
21
+ const clean = stripAnsi(text).trim();
22
+ if (!clean) return;
23
+
24
+ const time = new Date().toLocaleTimeString("pt-BR", {
25
+ hour: "2-digit",
26
+ minute: "2-digit",
27
+ second: "2-digit",
28
+ });
29
+
30
+ const lines = clean.split(/\r?\n/);
31
+ for (const raw of lines) {
32
+ const line = raw.trim();
33
+ if (!line) continue;
34
+ // Filter out raw ASCII box frames and border rows
35
+ if (/^[+\-=#]{5,}$/.test(line)) continue;
36
+ if (/^\|\s*\|$/.test(line)) continue;
37
+ if (line.startsWith("|") && line.endsWith("|")) continue;
38
+
39
+ // Filter out box remnants
40
+ if (
41
+ line === "QwenProxy" ||
42
+ line === "OpenAI & Anthropic Compatible API" ||
43
+ /^Endpoint\s+http/i.test(line) ||
44
+ /^Port\s+\d+/i.test(line) ||
45
+ /^Accounts\s+\d+\/\d+/i.test(line) ||
46
+ /^API Key\s+/i.test(line) ||
47
+ /^Status\s+●/i.test(line)
48
+ ) {
49
+ continue;
50
+ }
51
+
52
+ const entry: ServerLogMessage = { time, level, message: line };
53
+ logHistory.push(entry);
54
+ if (logHistory.length > MAX_LOG_HISTORY) {
55
+ logHistory.shift();
56
+ }
57
+
58
+ for (const sub of subscribers) {
59
+ try {
60
+ sub(entry);
61
+ } catch {}
62
+ }
63
+ }
64
+ }
65
+
66
+ export function getServerLogHistory(): ServerLogMessage[] {
67
+ return logHistory.slice();
68
+ }
69
+
70
+ export function subscribeServerLogStream(cb: (entry: ServerLogMessage) => void): () => void {
71
+ subscribers.add(cb);
72
+ return () => {
73
+ subscribers.delete(cb);
74
+ };
75
+ }
76
+
77
+ export function hookServerConsoleForLogging(): void {
78
+ if (consoleHooked) return;
79
+ consoleHooked = true;
80
+
81
+ const origLog = console.log;
82
+ const origInfo = console.info;
83
+ const origWarn = console.warn;
84
+ const origError = console.error;
85
+
86
+ console.log = (...args: any[]) => {
87
+ origLog(...args);
88
+ recordServerLog("INFO", args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" "));
89
+ };
90
+
91
+ console.info = (...args: any[]) => {
92
+ origInfo(...args);
93
+ recordServerLog("INFO", args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" "));
94
+ };
95
+
96
+ console.warn = (...args: any[]) => {
97
+ origWarn(...args);
98
+ recordServerLog("WARN", args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" "));
99
+ };
100
+
101
+ console.error = (...args: any[]) => {
102
+ origError(...args);
103
+ recordServerLog("ERROR", args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" "));
104
+ };
105
+ }
@@ -22,6 +22,7 @@ import {
22
22
  } from "./streaming.ts";
23
23
  import { config, type ChatMode } from "../../core/config.ts";
24
24
  import { logger } from "../../core/logger.ts";
25
+ import { metrics } from "../../core/metrics.ts";
25
26
  import { getContextMeterHeaders, type ContextMeterMode } from "../../services/context-meter.ts";
26
27
  import {
27
28
  getLogicalThreadState,
@@ -277,6 +278,11 @@ export async function chatCompletions(c: Context) {
277
278
  console.log(
278
279
  `📤 [${routeLabel}] Request | req=${reqId} | ${streamResult.activeAccountLabel} | ${body.model} | ${replayed ? parsed.messageCount : msgCount} msg(s) | ${replayed ? fullPromptForRequest.length : finalPrompt.length} chars${replayed ? " | full-replay" : ""} | chat=${streamResult.uiSessionId.substring(0, 12)}${declaredTools.length ? ` | ${declaredTools.length} tool(s)` : ""}${files.length ? ` | ${files.length} file(s)` : ""} | +${Date.now() - reqStartedAt}ms`,
279
280
  );
281
+ if (replayed || !ctx.existingThread) {
282
+ metrics.increment("requests.full");
283
+ } else {
284
+ metrics.increment("requests.delta");
285
+ }
280
286
 
281
287
  const onAssistantComplete: ((event: AssistantCompleteEvent) => Promise<void> | void) | undefined = undefined;
282
288
 
@@ -20,6 +20,7 @@ import { config } from "../core/config.ts";
20
20
  import { isAccountBusy } from "../core/account-concurrency.ts";
21
21
  import { isChatSessionActive } from "./qwen-thread-state.ts";
22
22
  import { sleep } from "./human-behavior.ts";
23
+ import { metrics } from "../core/metrics.ts";
23
24
 
24
25
  export interface DeleteChatsResult {
25
26
  attempted: number;
@@ -190,6 +191,7 @@ async function processOrphanChatQueue(): Promise<void> {
190
191
  try {
191
192
  const ok = await deleteSingleQwenChat(entry.accountId, entry.chatId);
192
193
  if (ok) {
194
+ metrics.increment("chats.cleaned");
193
195
  logger.debug("[ChatCleanup] Deleted orphan chat", {
194
196
  accountId: entry.accountId,
195
197
  chatId: entry.chatId,
@@ -247,6 +249,7 @@ export async function cleanOldChatsForAccount(
247
249
  const ok = await deleteSingleQwenChat(accountId, chat.id);
248
250
  if (ok) {
249
251
  cleaned++;
252
+ metrics.increment("chats.cleaned");
250
253
  await sleep(300);
251
254
  }
252
255
  }
package/src/sync/index.ts CHANGED
@@ -115,8 +115,12 @@ export function inspectClientSyncStatus(
115
115
  const data = JSON.parse(raw);
116
116
  const url = data?.env?.ANTHROPIC_BASE_URL || "";
117
117
  const model = data?.env?.ANTHROPIC_MODEL || data?.model || "";
118
+ const isLocalHost =
119
+ url.includes(String(port)) ||
120
+ url.includes(`127.0.0.1:${port}`) ||
121
+ url.includes(`localhost:${port}`);
118
122
  const isSynced =
119
- (url.includes(String(port)) || url.includes("qwenproxy")) &&
123
+ isLocalHost &&
120
124
  (model.toLowerCase().includes("qwen") || Boolean(data?.env?.ANTHROPIC_AUTH_TOKEN));
121
125
  return {
122
126
  id,
@@ -132,34 +136,66 @@ export function inspectClientSyncStatus(
132
136
  const isProviderActive = /^model_provider\s*=\s*["']qwenproxy["']/m.test(raw);
133
137
  const modelMatch = raw.match(/^model\s*=\s*["']([^"']+)["']/m);
134
138
  const model = modelMatch ? modelMatch[1] : undefined;
139
+ const urlMatch = raw.match(/\[model_providers\.qwenproxy\][\s\S]*?base_url\s*=\s*["']([^"']+)["']/);
140
+ const url = urlMatch ? urlMatch[1] : "";
141
+ const isLocalHost = Boolean(
142
+ url && (url.includes(String(port)) || url.includes(`127.0.0.1:${port}`) || url.includes(`localhost:${port}`)),
143
+ );
135
144
  return {
136
145
  id,
137
146
  installed: true,
138
- synced: hasProvider && isProviderActive,
147
+ synced: hasProvider && isProviderActive && isLocalHost,
139
148
  model,
140
149
  };
141
150
  }
142
151
 
143
152
  if (id === "opencode") {
144
- const hasQwen =
145
- raw.includes('"qwenproxy"') &&
146
- (raw.includes(String(port)) || raw.includes("QwenProxy") || raw.includes("qwen3"));
153
+ let isSynced = false;
154
+ try {
155
+ const data = JSON.parse(raw);
156
+ const provider = data?.provider?.qwenproxy;
157
+ const url = provider?.options?.baseURL || "";
158
+ const isLocalHost =
159
+ url.includes(String(port)) ||
160
+ url.includes(`127.0.0.1:${port}`) ||
161
+ url.includes(`localhost:${port}`);
162
+ isSynced = Boolean(provider && isLocalHost);
163
+ } catch {
164
+ const qwenBlockMatch = raw.match(/"qwenproxy"\s*:\s*\{[\s\S]*?"baseURL"\s*:\s*"([^"]+)"/);
165
+ const url = qwenBlockMatch ? qwenBlockMatch[1] : "";
166
+ isSynced = Boolean(
167
+ url &&
168
+ (url.includes(String(port)) ||
169
+ url.includes(`127.0.0.1:${port}`) ||
170
+ url.includes(`localhost:${port}`)),
171
+ );
172
+ }
147
173
  return {
148
174
  id,
149
175
  installed: true,
150
- synced: hasQwen,
176
+ synced: isSynced,
151
177
  };
152
178
  }
153
179
 
154
180
  if (id === "omp") {
155
- const hasQwen =
156
- /^ {2}qwenproxy:\s*$/m.test(raw) ||
157
- raw.includes("qwenproxy:") ||
158
- (raw.includes(String(port)) && raw.includes("qwen3"));
181
+ const ompMatch = raw.match(/^[ \t]*qwenproxy:\s*\r?\n((?:[ \t]{4,}.*\r?\n?)*)/m);
182
+ let isSynced = false;
183
+ let url: string | undefined;
184
+ if (ompMatch) {
185
+ const urlMatch = ompMatch[1].match(/baseUrl:\s*(\S+)/);
186
+ url = urlMatch ? urlMatch[1].replace(/['"]/g, "") : undefined;
187
+ isSynced = Boolean(
188
+ url &&
189
+ (url.includes(String(port)) ||
190
+ url.includes(`127.0.0.1:${port}`) ||
191
+ url.includes(`localhost:${port}`)),
192
+ );
193
+ }
159
194
  return {
160
195
  id,
161
196
  installed: true,
162
- synced: hasQwen,
197
+ synced: isSynced,
198
+ url,
163
199
  };
164
200
  }
165
201
  } catch {
@@ -1,6 +1,7 @@
1
1
  import crypto from "node:crypto";
2
2
  import { robustParseJSON, computeMissingJsonClosingTokens } from "../utils/json.ts";
3
3
  import { logger, isToolcallDebugEnabled } from "../core/logger.js";
4
+ import { metrics } from "../core/metrics.ts";
4
5
  import type { ParsedToolCall } from "./types";
5
6
  import type { FunctionToolDefinition } from "./types";
6
7
  import {
@@ -1501,7 +1502,7 @@ export class StreamingToolParser {
1501
1502
  }
1502
1503
 
1503
1504
  this.emittedCallKeys.add(key);
1504
-
1505
+ metrics.increment("toolcalls.total");
1505
1506
  const incremental = this.activeIncrementalToolCall;
1506
1507
  const matchesIncrementalCall =
1507
1508
  incremental?.name === tc.name && incremental.startEmitted;
@@ -1725,6 +1726,7 @@ export class StreamingToolParser {
1725
1726
  failureReason: options.failureReason,
1726
1727
  recoveryAttempts: options.recoveryAttempts,
1727
1728
  });
1729
+ metrics.increment("toolcalls.malformed");
1728
1730
  }
1729
1731
 
1730
1732
  private extractUndeclaredNamesFromContent(text: string): string[] {
@@ -1989,6 +1991,7 @@ export class StreamingToolParser {
1989
1991
  }
1990
1992
  }
1991
1993
  if (recovered) {
1994
+ metrics.increment("toolcalls.recovered");
1992
1995
  if (isToolcallDebugEnabled()) {
1993
1996
  logger.debug("[parser] flush: recovery successful", {
1994
1997
  name: recovered.name,
package/src/tui/app.ts CHANGED
@@ -26,6 +26,7 @@ import { SyncView } from "./views/sync-view.ts";
26
26
  import { StorageView } from "./views/storage-view.ts";
27
27
  import { AccountsView } from "./views/accounts-view.ts";
28
28
  import { LogsView } from "./views/logs-view.ts";
29
+ import { loadTuiSettings, saveTuiSettings } from "./settings.ts";
29
30
  export class TuiApp {
30
31
  private screen: Screen;
31
32
  private views: TuiView[] = [];
@@ -36,7 +37,7 @@ export class TuiApp {
36
37
  private pollInterval: NodeJS.Timeout | null = null;
37
38
  private statusSnapshot: ProxyStatusSnapshot | null = null;
38
39
  private renderScheduled = false;
39
- constructor(initialTab = 1) {
40
+ constructor(initialTab?: number) {
40
41
  this.screen = new Screen();
41
42
 
42
43
  this.views = [
@@ -47,10 +48,33 @@ export class TuiApp {
47
48
  new AccountsView(),
48
49
  new LogsView(),
49
50
  ];
50
- const tabIdx = Math.max(0, Math.min(this.views.length - 1, initialTab - 1));
51
+
52
+ let resolvedTab = initialTab;
53
+ if (!resolvedTab || isNaN(resolvedTab)) {
54
+ const saved = loadTuiSettings();
55
+ if (saved.lastTab && saved.lastTab >= 1 && saved.lastTab <= 6) {
56
+ resolvedTab = saved.lastTab;
57
+ } else {
58
+ resolvedTab = 1;
59
+ }
60
+ }
61
+
62
+ const tabIdx = Math.max(0, Math.min(this.views.length - 1, resolvedTab - 1));
51
63
  this.activeViewIndex = tabIdx;
52
64
  }
53
65
 
66
+ private switchTab(newIdx: number): void {
67
+ if (newIdx === this.activeViewIndex || !this.views[newIdx]) return;
68
+ const activeView = this.views[this.activeViewIndex];
69
+ if (activeView.onDeactivate) activeView.onDeactivate();
70
+ this.activeViewIndex = newIdx;
71
+ this.hoveredTabIndex = null;
72
+ const nextView = this.views[this.activeViewIndex];
73
+ if (nextView.onActivate) nextView.onActivate();
74
+ saveTuiSettings({ lastTab: newIdx + 1 });
75
+ this.requestRender();
76
+ }
77
+
54
78
  private getTabAtCol(col: number): number | null {
55
79
  let curCol = 3;
56
80
  for (let i = 0; i < this.views.length; i++) {
@@ -149,12 +173,7 @@ export class TuiApp {
149
173
  }
150
174
  if (key.name === "click") {
151
175
  if (tabIdx !== null && tabIdx !== this.activeViewIndex && this.views[tabIdx]) {
152
- if (activeView.onDeactivate) activeView.onDeactivate();
153
- this.activeViewIndex = tabIdx;
154
- this.hoveredTabIndex = null;
155
- const nextView = this.views[this.activeViewIndex];
156
- if (nextView.onActivate) nextView.onActivate();
157
- this.requestRender();
176
+ this.switchTab(tabIdx);
158
177
  return;
159
178
  }
160
179
  }
@@ -189,12 +208,7 @@ export class TuiApp {
189
208
  // Tab for cycling tabs across views (including from Chat view when no modal is open!)
190
209
  if (key.name === "tab" && !isModalOpen) {
191
210
  const newIdx = (this.activeViewIndex + 1) % this.views.length;
192
- if (activeView.onDeactivate) activeView.onDeactivate();
193
- this.activeViewIndex = newIdx;
194
- this.hoveredTabIndex = null;
195
- const nextView = this.views[this.activeViewIndex];
196
- if (nextView.onActivate) nextView.onActivate();
197
- this.requestRender();
211
+ this.switchTab(newIdx);
198
212
  return;
199
213
  }
200
214
  // Direct tab switching with numbers 1..6 only when NOT typing text in an input
@@ -207,12 +221,7 @@ export class TuiApp {
207
221
  if (!key.ctrl && !key.meta && ["1", "2", "3", "4", "5", "6"].includes(key.name)) {
208
222
  const newIdx = parseInt(key.name, 10) - 1;
209
223
  if (newIdx !== this.activeViewIndex && this.views[newIdx]) {
210
- if (activeView.onDeactivate) activeView.onDeactivate();
211
- this.activeViewIndex = newIdx;
212
- this.hoveredTabIndex = null;
213
- const nextView = this.views[this.activeViewIndex];
214
- if (nextView.onActivate) nextView.onActivate();
215
- this.requestRender();
224
+ this.switchTab(newIdx);
216
225
  return;
217
226
  }
218
227
  }
package/src/tui/index.ts CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { TuiApp } from "./app.ts";
7
7
 
8
- function parseInitialTab(): number {
8
+ function parseInitialTab(): number | undefined {
9
9
  const args = process.argv.slice(2);
10
10
  const tabArgIdx = args.findIndex((a) => a === "--tab" || a === "-t");
11
11
  if (tabArgIdx !== -1 && args[tabArgIdx + 1]) {
@@ -21,7 +21,7 @@ function parseInitialTab(): number {
21
21
  return parseInt(firstNumeric, 10);
22
22
  }
23
23
 
24
- return 1;
24
+ return undefined;
25
25
  }
26
26
 
27
27
  async function main() {