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
|
Binary file
|
package/src/api/openai-router.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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;
|
package/src/headroom/client.js
CHANGED
|
@@ -115,27 +115,48 @@ async function compressMessages(messages, tools = [], options = {}) {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
try {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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(
|
|
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 {
|