lynkr 9.14.0 → 9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lynkr",
3
- "version": "9.14.0",
3
+ "version": "9.14.1",
4
4
  "files": [
5
5
  "index.js",
6
6
  "install.sh",
Binary file
@@ -468,7 +468,11 @@ async function forwardAnthropicStreamAsOpenAIChunks(res, stream, requestedModel)
468
468
 
469
469
  router.post("/chat/completions", async (req, res) => {
470
470
  const startTime = Date.now();
471
- const sessionId = req.headers["x-session-id"] || req.headers["authorization"]?.split(" ")[1] || "openai-session";
471
+ // Issue #95: use sessionMiddleware's id (header → body id → content
472
+ // fingerprint → UUID). Falling back to the raw Authorization token here
473
+ // collapsed every conversation from a static-key client into ONE session
474
+ // (defeating WS1 pinning) and persisted the bearer token as a store key.
475
+ const sessionId = req.sessionId;
472
476
 
473
477
  try {
474
478
  if (!req.body || typeof req.body !== 'object') {
@@ -1588,7 +1592,10 @@ router.post("/embeddings", async (req, res) => {
1588
1592
  */
1589
1593
  router.post("/responses", async (req, res) => {
1590
1594
  const startTime = Date.now();
1591
- const sessionId = req.headers["x-session-id"] || req.headers["authorization"]?.split(" ")[1] || "responses-session";
1595
+ // Issue #95: same as /chat/completions — trust sessionMiddleware, never
1596
+ // the Authorization token. fingerprintSessionId understands the Responses
1597
+ // API's `input` field, so fingerprinting works on this route too.
1598
+ const sessionId = req.sessionId;
1592
1599
 
1593
1600
  try {
1594
1601
  const { convertResponsesToChat, convertChatToResponses } = require("../clients/responses-format");
@@ -791,6 +791,13 @@ async function invokeOpenRouter(body, incomingHeaders = {}) {
791
791
  stream: body.stream ?? false
792
792
  };
793
793
 
794
+ // Issue #95: forward the Lynkr session id so OpenRouter's meta-routers
795
+ // (auto/pareto) can apply session stickiness — without it every call in a
796
+ // pinned session re-ranks fresh and one agentic task can span 6+ models.
797
+ if (body._sessionId) {
798
+ openRouterBody.session_id = body._sessionId;
799
+ }
800
+
794
801
  // Add tools - inject standard tools if client didn't send any (passthrough mode)
795
802
  let toolsToSend = body.tools;
796
803
  let toolsInjected = false;
@@ -115,27 +115,48 @@ async function compressMessages(messages, tools = [], options = {}) {
115
115
  }
116
116
 
117
117
  try {
118
- const controller = new AbortController();
119
- const timeout = setTimeout(() => controller.abort(), headroomConfig.timeoutMs);
120
-
121
- const response = await fetch(`${headroomConfig.endpoint}/compress`, {
122
- method: "POST",
123
- headers: { "Content-Type": "application/json" },
124
- body: JSON.stringify({
125
- messages,
126
- tools,
127
- model: options.model || "claude-3-5-sonnet-20241022",
128
- model_limit: options.modelLimit || 200000,
129
- mode: options.mode || headroomConfig.mode,
130
- token_budget: options.tokenBudget,
131
- query_context: options.queryContext,
132
- preserve_recent_turns: options.preserveRecentTurns,
133
- target_ratio: options.targetRatio,
134
- }),
135
- signal: controller.signal,
118
+ const requestBody = JSON.stringify({
119
+ messages,
120
+ tools,
121
+ model: options.model || "claude-3-5-sonnet-20241022",
122
+ model_limit: options.modelLimit || 200000,
123
+ mode: options.mode || headroomConfig.mode,
124
+ token_budget: options.tokenBudget,
125
+ query_context: options.queryContext,
126
+ preserve_recent_turns: options.preserveRecentTurns,
127
+ target_ratio: options.targetRatio,
136
128
  });
137
129
 
138
- clearTimeout(timeout);
130
+ // The sidecar (uvicorn) closes idle keep-alive sockets after ~5s, and
131
+ // undici can reuse a pooled socket in that exact window → ECONNRESET
132
+ // surfaces as "fetch failed". One retry gets a fresh connection, so the
133
+ // race never costs us the compression. Aborts (real timeouts) still
134
+ // propagate immediately.
135
+ let response;
136
+ for (let attempt = 0; ; attempt++) {
137
+ const controller = new AbortController();
138
+ const timeout = setTimeout(() => controller.abort(), headroomConfig.timeoutMs);
139
+ try {
140
+ response = await fetch(`${headroomConfig.endpoint}/compress`, {
141
+ method: "POST",
142
+ headers: { "Content-Type": "application/json" },
143
+ body: requestBody,
144
+ signal: controller.signal,
145
+ });
146
+ break;
147
+ } catch (fetchErr) {
148
+ if (attempt === 0 && fetchErr.name !== "AbortError") {
149
+ logger.debug(
150
+ { error: fetchErr.message, cause: fetchErr.cause?.code || fetchErr.cause?.message },
151
+ "Headroom fetch failed, retrying on a fresh connection"
152
+ );
153
+ continue;
154
+ }
155
+ throw fetchErr;
156
+ } finally {
157
+ clearTimeout(timeout);
158
+ }
159
+ }
139
160
 
140
161
  if (!response.ok) {
141
162
  const errorText = await response.text();
@@ -179,7 +200,10 @@ async function compressMessages(messages, tools = [], options = {}) {
179
200
  if (err.name === "AbortError") {
180
201
  logger.warn({ timeoutMs: headroomConfig.timeoutMs }, "Headroom compression timed out");
181
202
  } else {
182
- logger.warn({ error: err.message }, "Headroom compression failed, using original");
203
+ logger.warn(
204
+ { error: err.message, cause: err.cause?.code || err.cause?.message },
205
+ "Headroom compression failed, using original"
206
+ );
183
207
  }
184
208
 
185
209
  return {