lynkr 3.3.1 → 4.1.0
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 +276 -2177
- package/README.md.backup +2996 -0
- package/docs/GSD_LEARNINGS.md +1116 -0
- package/docs/LOCAL_EMBEDDINGS_PLAN.md +1024 -0
- package/documentation/README.md +98 -0
- package/documentation/api.md +806 -0
- package/documentation/claude-code-cli.md +672 -0
- package/documentation/contributing.md +571 -0
- package/documentation/cursor-integration.md +731 -0
- package/documentation/docker.md +867 -0
- package/documentation/embeddings.md +760 -0
- package/documentation/faq.md +659 -0
- package/documentation/features.md +396 -0
- package/documentation/installation.md +706 -0
- package/documentation/memory-system.md +476 -0
- package/documentation/production.md +601 -0
- package/documentation/providers.md +735 -0
- package/documentation/testing.md +629 -0
- package/documentation/token-optimization.md +323 -0
- package/documentation/tools.md +697 -0
- package/documentation/troubleshooting.md +864 -0
- package/package.json +2 -2
- package/src/api/openai-router.js +919 -0
- package/src/api/router.js +4 -0
- package/src/clients/openai-format.js +427 -0
- package/src/config/index.js +8 -0
- package/test/cursor-integration.test.js +484 -0
package/src/api/router.js
CHANGED
|
@@ -3,6 +3,7 @@ const { processMessage } = require("../orchestrator");
|
|
|
3
3
|
const { getSession } = require("../sessions");
|
|
4
4
|
const metrics = require("../metrics");
|
|
5
5
|
const { createRateLimiter } = require("./middleware/rate-limiter");
|
|
6
|
+
const openaiRouter = require("./openai-router");
|
|
6
7
|
|
|
7
8
|
const router = express.Router();
|
|
8
9
|
|
|
@@ -383,4 +384,7 @@ router.get("/api/tokens/stats", (req, res) => {
|
|
|
383
384
|
}
|
|
384
385
|
});
|
|
385
386
|
|
|
387
|
+
// Mount OpenAI-compatible endpoints for Cursor IDE support
|
|
388
|
+
router.use("/v1", openaiRouter);
|
|
389
|
+
|
|
386
390
|
module.exports = router;
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI ↔ Anthropic Format Conversion Utilities
|
|
3
|
+
*
|
|
4
|
+
* Converts between OpenAI's /v1/chat/completions format and Anthropic's /v1/messages format.
|
|
5
|
+
* Used for Cursor IDE compatibility.
|
|
6
|
+
*
|
|
7
|
+
* @module clients/openai-format
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const logger = require("../logger");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Convert OpenAI chat completion request to Anthropic messages format
|
|
14
|
+
* @param {Object} openaiRequest - OpenAI format request
|
|
15
|
+
* @returns {Object} Anthropic format request
|
|
16
|
+
*/
|
|
17
|
+
function convertOpenAIToAnthropic(openaiRequest) {
|
|
18
|
+
const { messages, input, model, temperature, max_tokens, top_p, stream, tools, tool_choice } = openaiRequest;
|
|
19
|
+
|
|
20
|
+
// Cursor's inline edit uses "input" instead of "messages"
|
|
21
|
+
const messageArray = messages || input;
|
|
22
|
+
|
|
23
|
+
// Validate messages/input field
|
|
24
|
+
if (!messageArray) {
|
|
25
|
+
logger.error({
|
|
26
|
+
openaiRequest: JSON.stringify(openaiRequest),
|
|
27
|
+
hasMessages: !!messages,
|
|
28
|
+
hasInput: !!input,
|
|
29
|
+
messagesType: typeof messages,
|
|
30
|
+
inputType: typeof input
|
|
31
|
+
}, "convertOpenAIToAnthropic: neither messages nor input field present");
|
|
32
|
+
throw new Error("OpenAI request missing 'messages' or 'input' field");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!Array.isArray(messageArray)) {
|
|
36
|
+
logger.error({
|
|
37
|
+
messageArray: JSON.stringify(messageArray),
|
|
38
|
+
messageArrayType: typeof messageArray,
|
|
39
|
+
isArray: Array.isArray(messageArray)
|
|
40
|
+
}, "convertOpenAIToAnthropic: messages/input is not an array");
|
|
41
|
+
throw new Error(`OpenAI request 'messages'/'input' must be an array, got ${typeof messageArray}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Extract system message if present
|
|
45
|
+
let system = null;
|
|
46
|
+
const anthropicMessages = [];
|
|
47
|
+
|
|
48
|
+
for (const msg of messageArray) {
|
|
49
|
+
if (msg.role === "system") {
|
|
50
|
+
// Anthropic uses a separate system field
|
|
51
|
+
system = msg.content;
|
|
52
|
+
} else if (msg.role === "user" || msg.role === "assistant") {
|
|
53
|
+
// Convert content format
|
|
54
|
+
let content;
|
|
55
|
+
if (typeof msg.content === "string") {
|
|
56
|
+
content = msg.content;
|
|
57
|
+
} else if (Array.isArray(msg.content)) {
|
|
58
|
+
// OpenAI content parts format
|
|
59
|
+
content = msg.content.map(part => {
|
|
60
|
+
if (part.type === "text") {
|
|
61
|
+
return { type: "text", text: part.text };
|
|
62
|
+
} else if (part.type === "image_url") {
|
|
63
|
+
return {
|
|
64
|
+
type: "image",
|
|
65
|
+
source: {
|
|
66
|
+
type: "url",
|
|
67
|
+
url: part.image_url.url
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return part;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Handle tool calls in assistant messages (OpenAI format)
|
|
76
|
+
if (msg.role === "assistant" && msg.tool_calls) {
|
|
77
|
+
// Convert OpenAI tool_calls to Anthropic tool_use blocks
|
|
78
|
+
const contentBlocks = [];
|
|
79
|
+
|
|
80
|
+
// Add text content if present
|
|
81
|
+
if (msg.content) {
|
|
82
|
+
contentBlocks.push({ type: "text", text: msg.content });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Add tool use blocks
|
|
86
|
+
for (const toolCall of msg.tool_calls) {
|
|
87
|
+
contentBlocks.push({
|
|
88
|
+
type: "tool_use",
|
|
89
|
+
id: toolCall.id,
|
|
90
|
+
name: toolCall.function.name,
|
|
91
|
+
input: JSON.parse(toolCall.function.arguments)
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
anthropicMessages.push({
|
|
96
|
+
role: "assistant",
|
|
97
|
+
content: contentBlocks
|
|
98
|
+
});
|
|
99
|
+
} else {
|
|
100
|
+
anthropicMessages.push({
|
|
101
|
+
role: msg.role,
|
|
102
|
+
content
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
} else if (msg.role === "tool") {
|
|
106
|
+
// OpenAI tool response → Anthropic tool_result
|
|
107
|
+
const previousMsg = anthropicMessages[anthropicMessages.length - 1];
|
|
108
|
+
|
|
109
|
+
// Tool results must follow assistant message with tool_use
|
|
110
|
+
// Add as separate user message with tool_result
|
|
111
|
+
anthropicMessages.push({
|
|
112
|
+
role: "user",
|
|
113
|
+
content: [
|
|
114
|
+
{
|
|
115
|
+
type: "tool_result",
|
|
116
|
+
tool_use_id: msg.tool_call_id,
|
|
117
|
+
content: msg.content
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Convert tools format (OpenAI → Anthropic)
|
|
125
|
+
let anthropicTools = null;
|
|
126
|
+
if (tools && tools.length > 0) {
|
|
127
|
+
anthropicTools = tools.map(tool => ({
|
|
128
|
+
name: tool.function.name,
|
|
129
|
+
description: tool.function.description || "",
|
|
130
|
+
input_schema: tool.function.parameters || {
|
|
131
|
+
type: "object",
|
|
132
|
+
properties: {},
|
|
133
|
+
required: []
|
|
134
|
+
}
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Build Anthropic request
|
|
139
|
+
const anthropicRequest = {
|
|
140
|
+
model: model || "claude-3-5-sonnet-20241022",
|
|
141
|
+
messages: anthropicMessages,
|
|
142
|
+
max_tokens: max_tokens || 4096,
|
|
143
|
+
stream: stream || false
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (system) {
|
|
147
|
+
anthropicRequest.system = system;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (temperature !== undefined) {
|
|
151
|
+
anthropicRequest.temperature = temperature;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (top_p !== undefined) {
|
|
155
|
+
anthropicRequest.top_p = top_p;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (anthropicTools) {
|
|
159
|
+
anthropicRequest.tools = anthropicTools;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Handle tool_choice
|
|
163
|
+
if (tool_choice) {
|
|
164
|
+
if (tool_choice === "auto") {
|
|
165
|
+
anthropicRequest.tool_choice = { type: "auto" };
|
|
166
|
+
} else if (tool_choice === "none") {
|
|
167
|
+
anthropicRequest.tool_choice = { type: "none" };
|
|
168
|
+
} else if (typeof tool_choice === "object" && tool_choice.function) {
|
|
169
|
+
anthropicRequest.tool_choice = {
|
|
170
|
+
type: "tool",
|
|
171
|
+
name: tool_choice.function.name
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
logger.debug({
|
|
177
|
+
openaiMessageCount: messageArray.length,
|
|
178
|
+
anthropicMessageCount: anthropicMessages.length,
|
|
179
|
+
hasSystem: !!system,
|
|
180
|
+
hasTools: !!anthropicTools,
|
|
181
|
+
toolCount: anthropicTools?.length || 0
|
|
182
|
+
}, "Converted OpenAI request to Anthropic format");
|
|
183
|
+
|
|
184
|
+
return anthropicRequest;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Convert Anthropic messages response to OpenAI chat completion format
|
|
189
|
+
* @param {Object} anthropicResponse - Anthropic format response
|
|
190
|
+
* @param {string} model - Model name to include in response
|
|
191
|
+
* @returns {Object} OpenAI format response
|
|
192
|
+
*/
|
|
193
|
+
function convertAnthropicToOpenAI(anthropicResponse, model = "claude-3-5-sonnet-20241022") {
|
|
194
|
+
// Validate input
|
|
195
|
+
if (!anthropicResponse) {
|
|
196
|
+
throw new Error("convertAnthropicToOpenAI: anthropicResponse is undefined or null");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const { id, content, stop_reason, usage } = anthropicResponse;
|
|
200
|
+
|
|
201
|
+
// Validate required fields
|
|
202
|
+
if (!content || !Array.isArray(content)) {
|
|
203
|
+
throw new Error(`convertAnthropicToOpenAI: invalid content field (got ${typeof content})`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Convert content blocks to OpenAI format
|
|
207
|
+
let messageContent = "";
|
|
208
|
+
const toolCalls = [];
|
|
209
|
+
|
|
210
|
+
for (const block of content) {
|
|
211
|
+
if (block.type === "text") {
|
|
212
|
+
messageContent += block.text;
|
|
213
|
+
} else if (block.type === "tool_use") {
|
|
214
|
+
toolCalls.push({
|
|
215
|
+
id: block.id,
|
|
216
|
+
type: "function",
|
|
217
|
+
function: {
|
|
218
|
+
name: block.name,
|
|
219
|
+
arguments: JSON.stringify(block.input)
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Build OpenAI response
|
|
226
|
+
const openaiResponse = {
|
|
227
|
+
id: id || `chatcmpl-${Date.now()}`,
|
|
228
|
+
object: "chat.completion",
|
|
229
|
+
created: Math.floor(Date.now() / 1000),
|
|
230
|
+
model: model,
|
|
231
|
+
choices: [
|
|
232
|
+
{
|
|
233
|
+
index: 0,
|
|
234
|
+
message: {
|
|
235
|
+
role: "assistant",
|
|
236
|
+
content: messageContent || null
|
|
237
|
+
},
|
|
238
|
+
finish_reason: mapStopReason(stop_reason)
|
|
239
|
+
}
|
|
240
|
+
],
|
|
241
|
+
usage: {
|
|
242
|
+
prompt_tokens: usage?.input_tokens || 0,
|
|
243
|
+
completion_tokens: usage?.output_tokens || 0,
|
|
244
|
+
total_tokens: (usage?.input_tokens || 0) + (usage?.output_tokens || 0)
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Add tool_calls if present
|
|
249
|
+
if (toolCalls.length > 0) {
|
|
250
|
+
openaiResponse.choices[0].message.tool_calls = toolCalls;
|
|
251
|
+
openaiResponse.choices[0].finish_reason = "tool_calls";
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
logger.debug({
|
|
255
|
+
anthropicStopReason: stop_reason,
|
|
256
|
+
openaiFinishReason: openaiResponse.choices[0].finish_reason,
|
|
257
|
+
hasToolCalls: toolCalls.length > 0,
|
|
258
|
+
messageLength: messageContent.length
|
|
259
|
+
}, "Converted Anthropic response to OpenAI format");
|
|
260
|
+
|
|
261
|
+
return openaiResponse;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Convert Anthropic streaming chunk to OpenAI streaming format
|
|
266
|
+
* @param {Object} chunk - Anthropic SSE event
|
|
267
|
+
* @param {string} model - Model name
|
|
268
|
+
* @returns {string} OpenAI format SSE line (data: {...})
|
|
269
|
+
*/
|
|
270
|
+
function convertAnthropicStreamChunkToOpenAI(chunk, model = "claude-3-5-sonnet-20241022") {
|
|
271
|
+
const eventType = chunk.type;
|
|
272
|
+
|
|
273
|
+
if (eventType === "message_start") {
|
|
274
|
+
// Initial message metadata
|
|
275
|
+
return {
|
|
276
|
+
id: chunk.message?.id || `chatcmpl-${Date.now()}`,
|
|
277
|
+
object: "chat.completion.chunk",
|
|
278
|
+
created: Math.floor(Date.now() / 1000),
|
|
279
|
+
model: model,
|
|
280
|
+
choices: [
|
|
281
|
+
{
|
|
282
|
+
index: 0,
|
|
283
|
+
delta: { role: "assistant", content: "" },
|
|
284
|
+
finish_reason: null
|
|
285
|
+
}
|
|
286
|
+
]
|
|
287
|
+
};
|
|
288
|
+
} else if (eventType === "content_block_start") {
|
|
289
|
+
// Start of content block (text or tool_use)
|
|
290
|
+
const contentBlock = chunk.content_block;
|
|
291
|
+
|
|
292
|
+
if (contentBlock?.type === "tool_use") {
|
|
293
|
+
return {
|
|
294
|
+
id: `chatcmpl-${Date.now()}`,
|
|
295
|
+
object: "chat.completion.chunk",
|
|
296
|
+
created: Math.floor(Date.now() / 1000),
|
|
297
|
+
model: model,
|
|
298
|
+
choices: [
|
|
299
|
+
{
|
|
300
|
+
index: 0,
|
|
301
|
+
delta: {
|
|
302
|
+
tool_calls: [
|
|
303
|
+
{
|
|
304
|
+
index: chunk.index,
|
|
305
|
+
id: contentBlock.id,
|
|
306
|
+
type: "function",
|
|
307
|
+
function: {
|
|
308
|
+
name: contentBlock.name,
|
|
309
|
+
arguments: ""
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
]
|
|
313
|
+
},
|
|
314
|
+
finish_reason: null
|
|
315
|
+
}
|
|
316
|
+
]
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
} else if (eventType === "content_block_delta") {
|
|
320
|
+
// Incremental content
|
|
321
|
+
const delta = chunk.delta;
|
|
322
|
+
|
|
323
|
+
if (delta?.type === "text_delta") {
|
|
324
|
+
return {
|
|
325
|
+
id: `chatcmpl-${Date.now()}`,
|
|
326
|
+
object: "chat.completion.chunk",
|
|
327
|
+
created: Math.floor(Date.now() / 1000),
|
|
328
|
+
model: model,
|
|
329
|
+
choices: [
|
|
330
|
+
{
|
|
331
|
+
index: 0,
|
|
332
|
+
delta: { content: delta.text },
|
|
333
|
+
finish_reason: null
|
|
334
|
+
}
|
|
335
|
+
]
|
|
336
|
+
};
|
|
337
|
+
} else if (delta?.type === "input_json_delta") {
|
|
338
|
+
// Tool call arguments streaming
|
|
339
|
+
return {
|
|
340
|
+
id: `chatcmpl-${Date.now()}`,
|
|
341
|
+
object: "chat.completion.chunk",
|
|
342
|
+
created: Math.floor(Date.now() / 1000),
|
|
343
|
+
model: model,
|
|
344
|
+
choices: [
|
|
345
|
+
{
|
|
346
|
+
index: 0,
|
|
347
|
+
delta: {
|
|
348
|
+
tool_calls: [
|
|
349
|
+
{
|
|
350
|
+
index: chunk.index,
|
|
351
|
+
function: {
|
|
352
|
+
arguments: delta.partial_json
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
]
|
|
356
|
+
},
|
|
357
|
+
finish_reason: null
|
|
358
|
+
}
|
|
359
|
+
]
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
} else if (eventType === "message_delta") {
|
|
363
|
+
// Final message metadata (stop reason, usage)
|
|
364
|
+
const stopReason = chunk.delta?.stop_reason;
|
|
365
|
+
const usage = chunk.usage;
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
id: `chatcmpl-${Date.now()}`,
|
|
369
|
+
object: "chat.completion.chunk",
|
|
370
|
+
created: Math.floor(Date.now() / 1000),
|
|
371
|
+
model: model,
|
|
372
|
+
choices: [
|
|
373
|
+
{
|
|
374
|
+
index: 0,
|
|
375
|
+
delta: {},
|
|
376
|
+
finish_reason: mapStopReason(stopReason)
|
|
377
|
+
}
|
|
378
|
+
],
|
|
379
|
+
usage: usage ? {
|
|
380
|
+
prompt_tokens: 0, // Not available in streaming
|
|
381
|
+
completion_tokens: usage.output_tokens || 0,
|
|
382
|
+
total_tokens: usage.output_tokens || 0
|
|
383
|
+
} : undefined
|
|
384
|
+
};
|
|
385
|
+
} else if (eventType === "message_stop") {
|
|
386
|
+
// End of stream
|
|
387
|
+
return {
|
|
388
|
+
id: `chatcmpl-${Date.now()}`,
|
|
389
|
+
object: "chat.completion.chunk",
|
|
390
|
+
created: Math.floor(Date.now() / 1000),
|
|
391
|
+
model: model,
|
|
392
|
+
choices: [
|
|
393
|
+
{
|
|
394
|
+
index: 0,
|
|
395
|
+
delta: {},
|
|
396
|
+
finish_reason: "stop"
|
|
397
|
+
}
|
|
398
|
+
]
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Unknown event type, return empty chunk
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Map Anthropic stop_reason to OpenAI finish_reason
|
|
408
|
+
* @param {string} stopReason - Anthropic stop reason
|
|
409
|
+
* @returns {string} OpenAI finish reason
|
|
410
|
+
*/
|
|
411
|
+
function mapStopReason(stopReason) {
|
|
412
|
+
const mapping = {
|
|
413
|
+
"end_turn": "stop",
|
|
414
|
+
"max_tokens": "length",
|
|
415
|
+
"stop_sequence": "stop",
|
|
416
|
+
"tool_use": "tool_calls"
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
return mapping[stopReason] || "stop";
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
module.exports = {
|
|
423
|
+
convertOpenAIToAnthropic,
|
|
424
|
+
convertAnthropicToOpenAI,
|
|
425
|
+
convertAnthropicStreamChunkToOpenAI,
|
|
426
|
+
mapStopReason
|
|
427
|
+
};
|
package/src/config/index.js
CHANGED
|
@@ -78,10 +78,13 @@ const azureAnthropicVersion = process.env.AZURE_ANTHROPIC_VERSION ?? "2023-06-01
|
|
|
78
78
|
const ollamaEndpoint = process.env.OLLAMA_ENDPOINT ?? "http://localhost:11434";
|
|
79
79
|
const ollamaModel = process.env.OLLAMA_MODEL ?? "qwen2.5-coder:7b";
|
|
80
80
|
const ollamaTimeout = Number.parseInt(process.env.OLLAMA_TIMEOUT_MS ?? "120000", 10);
|
|
81
|
+
const ollamaEmbeddingsEndpoint = process.env.OLLAMA_EMBEDDINGS_ENDPOINT ?? `${ollamaEndpoint}/api/embeddings`;
|
|
82
|
+
const ollamaEmbeddingsModel = process.env.OLLAMA_EMBEDDINGS_MODEL ?? "nomic-embed-text";
|
|
81
83
|
|
|
82
84
|
// OpenRouter configuration
|
|
83
85
|
const openRouterApiKey = process.env.OPENROUTER_API_KEY ?? null;
|
|
84
86
|
const openRouterModel = process.env.OPENROUTER_MODEL ?? "openai/gpt-4o-mini";
|
|
87
|
+
const openRouterEmbeddingsModel = process.env.OPENROUTER_EMBEDDINGS_MODEL ?? "openai/text-embedding-ada-002";
|
|
85
88
|
const openRouterEndpoint = process.env.OPENROUTER_ENDPOINT ?? "https://openrouter.ai/api/v1/chat/completions";
|
|
86
89
|
|
|
87
90
|
// Azure OpenAI configuration
|
|
@@ -101,6 +104,7 @@ const llamacppEndpoint = process.env.LLAMACPP_ENDPOINT?.trim() || "http://localh
|
|
|
101
104
|
const llamacppModel = process.env.LLAMACPP_MODEL?.trim() || "default";
|
|
102
105
|
const llamacppTimeout = Number.parseInt(process.env.LLAMACPP_TIMEOUT_MS ?? "120000", 10);
|
|
103
106
|
const llamacppApiKey = process.env.LLAMACPP_API_KEY?.trim() || null;
|
|
107
|
+
const llamacppEmbeddingsEndpoint = process.env.LLAMACPP_EMBEDDINGS_ENDPOINT?.trim() || `${llamacppEndpoint}/embeddings`;
|
|
104
108
|
|
|
105
109
|
// LM Studio configuration
|
|
106
110
|
const lmstudioEndpoint = process.env.LMSTUDIO_ENDPOINT?.trim() || "http://localhost:1234";
|
|
@@ -433,10 +437,13 @@ const config = {
|
|
|
433
437
|
endpoint: ollamaEndpoint,
|
|
434
438
|
model: ollamaModel,
|
|
435
439
|
timeout: Number.isNaN(ollamaTimeout) ? 120000 : ollamaTimeout,
|
|
440
|
+
embeddingsEndpoint: ollamaEmbeddingsEndpoint,
|
|
441
|
+
embeddingsModel: ollamaEmbeddingsModel,
|
|
436
442
|
},
|
|
437
443
|
openrouter: {
|
|
438
444
|
apiKey: openRouterApiKey,
|
|
439
445
|
model: openRouterModel,
|
|
446
|
+
embeddingsModel: openRouterEmbeddingsModel,
|
|
440
447
|
endpoint: openRouterEndpoint,
|
|
441
448
|
},
|
|
442
449
|
azureOpenAI: {
|
|
@@ -456,6 +463,7 @@ const config = {
|
|
|
456
463
|
model: llamacppModel,
|
|
457
464
|
timeout: Number.isNaN(llamacppTimeout) ? 120000 : llamacppTimeout,
|
|
458
465
|
apiKey: llamacppApiKey,
|
|
466
|
+
embeddingsEndpoint: llamacppEmbeddingsEndpoint,
|
|
459
467
|
},
|
|
460
468
|
lmstudio: {
|
|
461
469
|
endpoint: lmstudioEndpoint,
|