ldrouter 1.13.1 → 1.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/) and the project adheres to
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [1.14.1] - 2026-09-12
8
+
9
+ ### Fixed
10
+
11
+ - Do not reject any model solely because its stored `reasoning` capability is `false`; reasoning support is advisory for OpenAI-compatible providers and must not block Claude/Hermes requests.
12
+ - Redact API-key digests and metadata from authentication debug logs.
13
+
14
+ ## [1.14.0] - 2026-09-11
15
+
16
+ ### Added
17
+
18
+ - **Docker request logging**: every request now emits structured `info` logs when received and a completion log classified as `info` (2xx/3xx), `warn` (4xx), or `error` (5xx), with request ID, method, route, status, and duration. Query strings, headers, bodies, and secrets are excluded.
19
+
7
20
  ## [1.13.1] - 2026-09-11
8
21
 
9
22
  ### Fixed
@@ -21,7 +21,7 @@ import { registerGatewayRoutes } from './routes/gateway.js';
21
21
  import { registerHealthRoutes } from './routes/health.js';
22
22
  import { registerAdminIpGate } from './security/admin-ip-gate.js';
23
23
  import { metricsRegistry } from './metrics/registry.js';
24
- import { fatal, lifecycle, formatError, getDebugFlags, errorLine } from './logging/debug.js';
24
+ import { fatal, lifecycle, formatError, getDebugFlags, errorLine, requestLogFields, requestLogLevel, requestLogMessage } from './logging/debug.js';
25
25
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
26
26
  export async function buildApp(opts = {}) {
27
27
  const cfg = loadConfig();
@@ -43,9 +43,19 @@ export async function buildApp(opts = {}) {
43
43
  crossOriginEmbedderPolicy: false,
44
44
  });
45
45
  await app.register(cors, { origin: false, credentials: true });
46
- // Per-request logging + error shaping
46
+ // Every request is logged as structured JSON for Docker. Bodies and headers
47
+ // are intentionally excluded; sensitive values must never reach logs.
48
+ app.addHook('onRequest', async (req) => {
49
+ req.requestStartedAt = Date.now();
50
+ log.info({ requestId: req.id, method: req.method, url: req.url }, 'request received');
51
+ });
47
52
  app.addHook('onResponse', async (req, reply) => {
48
53
  reply.header('x-request-id', req.id);
54
+ const startedAt = req.requestStartedAt ?? Date.now();
55
+ const statusCode = reply.statusCode;
56
+ const fields = requestLogFields(String(req.id), req.method, req.url, statusCode, Date.now() - startedAt, req.routeOptions?.url);
57
+ const level = requestLogLevel(statusCode);
58
+ log[level](fields, requestLogMessage(statusCode));
49
59
  });
50
60
  app.setErrorHandler((err, req, reply) => {
51
61
  // Zod validation failures surface as 400 with readable field messages;
@@ -37,17 +37,17 @@ export function authenticateGatewayKey(req) {
37
37
  candidate = anthropic;
38
38
  if (!candidate)
39
39
  return null;
40
- // DEBUG: Log what we're trying to authenticate
41
- console.log(`🔑 API KEY AUTH - Candidate extracted: ${candidate.slice(0, 8)}...${candidate.slice(-4)}`);
40
+ // Never log API-key material, including partial values.
41
+ console.log('🔑 API KEY AUTH - Candidate received');
42
42
  // Custom keys are stored verbatim (no prefix requirement); auto-generated
43
43
  // keys start with ld-, but authentication must accept any stored secret.
44
44
  const digest = sha256Hex(candidate);
45
- console.log(`🔑 API KEY AUTH - SHA256 Digest: ${digest.slice(0, 16)}...`);
45
+ console.log('🔑 API KEY AUTH - Digest computed');
46
46
  const db = getDb();
47
47
  // DEBUG: Check total keys in database
48
48
  const allKeys = db.select().from(schema.apiKeys).all();
49
49
  console.log(`📊 TOTAL KEYS IN DB: ${allKeys.length}`);
50
- console.log(`📋 ALL KEYS:`, allKeys.map(k => ({ id: k.id.slice(0, 8), name: k.name, keyPrefix: k.keyPrefix, digestPreview: k.keyDigest?.slice(0, 16) + '...' })));
50
+ console.log('📋 API key metadata loaded');
51
51
  const row = db.select().from(schema.apiKeys).where(eq(schema.apiKeys.keyDigest, digest)).get();
52
52
  console.log(`❓ QUERY RESULT: Found match? ${!!row}`);
53
53
  if (!row) {
@@ -14,6 +14,24 @@
14
14
  // they emit at debug, so set LOG_LEVEL=debug to see them.
15
15
  import process from 'node:process';
16
16
  import { redactValue } from '../security/redact.js';
17
+ export function requestLogLevel(statusCode) {
18
+ if (statusCode >= 500)
19
+ return 'error';
20
+ if (statusCode >= 400)
21
+ return 'warn';
22
+ return 'info';
23
+ }
24
+ export function requestLogMessage(statusCode) {
25
+ if (statusCode >= 500)
26
+ return 'request completed with server error';
27
+ if (statusCode >= 400)
28
+ return 'request completed with client error';
29
+ return 'request completed';
30
+ }
31
+ export function requestLogFields(requestId, method, url, statusCode, durationMs, route) {
32
+ const safeUrl = url.split('?')[0] ?? url;
33
+ return { requestId, method, url: safeUrl, route: route ?? safeUrl, statusCode, durationMs };
34
+ }
17
35
  function envFlag(name) {
18
36
  const v = process.env[name];
19
37
  return v === '1' || v === 'true' || v === 'yes';
@@ -114,8 +114,8 @@ export async function registerApiKeyRoutes(app) {
114
114
  const enc = encryptSecret(secret);
115
115
  // DEBUG: Log what we're about to insert
116
116
  console.log(`📝 CREATE API KEY - Name: ${body.name}, Prefix: ${keyPrefix}`);
117
- console.log(`📝 CREATE API KEY - Secret (full): ${secret}`);
118
- console.log(`📝 CREATE API KEY - Digest: ${keyDigest}`);
117
+ console.log('📝 CREATE API KEY - Secret: [REDACTED]');
118
+ console.log('📝 CREATE API KEY - Digest: [REDACTED]');
119
119
  db.insert(schema.apiKeys).values({
120
120
  id,
121
121
  name: body.name,
@@ -38,7 +38,9 @@ export function deriveRequiredCapabilities(req) {
38
38
  * IMPORTANT: Treat undefined as "unknown" rather than "unsupported".
39
39
  * For generic OpenAI-compatible providers where capabilities weren't explicitly imported,
40
40
  * undefined means we don't know, so we should assume it's potentially supported.
41
- * Explicit false means "known unsupported".
41
+ * Explicit false means "known unsupported" for protocol capabilities such as
42
+ * tools, images, and streaming. Reasoning is advisory metadata because the
43
+ * upstream may support it even when discovery cannot identify it.
42
44
  */
43
45
  export function modelMeets(caps, req) {
44
46
  // Only reject if capability is explicitly false, not if unknown (undefined)
@@ -52,8 +54,6 @@ export function modelMeets(caps, req) {
52
54
  return false;
53
55
  if (req.audioInput && caps.audio_input === false)
54
56
  return false;
55
- if (req.reasoning && caps.reasoning === false)
56
- return false;
57
57
  if (req.responses && caps.responses === false)
58
58
  return false;
59
59
  return true;
@@ -69,8 +69,6 @@ function capabilityRejection(caps, req) {
69
69
  return 'image_input';
70
70
  if (req.audioInput && caps.audio_input === false)
71
71
  return 'audio_input';
72
- if (req.reasoning && caps.reasoning === false)
73
- return 'reasoning';
74
72
  if (req.responses && caps.responses === false)
75
73
  return 'responses';
76
74
  return 'capability_mismatch';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ldrouter",
3
- "version": "1.13.1",
3
+ "version": "1.14.1",
4
4
  "description": "LateDev Router — lightweight self-hosted LLM gateway with admin UI",
5
5
  "type": "module",
6
6
  "license": "MIT",