lynkr 9.14.1 → 9.14.3
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/README.md +5 -1
- package/bin/cli.js +14 -6
- package/bin/lynkr-desktop-token.js +108 -0
- package/bin/lynkr-init.js +13 -1
- package/bin/lynkr-restart.js +125 -0
- package/bin/opencode-setup.js +175 -0
- package/bin/run.js +92 -0
- package/config/model-capabilities.json +20 -0
- package/config/model-capability-seeds.json +75 -0
- package/config/model-tiers.json +13 -1
- package/package.json +16 -22
- package/scripts/claude-desktop.js +212 -0
- package/scripts/ope-report.js +78 -0
- package/scripts/seed-capabilities.js +230 -0
- package/src/api/claude-desktop-gateway.js +123 -0
- package/src/api/health.js +0 -1
- package/src/api/mcp-broker.js +149 -0
- package/src/api/middleware/budget-enforcer.js +77 -7
- package/src/api/middleware/budget.js +36 -2
- package/src/api/middleware/session.js +0 -0
- package/src/api/openai-router.js +210 -15
- package/src/api/providers-handler.js +16 -1
- package/src/api/router.js +73 -106
- package/src/auth-mode.js +40 -4
- package/src/budget/index.js +86 -0
- package/src/cache/embeddings.js +125 -35
- package/src/cache/onnx-embedder.js +92 -0
- package/src/cache/prompt.js +0 -7
- package/src/clients/bedrock-utils.js +2 -1
- package/src/clients/codex-process.js +1 -1
- package/src/clients/codex-utils.js +0 -2
- package/src/clients/databricks.js +337 -46
- package/src/clients/gpt-utils.js +84 -8
- package/src/clients/health-probe.js +157 -0
- package/src/clients/openai-format.js +73 -23
- package/src/clients/provider-capabilities.js +27 -2
- package/src/clients/resilience.js +0 -1
- package/src/clients/responses-format.js +0 -56
- package/src/clients/routing.js +1 -1
- package/src/config/index.js +13 -2
- package/src/context/budget.js +1 -1
- package/src/context/compression.js +127 -19
- package/src/context/tool-result-compressor.js +3 -5
- package/src/dashboard/api.js +1 -1
- package/src/logger/oversized-error-stream.js +1 -1
- package/src/memory/extractor.js +3 -3
- package/src/memory/search.js +2 -2
- package/src/memory/store.js +9 -24
- package/src/memory/tools.js +14 -0
- package/src/observability/metrics.js +52 -3
- package/src/observability/otel.js +180 -0
- package/src/orchestrator/azure-responses-sse.js +223 -0
- package/src/orchestrator/index.js +216 -212
- package/src/orchestrator/passthrough-stream.js +7 -1
- package/src/orchestrator/sse-transformer.js +130 -13
- package/src/policy/safe-commands.js +0 -1
- package/src/routing/bandit.js +39 -0
- package/src/routing/calibration.js +21 -1
- package/src/routing/capabilities.js +88 -0
- package/src/routing/capability-seeds/benchmarks-dir.js +58 -0
- package/src/routing/capability-seeds/family-heuristics.js +77 -0
- package/src/routing/capability-seeds/family.js +105 -0
- package/src/routing/capability-seeds/models-dev.js +64 -0
- package/src/routing/capability-seeds/normalize.js +160 -0
- package/src/routing/capability-seeds/registry.js +125 -0
- package/src/routing/capability-seeds/sources.js +69 -0
- package/src/routing/capability-seeds/swebench.js +66 -0
- package/src/routing/client-profiles.js +26 -0
- package/src/routing/complexity-analyzer.js +5 -8
- package/src/routing/confidence-scorer.js +2 -2
- package/src/routing/cost-optimizer.js +1 -2
- package/src/routing/decide.js +149 -0
- package/src/routing/deescalator.js +38 -1
- package/src/routing/embedding-cache.js +0 -1
- package/src/routing/index.js +202 -86
- package/src/routing/intent-score.js +0 -1
- package/src/routing/knn-router.js +27 -5
- package/src/routing/latency-tracker.js +1 -1
- package/src/routing/model-registry.js +25 -0
- package/src/routing/model-slots.js +65 -0
- package/src/routing/model-tiers.js +2 -0
- package/src/routing/ope.js +255 -0
- package/src/routing/openai-model-slots.js +82 -0
- package/src/routing/regret-estimator.js +1 -1
- package/src/routing/shortfall.js +290 -0
- package/src/routing/stuck-detector.js +127 -0
- package/src/routing/telemetry.js +11 -2
- package/src/server.js +41 -3
- package/src/stores/file-store.js +5 -1
- package/src/tools/web-search-exec.js +182 -0
- package/src/training/trajectory-compressor.js +1 -1
- package/src/workers/pool.js +1 -1
- package/src/workers/worker.js +1 -1
- package/scripts/convert-routellm.py +0 -105
- package/src/diff/comments.js +0 -153
- package/src/policy/web-fallback.js +0 -33
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure OpenAI Responses API — streaming adapter.
|
|
3
|
+
*
|
|
4
|
+
* The Responses API's streaming event vocabulary (response.created,
|
|
5
|
+
* response.output_item.added, response.content_part.added,
|
|
6
|
+
* response.output_text.delta/.done, response.function_call_arguments.delta/
|
|
7
|
+
* .done, response.output_item.done, response.completed) is structurally
|
|
8
|
+
* different from OpenAI Chat Completions' generic `choices[0].delta` chunk
|
|
9
|
+
* shape, which is what sse-transformer.js's `_openaiToAnthropicEvents`
|
|
10
|
+
* understands.
|
|
11
|
+
*
|
|
12
|
+
* Rather than duplicate that function's Anthropic-SSE emission logic (badge
|
|
13
|
+
* injection, message_start/message_stop, the mid-stream-error and
|
|
14
|
+
* no-finish-reason safety branches, usage extraction) for a second wire
|
|
15
|
+
* format, this module translates Responses-API events into SYNTHETIC OpenAI
|
|
16
|
+
* Chat-Completions SSE TEXT (`data: {"choices":[...]}\n\n`) and hands that
|
|
17
|
+
* off unchanged to the existing, already-tested transformer. Net effect:
|
|
18
|
+
* `invokeAzureOpenAI` returns a stream from this module exactly the way
|
|
19
|
+
* invokeMoonshot/invokeBaidu/invokeOpenAI already return a raw upstream
|
|
20
|
+
* stream — the orchestrator's existing `sseTransform.openaiToAnthropicSSE`
|
|
21
|
+
* call site needs zero changes to consume it.
|
|
22
|
+
*
|
|
23
|
+
* Verified against two live captures from this deployment's actual Azure
|
|
24
|
+
* endpoint (2026-08-26, api-version=2025-04-01-preview, model gpt-5.6-sol):
|
|
25
|
+
* one plain-text response, one forced tool-call response. Re-verify against
|
|
26
|
+
* a live capture if Azure's preview API version changes this shape.
|
|
27
|
+
*
|
|
28
|
+
* Tool-call dedup: the buffered Responses-API conversion in databricks.js
|
|
29
|
+
* (invokeAzureOpenAI) filters out exact duplicate {name, arguments}
|
|
30
|
+
* function-call signatures within one response (an observed GPT-5.x quirk).
|
|
31
|
+
* To preserve that guarantee here without needing the FULL response in hand
|
|
32
|
+
* first, this module buffers a function_call's argument fragments locally
|
|
33
|
+
* (never emitting them) until that call's response.function_call_arguments.done
|
|
34
|
+
* fires, checks the signature, and only then emits the accumulated deltas —
|
|
35
|
+
* as one burst — or silently drops them if it's a repeat. This means
|
|
36
|
+
* function-call arguments have a small, per-call delay (between that call's
|
|
37
|
+
* first and last argument fragment) while text streams fully live with zero
|
|
38
|
+
* delay — a deliberate tradeoff to keep the existing dedup guarantee, not an
|
|
39
|
+
* oversight.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
const logger = require("../logger");
|
|
43
|
+
const { _sseDataLines } = require("./sse-transformer");
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {ReadableStream|AsyncIterable} rawResponsesStream - raw Azure
|
|
47
|
+
* Responses API SSE byte stream (stream:true was set on the request).
|
|
48
|
+
* @param {Object} [opts]
|
|
49
|
+
* @param {string} [opts.model] - fallback model id for synthesized chunks.
|
|
50
|
+
* @returns {ReadableStream} synthetic OpenAI Chat-Completions SSE byte stream.
|
|
51
|
+
*/
|
|
52
|
+
function azureResponsesToOpenAIChunks(rawResponsesStream, opts = {}) {
|
|
53
|
+
const generator = _toOpenAIChunkText(rawResponsesStream, opts);
|
|
54
|
+
const encoder = new TextEncoder();
|
|
55
|
+
return new ReadableStream({
|
|
56
|
+
async pull(controller) {
|
|
57
|
+
try {
|
|
58
|
+
const { value, done } = await generator.next();
|
|
59
|
+
if (done) {
|
|
60
|
+
controller.close();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
controller.enqueue(encoder.encode(value));
|
|
64
|
+
} catch (err) {
|
|
65
|
+
controller.error(err);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
async cancel() {
|
|
69
|
+
try { await generator.return(); } catch { /* already finished */ }
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function* _toOpenAIChunkText(rawResponsesStream, opts) {
|
|
75
|
+
const id = `chatcmpl-azresp-${Date.now()}`;
|
|
76
|
+
const created = Math.floor(Date.now() / 1000);
|
|
77
|
+
const model = opts.model || "gpt-5.6-sol";
|
|
78
|
+
|
|
79
|
+
const chunk = (delta, extra = {}) => `data: ${JSON.stringify({
|
|
80
|
+
id, object: "chat.completion.chunk", created, model,
|
|
81
|
+
choices: [{ index: 0, delta, finish_reason: null }],
|
|
82
|
+
...extra,
|
|
83
|
+
})}\n\n`;
|
|
84
|
+
|
|
85
|
+
const finishChunk = (finishReason, usage) => `data: ${JSON.stringify({
|
|
86
|
+
id, object: "chat.completion.chunk", created, model,
|
|
87
|
+
choices: [{ index: 0, delta: {}, finish_reason: finishReason }],
|
|
88
|
+
...(usage ? { usage } : {}),
|
|
89
|
+
})}\n\n`;
|
|
90
|
+
|
|
91
|
+
// output_index -> {kind: "message"|"function_call", openaiToolIndex,
|
|
92
|
+
// name, callId, argBuf: string}. argBuf only used for function_call.
|
|
93
|
+
const items = new Map();
|
|
94
|
+
let nextToolIndex = 0;
|
|
95
|
+
const seenToolSignatures = new Set();
|
|
96
|
+
let sawFunctionCall = false;
|
|
97
|
+
let sawAnyOutput = false;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
for await (const payload of _sseDataLines(rawResponsesStream)) {
|
|
101
|
+
if (payload === "[DONE]") break;
|
|
102
|
+
let ev;
|
|
103
|
+
try { ev = JSON.parse(payload); } catch { continue; }
|
|
104
|
+
|
|
105
|
+
switch (ev.type) {
|
|
106
|
+
case "response.output_item.added": {
|
|
107
|
+
const item = ev.item || {};
|
|
108
|
+
if (item.type === "function_call") {
|
|
109
|
+
items.set(ev.output_index, {
|
|
110
|
+
kind: "function_call",
|
|
111
|
+
openaiToolIndex: nextToolIndex++,
|
|
112
|
+
name: item.name || "",
|
|
113
|
+
callId: item.call_id || item.id || "",
|
|
114
|
+
argBuf: "",
|
|
115
|
+
});
|
|
116
|
+
} else {
|
|
117
|
+
items.set(ev.output_index, { kind: "message" });
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
case "response.output_text.delta": {
|
|
123
|
+
sawAnyOutput = true;
|
|
124
|
+
if (ev.delta) yield chunk({ content: ev.delta });
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case "response.function_call_arguments.delta": {
|
|
129
|
+
const state = items.get(ev.output_index);
|
|
130
|
+
if (state && state.kind === "function_call") {
|
|
131
|
+
state.argBuf += ev.delta || "";
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
case "response.function_call_arguments.done": {
|
|
137
|
+
const state = items.get(ev.output_index);
|
|
138
|
+
if (!state || state.kind !== "function_call") break;
|
|
139
|
+
sawAnyOutput = true;
|
|
140
|
+
sawFunctionCall = true;
|
|
141
|
+
const finalArgs = typeof ev.arguments === "string" ? ev.arguments : state.argBuf;
|
|
142
|
+
const signature = `${state.name}:${finalArgs}`;
|
|
143
|
+
if (seenToolSignatures.has(signature)) {
|
|
144
|
+
logger.warn({
|
|
145
|
+
name: state.name,
|
|
146
|
+
argsPreview: finalArgs.slice(0, 120),
|
|
147
|
+
}, "[AzureResponsesSSE] Filtered duplicate streamed tool call");
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
seenToolSignatures.add(signature);
|
|
151
|
+
// First delta for this tool carries id/name (empty arguments);
|
|
152
|
+
// second carries the complete arguments as one fragment — matches
|
|
153
|
+
// real OpenAI streaming shape, which sse-transformer.js's toolAcc
|
|
154
|
+
// merge logic already expects (id/name first-non-null-wins, args
|
|
155
|
+
// concatenated across fragments).
|
|
156
|
+
yield chunk({
|
|
157
|
+
tool_calls: [{
|
|
158
|
+
index: state.openaiToolIndex,
|
|
159
|
+
id: state.callId,
|
|
160
|
+
type: "function",
|
|
161
|
+
function: { name: state.name, arguments: "" },
|
|
162
|
+
}],
|
|
163
|
+
});
|
|
164
|
+
yield chunk({
|
|
165
|
+
tool_calls: [{
|
|
166
|
+
index: state.openaiToolIndex,
|
|
167
|
+
function: { arguments: finalArgs },
|
|
168
|
+
}],
|
|
169
|
+
});
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case "response.completed": {
|
|
174
|
+
const usageRaw = ev.response?.usage;
|
|
175
|
+
const usage = usageRaw ? {
|
|
176
|
+
prompt_tokens: usageRaw.input_tokens ?? 0,
|
|
177
|
+
completion_tokens: usageRaw.output_tokens ?? 0,
|
|
178
|
+
total_tokens: usageRaw.total_tokens
|
|
179
|
+
?? ((usageRaw.input_tokens ?? 0) + (usageRaw.output_tokens ?? 0)),
|
|
180
|
+
prompt_tokens_details: {
|
|
181
|
+
cached_tokens: usageRaw.input_tokens_details?.cached_tokens ?? 0,
|
|
182
|
+
},
|
|
183
|
+
} : undefined;
|
|
184
|
+
const finishReason = sawFunctionCall ? "tool_calls" : "stop";
|
|
185
|
+
yield finishChunk(finishReason, usage);
|
|
186
|
+
yield "data: [DONE]\n\n";
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
case "response.failed":
|
|
191
|
+
case "response.incomplete": {
|
|
192
|
+
logger.warn({
|
|
193
|
+
type: ev.type,
|
|
194
|
+
error: ev.response?.error || ev.error || null,
|
|
195
|
+
}, "[AzureResponsesSSE] Upstream reported failure mid-stream");
|
|
196
|
+
// No finish_reason chunk, no [DONE] — matches sse-transformer.js's
|
|
197
|
+
// own no-finish-reason safety branch (its EOF-without-finish_reason
|
|
198
|
+
// case), which synthesizes an Anthropic error event rather than a
|
|
199
|
+
// false-success message_stop when downstream sees the SSE end here.
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
default:
|
|
204
|
+
break; // response.created/.in_progress, content_part.*, output_item.done — no chunk needed
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} catch (err) {
|
|
208
|
+
logger.warn({ err: err.message }, "[AzureResponsesSSE] Upstream stream failed mid-flight");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Stream ended without response.completed and without sawAnyOutput ever
|
|
213
|
+
// firing a finish — same "don't fake success" discipline as above.
|
|
214
|
+
if (!sawAnyOutput) {
|
|
215
|
+
logger.warn("[AzureResponsesSSE] Upstream stream ended with no output and no response.completed");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = {
|
|
220
|
+
azureResponsesToOpenAIChunks,
|
|
221
|
+
// Exported for unit tests.
|
|
222
|
+
_toOpenAIChunkText,
|
|
223
|
+
};
|