ydc-mcp-server 1.6.1 ā 1.7.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/index.js +1 -1
- package/lib/anthropic-mapper.js +248 -0
- package/lib/conversation-store.js +151 -64
- package/lib/routes/anthropic-messages.js +196 -0
- package/openai-server.js +8 -2
- package/package.json +4 -4
package/index.js
CHANGED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic/Claude Parameter Mapper Module
|
|
3
|
+
* Maps Anthropic API parameters to You.com API parameters
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
getAdvancedVersion,
|
|
8
|
+
isAdvancedVersion,
|
|
9
|
+
getDefaultAdvancedVersion,
|
|
10
|
+
adjustWorkflowSteps
|
|
11
|
+
} from './advanced-versions.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Map Anthropic request parameters to You.com parameters
|
|
15
|
+
*/
|
|
16
|
+
export function mapAnthropicToYouParams(anthropicRequest) {
|
|
17
|
+
const {
|
|
18
|
+
model = 'advanced-3.0-high',
|
|
19
|
+
messages,
|
|
20
|
+
system,
|
|
21
|
+
temperature = 0.7,
|
|
22
|
+
max_tokens = 1024,
|
|
23
|
+
stream = false
|
|
24
|
+
} = anthropicRequest;
|
|
25
|
+
|
|
26
|
+
let input = '';
|
|
27
|
+
const conversationHistory = [];
|
|
28
|
+
|
|
29
|
+
// Handle system prompt
|
|
30
|
+
if (system) {
|
|
31
|
+
input = `[System Instructions]\n${system}\n\n`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Process messages
|
|
35
|
+
messages.forEach(msg => {
|
|
36
|
+
if (msg.role === 'user') {
|
|
37
|
+
// Handle content as string or array
|
|
38
|
+
const content = typeof msg.content === 'string'
|
|
39
|
+
? msg.content
|
|
40
|
+
: msg.content.filter(c => c.type === 'text').map(c => c.text).join('\n');
|
|
41
|
+
conversationHistory.push(`User: ${content}`);
|
|
42
|
+
} else if (msg.role === 'assistant') {
|
|
43
|
+
const content = typeof msg.content === 'string'
|
|
44
|
+
? msg.content
|
|
45
|
+
: msg.content.filter(c => c.type === 'text').map(c => c.text).join('\n');
|
|
46
|
+
conversationHistory.push(`Assistant: ${content}`);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (conversationHistory.length > 1) {
|
|
51
|
+
input += `[Conversation History]\n${conversationHistory.slice(0, -1).join('\n\n')}\n\n`;
|
|
52
|
+
input += `[Current Message]\n${conversationHistory[conversationHistory.length - 1].replace(/^User: /, '')}`;
|
|
53
|
+
} else if (conversationHistory.length === 1) {
|
|
54
|
+
input += conversationHistory[0].replace(/^User: /, '');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Map model to You.com agent
|
|
58
|
+
const modelMapping = {
|
|
59
|
+
'claude-3-opus-20240229': 'advanced-3.0-high',
|
|
60
|
+
'claude-3-sonnet-20240229': 'advanced-3.0-medium',
|
|
61
|
+
'claude-3-haiku-20240307': 'express',
|
|
62
|
+
'claude-3-5-sonnet-20240620': 'advanced-3.0-high',
|
|
63
|
+
'claude-3-5-sonnet-20241022': 'advanced-3.0-high',
|
|
64
|
+
'claude-sonnet-4-20250514': 'advanced-3.0-high',
|
|
65
|
+
'claude-sonnet-4-5-20250929': 'advanced-4.5-research'
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const mappedModel = modelMapping[model] || model;
|
|
69
|
+
|
|
70
|
+
// Check if it's an advanced version model
|
|
71
|
+
if (isAdvancedVersion(mappedModel)) {
|
|
72
|
+
const versionConfig = getAdvancedVersion(mappedModel);
|
|
73
|
+
if (versionConfig) {
|
|
74
|
+
const adjustedSteps = adjustWorkflowSteps(versionConfig.max_workflow_steps, temperature);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
agent: 'advanced',
|
|
78
|
+
input,
|
|
79
|
+
stream,
|
|
80
|
+
verbosity: versionConfig.verbosity,
|
|
81
|
+
tools: versionConfig.tools,
|
|
82
|
+
workflow_config: {
|
|
83
|
+
max_workflow_steps: adjustedSteps
|
|
84
|
+
},
|
|
85
|
+
timeout: versionConfig.timeout
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Default handling
|
|
91
|
+
let agent = mappedModel;
|
|
92
|
+
let verbosity = 'medium';
|
|
93
|
+
let timeout = 300000;
|
|
94
|
+
|
|
95
|
+
if (temperature <= 0.3) verbosity = 'medium';
|
|
96
|
+
else if (temperature >= 0.8) verbosity = 'high';
|
|
97
|
+
|
|
98
|
+
const max_workflow_steps = Math.min(Math.max(Math.floor(max_tokens / 100), 1), 20);
|
|
99
|
+
|
|
100
|
+
const knownAgents = ['express', 'research', 'advanced'];
|
|
101
|
+
if (!knownAgents.includes(agent)) {
|
|
102
|
+
agent = 'advanced';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (agent === 'advanced') {
|
|
106
|
+
timeout = 3000000;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
agent,
|
|
111
|
+
input,
|
|
112
|
+
stream,
|
|
113
|
+
verbosity,
|
|
114
|
+
tools: [
|
|
115
|
+
{ type: 'research', search_effort: 'auto', report_verbosity: 'medium' },
|
|
116
|
+
{ type: 'compute' }
|
|
117
|
+
],
|
|
118
|
+
workflow_config: {
|
|
119
|
+
max_workflow_steps
|
|
120
|
+
},
|
|
121
|
+
timeout
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Convert You.com response to Anthropic format
|
|
127
|
+
*/
|
|
128
|
+
export function convertToAnthropicResponse(youResponse, model, inputTokens = 100) {
|
|
129
|
+
const content = youResponse.output && Array.isArray(youResponse.output)
|
|
130
|
+
? youResponse.output
|
|
131
|
+
.filter(item => item.type === 'message.answer')
|
|
132
|
+
.map(item => item.text)
|
|
133
|
+
.join('\n\n')
|
|
134
|
+
: 'No response content';
|
|
135
|
+
|
|
136
|
+
const outputTokens = Math.floor(content.length / 4);
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
id: `msg_${Date.now()}`,
|
|
140
|
+
type: 'message',
|
|
141
|
+
role: 'assistant',
|
|
142
|
+
content: [
|
|
143
|
+
{
|
|
144
|
+
type: 'text',
|
|
145
|
+
text: content
|
|
146
|
+
}
|
|
147
|
+
],
|
|
148
|
+
model: model,
|
|
149
|
+
stop_reason: 'end_turn',
|
|
150
|
+
stop_sequence: null,
|
|
151
|
+
usage: {
|
|
152
|
+
input_tokens: inputTokens,
|
|
153
|
+
output_tokens: outputTokens
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Create streaming event in Anthropic format
|
|
160
|
+
*/
|
|
161
|
+
export function createAnthropicStreamEvent(eventType, data) {
|
|
162
|
+
return `event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Create message_start event
|
|
167
|
+
*/
|
|
168
|
+
export function createMessageStartEvent(model) {
|
|
169
|
+
return createAnthropicStreamEvent('message_start', {
|
|
170
|
+
type: 'message_start',
|
|
171
|
+
message: {
|
|
172
|
+
id: `msg_${Date.now()}`,
|
|
173
|
+
type: 'message',
|
|
174
|
+
role: 'assistant',
|
|
175
|
+
content: [],
|
|
176
|
+
model: model,
|
|
177
|
+
stop_reason: null,
|
|
178
|
+
stop_sequence: null,
|
|
179
|
+
usage: {
|
|
180
|
+
input_tokens: 0,
|
|
181
|
+
output_tokens: 0
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Create content_block_start event
|
|
189
|
+
*/
|
|
190
|
+
export function createContentBlockStartEvent(index = 0) {
|
|
191
|
+
return createAnthropicStreamEvent('content_block_start', {
|
|
192
|
+
type: 'content_block_start',
|
|
193
|
+
index: index,
|
|
194
|
+
content_block: {
|
|
195
|
+
type: 'text',
|
|
196
|
+
text: ''
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Create content_block_delta event
|
|
203
|
+
*/
|
|
204
|
+
export function createContentBlockDeltaEvent(text, index = 0) {
|
|
205
|
+
return createAnthropicStreamEvent('content_block_delta', {
|
|
206
|
+
type: 'content_block_delta',
|
|
207
|
+
index: index,
|
|
208
|
+
delta: {
|
|
209
|
+
type: 'text_delta',
|
|
210
|
+
text: text
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Create content_block_stop event
|
|
217
|
+
*/
|
|
218
|
+
export function createContentBlockStopEvent(index = 0) {
|
|
219
|
+
return createAnthropicStreamEvent('content_block_stop', {
|
|
220
|
+
type: 'content_block_stop',
|
|
221
|
+
index: index
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Create message_delta event
|
|
227
|
+
*/
|
|
228
|
+
export function createMessageDeltaEvent(outputTokens = 0) {
|
|
229
|
+
return createAnthropicStreamEvent('message_delta', {
|
|
230
|
+
type: 'message_delta',
|
|
231
|
+
delta: {
|
|
232
|
+
stop_reason: 'end_turn',
|
|
233
|
+
stop_sequence: null
|
|
234
|
+
},
|
|
235
|
+
usage: {
|
|
236
|
+
output_tokens: outputTokens
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Create message_stop event
|
|
243
|
+
*/
|
|
244
|
+
export function createMessageStopEvent() {
|
|
245
|
+
return createAnthropicStreamEvent('message_stop', {
|
|
246
|
+
type: 'message_stop'
|
|
247
|
+
});
|
|
248
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Conversation Store Module
|
|
3
|
-
* Supports SQLite (persistent) and Memory (in-memory) storage
|
|
3
|
+
* Supports SQLite (persistent via sql.js) and Memory (in-memory) storage
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import Database from 'better-sqlite3';
|
|
7
6
|
import { fileURLToPath } from 'url';
|
|
8
7
|
import { dirname, join } from 'path';
|
|
8
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
9
9
|
import crypto from 'crypto';
|
|
10
10
|
|
|
11
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -21,26 +21,66 @@ const MAX_MESSAGES_PER_CONVERSATION = 100;
|
|
|
21
21
|
// Memory store (fallback)
|
|
22
22
|
const memoryStore = new Map();
|
|
23
23
|
|
|
24
|
-
// SQLite database
|
|
24
|
+
// SQLite database (sql.js)
|
|
25
25
|
let db = null;
|
|
26
|
+
let SQL = null;
|
|
27
|
+
let dbInitialized = false;
|
|
28
|
+
|
|
29
|
+
// Save database to file periodically
|
|
30
|
+
let saveTimeout = null;
|
|
31
|
+
function scheduleSave() {
|
|
32
|
+
if (saveTimeout) clearTimeout(saveTimeout);
|
|
33
|
+
saveTimeout = setTimeout(() => {
|
|
34
|
+
if (db && STORE_TYPE === 'sqlite') {
|
|
35
|
+
try {
|
|
36
|
+
const data = db.export();
|
|
37
|
+
writeFileSync(DB_PATH, Buffer.from(data));
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('ā ļø Failed to save database:', error.message);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}, 1000); // Debounce saves by 1 second
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function initDatabase() {
|
|
46
|
+
if (dbInitialized) return;
|
|
47
|
+
dbInitialized = true;
|
|
26
48
|
|
|
27
|
-
function initDatabase() {
|
|
28
49
|
if (STORE_TYPE === 'memory') {
|
|
29
50
|
console.log('š¦ Using in-memory conversation store');
|
|
30
51
|
return;
|
|
31
52
|
}
|
|
32
53
|
|
|
33
54
|
try {
|
|
34
|
-
|
|
55
|
+
// Dynamic import sql.js
|
|
56
|
+
const initSqlJs = (await import('sql.js')).default;
|
|
57
|
+
SQL = await initSqlJs();
|
|
35
58
|
|
|
36
|
-
|
|
59
|
+
// Load existing database or create new one
|
|
60
|
+
if (existsSync(DB_PATH)) {
|
|
61
|
+
try {
|
|
62
|
+
const fileBuffer = readFileSync(DB_PATH);
|
|
63
|
+
db = new SQL.Database(fileBuffer);
|
|
64
|
+
console.log(`š¦ Loaded SQLite database: ${DB_PATH}`);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('ā ļø Failed to load existing database, creating new one:', error.message);
|
|
67
|
+
db = new SQL.Database();
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
db = new SQL.Database();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Create tables
|
|
74
|
+
db.run(`
|
|
37
75
|
CREATE TABLE IF NOT EXISTS conversations (
|
|
38
76
|
id TEXT PRIMARY KEY,
|
|
39
77
|
metadata TEXT DEFAULT '{}',
|
|
40
78
|
created_at INTEGER NOT NULL,
|
|
41
79
|
updated_at INTEGER NOT NULL
|
|
42
|
-
)
|
|
43
|
-
|
|
80
|
+
)
|
|
81
|
+
`);
|
|
82
|
+
|
|
83
|
+
db.run(`
|
|
44
84
|
CREATE TABLE IF NOT EXISTS messages (
|
|
45
85
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
46
86
|
conversation_id TEXT NOT NULL,
|
|
@@ -48,12 +88,15 @@ function initDatabase() {
|
|
|
48
88
|
content TEXT NOT NULL,
|
|
49
89
|
timestamp INTEGER NOT NULL,
|
|
50
90
|
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id);
|
|
54
|
-
CREATE INDEX IF NOT EXISTS idx_conversations_updated ON conversations(updated_at);
|
|
91
|
+
)
|
|
55
92
|
`);
|
|
56
93
|
|
|
94
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id)`);
|
|
95
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_conversations_updated ON conversations(updated_at)`);
|
|
96
|
+
|
|
97
|
+
// Save initial database
|
|
98
|
+
scheduleSave();
|
|
99
|
+
|
|
57
100
|
console.log(`š¦ Using SQLite conversation store: ${DB_PATH}`);
|
|
58
101
|
} catch (error) {
|
|
59
102
|
console.error('ā ļø Failed to initialize SQLite, falling back to memory store:', error.message);
|
|
@@ -61,8 +104,8 @@ function initDatabase() {
|
|
|
61
104
|
}
|
|
62
105
|
}
|
|
63
106
|
|
|
64
|
-
//
|
|
65
|
-
initDatabase
|
|
107
|
+
// Export init function for async initialization
|
|
108
|
+
export { initDatabase };
|
|
66
109
|
|
|
67
110
|
export function generateConversationId() {
|
|
68
111
|
return crypto.randomUUID();
|
|
@@ -78,17 +121,27 @@ export function getConversation(conversationId) {
|
|
|
78
121
|
return conv;
|
|
79
122
|
}
|
|
80
123
|
|
|
81
|
-
const
|
|
82
|
-
if (!
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
124
|
+
const convResult = db.exec('SELECT * FROM conversations WHERE id = ?', [conversationId]);
|
|
125
|
+
if (!convResult.length || !convResult[0].values.length) return null;
|
|
126
|
+
|
|
127
|
+
const conv = convResult[0].values[0];
|
|
128
|
+
const [id, metadata, created_at, updated_at] = conv;
|
|
129
|
+
|
|
130
|
+
const messagesResult = db.exec('SELECT role, content, timestamp FROM messages WHERE conversation_id = ? ORDER BY id', [conversationId]);
|
|
131
|
+
const messages = messagesResult.length ? messagesResult[0].values.map(row => ({
|
|
132
|
+
role: row[0],
|
|
133
|
+
content: row[1],
|
|
134
|
+
timestamp: row[2]
|
|
135
|
+
})) : [];
|
|
136
|
+
|
|
137
|
+
db.run('UPDATE conversations SET updated_at = ? WHERE id = ?', [Date.now(), conversationId]);
|
|
138
|
+
scheduleSave();
|
|
86
139
|
|
|
87
140
|
return {
|
|
88
|
-
id
|
|
141
|
+
id,
|
|
89
142
|
messages,
|
|
90
|
-
metadata: JSON.parse(
|
|
91
|
-
createdAt:
|
|
143
|
+
metadata: JSON.parse(metadata || '{}'),
|
|
144
|
+
createdAt: created_at,
|
|
92
145
|
updatedAt: Date.now()
|
|
93
146
|
};
|
|
94
147
|
}
|
|
@@ -103,8 +156,9 @@ export function createConversation(conversationId = null, metadata = {}) {
|
|
|
103
156
|
return conv;
|
|
104
157
|
}
|
|
105
158
|
|
|
106
|
-
db.
|
|
107
|
-
|
|
159
|
+
db.run('INSERT OR REPLACE INTO conversations (id, metadata, created_at, updated_at) VALUES (?, ?, ?, ?)',
|
|
160
|
+
[id, JSON.stringify(metadata), now, now]);
|
|
161
|
+
scheduleSave();
|
|
108
162
|
|
|
109
163
|
return { id, messages: [], createdAt: now, updatedAt: now, metadata };
|
|
110
164
|
}
|
|
@@ -128,33 +182,49 @@ export function addMessageToConversation(conversationId, role, content) {
|
|
|
128
182
|
return conv;
|
|
129
183
|
}
|
|
130
184
|
|
|
131
|
-
const existing = db.
|
|
132
|
-
if (!existing) {
|
|
185
|
+
const existing = db.exec('SELECT id FROM conversations WHERE id = ?', [conversationId]);
|
|
186
|
+
if (!existing.length || !existing[0].values.length) {
|
|
133
187
|
createConversation(conversationId);
|
|
134
188
|
}
|
|
135
189
|
|
|
136
|
-
const
|
|
190
|
+
const countResult = db.exec('SELECT COUNT(*) as count FROM messages WHERE conversation_id = ?', [conversationId]);
|
|
191
|
+
const count = countResult.length ? countResult[0].values[0][0] : 0;
|
|
192
|
+
|
|
137
193
|
if (count >= MAX_MESSAGES_PER_CONVERSATION) {
|
|
138
|
-
const
|
|
194
|
+
const systemMsgResult = db.exec("SELECT id FROM messages WHERE conversation_id = ? AND role = 'system' LIMIT 1", [conversationId]);
|
|
139
195
|
const deleteCount = count - MAX_MESSAGES_PER_CONVERSATION + 2;
|
|
140
196
|
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
197
|
+
if (systemMsgResult.length && systemMsgResult[0].values.length) {
|
|
198
|
+
const systemId = systemMsgResult[0].values[0][0];
|
|
199
|
+
// Delete oldest messages except system message
|
|
200
|
+
const toDeleteResult = db.exec('SELECT id FROM messages WHERE conversation_id = ? AND id != ? ORDER BY id LIMIT ?',
|
|
201
|
+
[conversationId, systemId, deleteCount]);
|
|
202
|
+
if (toDeleteResult.length) {
|
|
203
|
+
toDeleteResult[0].values.forEach(row => {
|
|
204
|
+
db.run('DELETE FROM messages WHERE id = ?', [row[0]]);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
144
207
|
} else {
|
|
145
|
-
db.
|
|
146
|
-
|
|
208
|
+
const toDeleteResult = db.exec('SELECT id FROM messages WHERE conversation_id = ? ORDER BY id LIMIT ?',
|
|
209
|
+
[conversationId, deleteCount]);
|
|
210
|
+
if (toDeleteResult.length) {
|
|
211
|
+
toDeleteResult[0].values.forEach(row => {
|
|
212
|
+
db.run('DELETE FROM messages WHERE id = ?', [row[0]]);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
147
215
|
}
|
|
148
216
|
}
|
|
149
217
|
|
|
150
|
-
db.
|
|
151
|
-
|
|
218
|
+
db.run('INSERT INTO messages (conversation_id, role, content, timestamp) VALUES (?, ?, ?, ?)',
|
|
219
|
+
[conversationId, role, content, now]);
|
|
152
220
|
|
|
153
|
-
db.
|
|
221
|
+
db.run('UPDATE conversations SET updated_at = ? WHERE id = ?', [now, conversationId]);
|
|
222
|
+
scheduleSave();
|
|
154
223
|
|
|
155
224
|
return getConversation(conversationId);
|
|
156
225
|
}
|
|
157
226
|
|
|
227
|
+
|
|
158
228
|
export function listAllConversations() {
|
|
159
229
|
if (STORE_TYPE === 'memory' || !db) {
|
|
160
230
|
const conversations = [];
|
|
@@ -171,17 +241,21 @@ export function listAllConversations() {
|
|
|
171
241
|
return conversations.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at));
|
|
172
242
|
}
|
|
173
243
|
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
244
|
+
const convsResult = db.exec('SELECT * FROM conversations ORDER BY updated_at DESC');
|
|
245
|
+
if (!convsResult.length) return [];
|
|
246
|
+
|
|
247
|
+
return convsResult[0].values.map(conv => {
|
|
248
|
+
const [id, metadata, created_at, updated_at] = conv;
|
|
249
|
+
const lastMsgResult = db.exec('SELECT content FROM messages WHERE conversation_id = ? ORDER BY id DESC LIMIT 1', [id]);
|
|
250
|
+
const msgCountResult = db.exec('SELECT COUNT(*) as count FROM messages WHERE conversation_id = ?', [id]);
|
|
251
|
+
|
|
178
252
|
return {
|
|
179
|
-
id
|
|
180
|
-
message_count:
|
|
181
|
-
created_at: new Date(
|
|
182
|
-
updated_at: new Date(
|
|
183
|
-
metadata: JSON.parse(
|
|
184
|
-
preview:
|
|
253
|
+
id,
|
|
254
|
+
message_count: msgCountResult.length ? msgCountResult[0].values[0][0] : 0,
|
|
255
|
+
created_at: new Date(created_at).toISOString(),
|
|
256
|
+
updated_at: new Date(updated_at).toISOString(),
|
|
257
|
+
metadata: JSON.parse(metadata || '{}'),
|
|
258
|
+
preview: lastMsgResult.length && lastMsgResult[0].values.length ? lastMsgResult[0].values[0][0]?.substring(0, 100) || '' : ''
|
|
185
259
|
};
|
|
186
260
|
});
|
|
187
261
|
}
|
|
@@ -191,9 +265,10 @@ export function deleteConversation(conversationId) {
|
|
|
191
265
|
return memoryStore.delete(conversationId);
|
|
192
266
|
}
|
|
193
267
|
|
|
194
|
-
db.
|
|
195
|
-
|
|
196
|
-
|
|
268
|
+
db.run('DELETE FROM messages WHERE conversation_id = ?', [conversationId]);
|
|
269
|
+
db.run('DELETE FROM conversations WHERE id = ?', [conversationId]);
|
|
270
|
+
scheduleSave();
|
|
271
|
+
return true;
|
|
197
272
|
}
|
|
198
273
|
|
|
199
274
|
export function clearAllConversations() {
|
|
@@ -203,9 +278,11 @@ export function clearAllConversations() {
|
|
|
203
278
|
return count;
|
|
204
279
|
}
|
|
205
280
|
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
db.
|
|
281
|
+
const countResult = db.exec('SELECT COUNT(*) as count FROM conversations');
|
|
282
|
+
const count = countResult.length ? countResult[0].values[0][0] : 0;
|
|
283
|
+
db.run('DELETE FROM messages');
|
|
284
|
+
db.run('DELETE FROM conversations');
|
|
285
|
+
scheduleSave();
|
|
209
286
|
return count;
|
|
210
287
|
}
|
|
211
288
|
|
|
@@ -213,7 +290,8 @@ export function getConversationCount() {
|
|
|
213
290
|
if (STORE_TYPE === 'memory' || !db) {
|
|
214
291
|
return memoryStore.size;
|
|
215
292
|
}
|
|
216
|
-
|
|
293
|
+
const result = db.exec('SELECT COUNT(*) as count FROM conversations');
|
|
294
|
+
return result.length ? result[0].values[0][0] : 0;
|
|
217
295
|
}
|
|
218
296
|
|
|
219
297
|
export function cleanupConversations() {
|
|
@@ -238,23 +316,32 @@ export function cleanupConversations() {
|
|
|
238
316
|
return deleted;
|
|
239
317
|
}
|
|
240
318
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
319
|
+
// Delete expired conversations
|
|
320
|
+
const expiredResult = db.exec('SELECT id FROM conversations WHERE updated_at < ?', [expireTime]);
|
|
321
|
+
let deleted = 0;
|
|
322
|
+
if (expiredResult.length) {
|
|
323
|
+
expiredResult[0].values.forEach(row => {
|
|
324
|
+
db.run('DELETE FROM messages WHERE conversation_id = ?', [row[0]]);
|
|
325
|
+
db.run('DELETE FROM conversations WHERE id = ?', [row[0]]);
|
|
326
|
+
deleted++;
|
|
327
|
+
});
|
|
244
328
|
}
|
|
245
|
-
let result = db.prepare('DELETE FROM conversations WHERE updated_at < ?').run(expireTime);
|
|
246
|
-
let deleted = result.changes;
|
|
247
329
|
|
|
248
|
-
|
|
330
|
+
// Enforce max conversations limit
|
|
331
|
+
const countResult = db.exec('SELECT COUNT(*) as count FROM conversations');
|
|
332
|
+
const count = countResult.length ? countResult[0].values[0][0] : 0;
|
|
249
333
|
if (count > MAX_CONVERSATIONS) {
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
334
|
+
const toDeleteResult = db.exec('SELECT id FROM conversations ORDER BY updated_at ASC LIMIT ?', [count - MAX_CONVERSATIONS]);
|
|
335
|
+
if (toDeleteResult.length) {
|
|
336
|
+
toDeleteResult[0].values.forEach(row => {
|
|
337
|
+
db.run('DELETE FROM messages WHERE conversation_id = ?', [row[0]]);
|
|
338
|
+
db.run('DELETE FROM conversations WHERE id = ?', [row[0]]);
|
|
339
|
+
deleted++;
|
|
340
|
+
});
|
|
255
341
|
}
|
|
256
342
|
}
|
|
257
343
|
|
|
344
|
+
if (deleted > 0) scheduleSave();
|
|
258
345
|
return deleted;
|
|
259
346
|
}
|
|
260
347
|
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic Messages Route
|
|
3
|
+
* Handles /v1/messages endpoint (Anthropic/Claude API compatible)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Router } from 'express';
|
|
7
|
+
import { callYouApi, extractText } from '../api-client.js';
|
|
8
|
+
import {
|
|
9
|
+
mapAnthropicToYouParams,
|
|
10
|
+
convertToAnthropicResponse,
|
|
11
|
+
createMessageStartEvent,
|
|
12
|
+
createContentBlockStartEvent,
|
|
13
|
+
createContentBlockDeltaEvent,
|
|
14
|
+
createContentBlockStopEvent,
|
|
15
|
+
createMessageDeltaEvent,
|
|
16
|
+
createMessageStopEvent
|
|
17
|
+
} from '../anthropic-mapper.js';
|
|
18
|
+
import {
|
|
19
|
+
getConversation,
|
|
20
|
+
createConversation,
|
|
21
|
+
addMessageToConversation,
|
|
22
|
+
generateConversationId
|
|
23
|
+
} from '../conversation-store.js';
|
|
24
|
+
|
|
25
|
+
const router = Router();
|
|
26
|
+
|
|
27
|
+
// Get API key with rotation support
|
|
28
|
+
function getApiKey() {
|
|
29
|
+
const keys = (process.env.YDC_API_KEYS || process.env.YDC_API_KEY || '').split(',').filter(k => k.trim());
|
|
30
|
+
if (keys.length === 0) throw new Error('No API key configured');
|
|
31
|
+
return keys[Math.floor(Math.random() * keys.length)].trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Anthropic Messages endpoint
|
|
35
|
+
router.post('/v1/messages', async (req, res) => {
|
|
36
|
+
try {
|
|
37
|
+
const {
|
|
38
|
+
model = 'claude-3-5-sonnet-20241022',
|
|
39
|
+
messages,
|
|
40
|
+
system,
|
|
41
|
+
max_tokens = 1024,
|
|
42
|
+
temperature = 0.7,
|
|
43
|
+
stream = false,
|
|
44
|
+
metadata
|
|
45
|
+
} = req.body;
|
|
46
|
+
|
|
47
|
+
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
|
48
|
+
return res.status(400).json({
|
|
49
|
+
type: 'error',
|
|
50
|
+
error: {
|
|
51
|
+
type: 'invalid_request_error',
|
|
52
|
+
message: 'messages is required and must be a non-empty array'
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const apiKey = getApiKey();
|
|
58
|
+
|
|
59
|
+
// Handle conversation persistence via metadata
|
|
60
|
+
let conversationId = metadata?.conversation_id;
|
|
61
|
+
if (conversationId) {
|
|
62
|
+
const existingConv = getConversation(conversationId);
|
|
63
|
+
if (!existingConv) {
|
|
64
|
+
createConversation(conversationId);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Map to You.com parameters
|
|
69
|
+
const youParams = mapAnthropicToYouParams({
|
|
70
|
+
model,
|
|
71
|
+
messages,
|
|
72
|
+
system,
|
|
73
|
+
temperature,
|
|
74
|
+
max_tokens,
|
|
75
|
+
stream
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Store user message
|
|
79
|
+
if (conversationId) {
|
|
80
|
+
const lastUserMsg = messages.filter(m => m.role === 'user').pop();
|
|
81
|
+
if (lastUserMsg) {
|
|
82
|
+
const content = typeof lastUserMsg.content === 'string'
|
|
83
|
+
? lastUserMsg.content
|
|
84
|
+
: lastUserMsg.content.filter(c => c.type === 'text').map(c => c.text).join('\n');
|
|
85
|
+
addMessageToConversation(conversationId, 'user', content);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (stream) {
|
|
90
|
+
// Streaming response
|
|
91
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
92
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
93
|
+
res.setHeader('Connection', 'keep-alive');
|
|
94
|
+
|
|
95
|
+
// Send message_start
|
|
96
|
+
res.write(createMessageStartEvent(model));
|
|
97
|
+
|
|
98
|
+
// Send content_block_start
|
|
99
|
+
res.write(createContentBlockStartEvent(0));
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const response = await callYouApi(apiKey, { ...youParams, stream: true });
|
|
103
|
+
|
|
104
|
+
let fullContent = '';
|
|
105
|
+
let buffer = '';
|
|
106
|
+
|
|
107
|
+
response.body.on('data', (chunk) => {
|
|
108
|
+
buffer += chunk.toString();
|
|
109
|
+
const lines = buffer.split('\n');
|
|
110
|
+
buffer = lines.pop() || '';
|
|
111
|
+
|
|
112
|
+
for (const line of lines) {
|
|
113
|
+
if (line.startsWith('data: ')) {
|
|
114
|
+
const data = line.slice(6);
|
|
115
|
+
if (data === '[DONE]') continue;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const parsed = JSON.parse(data);
|
|
119
|
+
if (parsed.output) {
|
|
120
|
+
for (const item of parsed.output) {
|
|
121
|
+
if (item.type === 'message.answer' && item.text) {
|
|
122
|
+
const newText = item.text.slice(fullContent.length);
|
|
123
|
+
if (newText) {
|
|
124
|
+
fullContent = item.text;
|
|
125
|
+
res.write(createContentBlockDeltaEvent(newText, 0));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch (e) {
|
|
131
|
+
// Skip invalid JSON
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
response.body.on('end', () => {
|
|
138
|
+
// Store assistant response
|
|
139
|
+
if (conversationId && fullContent) {
|
|
140
|
+
addMessageToConversation(conversationId, 'assistant', fullContent);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Send closing events
|
|
144
|
+
res.write(createContentBlockStopEvent(0));
|
|
145
|
+
res.write(createMessageDeltaEvent(Math.floor(fullContent.length / 4)));
|
|
146
|
+
res.write(createMessageStopEvent());
|
|
147
|
+
res.end();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
response.body.on('error', (error) => {
|
|
151
|
+
console.error('Stream error:', error);
|
|
152
|
+
res.end();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error('Streaming error:', error);
|
|
157
|
+
res.write(createContentBlockDeltaEvent(`Error: ${error.message}`, 0));
|
|
158
|
+
res.write(createContentBlockStopEvent(0));
|
|
159
|
+
res.write(createMessageDeltaEvent(0));
|
|
160
|
+
res.write(createMessageStopEvent());
|
|
161
|
+
res.end();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
} else {
|
|
165
|
+
// Non-streaming response
|
|
166
|
+
const response = await callYouApi(apiKey, youParams);
|
|
167
|
+
const data = await response.json();
|
|
168
|
+
|
|
169
|
+
const anthropicResponse = convertToAnthropicResponse(data, model);
|
|
170
|
+
|
|
171
|
+
// Store assistant response
|
|
172
|
+
if (conversationId) {
|
|
173
|
+
const content = anthropicResponse.content[0]?.text || '';
|
|
174
|
+
if (content) {
|
|
175
|
+
addMessageToConversation(conversationId, 'assistant', content);
|
|
176
|
+
}
|
|
177
|
+
// Add conversation_id to response
|
|
178
|
+
anthropicResponse.metadata = { conversation_id: conversationId };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
res.json(anthropicResponse);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error('Anthropic messages error:', error);
|
|
186
|
+
res.status(500).json({
|
|
187
|
+
type: 'error',
|
|
188
|
+
error: {
|
|
189
|
+
type: 'api_error',
|
|
190
|
+
message: error.message
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
export default router;
|
package/openai-server.js
CHANGED
|
@@ -15,9 +15,10 @@ import chatRoutes from './lib/routes/chat.js';
|
|
|
15
15
|
import modelsRoutes from './lib/routes/models.js';
|
|
16
16
|
import conversationsRoutes from './lib/routes/conversations.js';
|
|
17
17
|
import healthRoutes from './lib/routes/health.js';
|
|
18
|
+
import anthropicRoutes from './lib/routes/anthropic-messages.js';
|
|
18
19
|
|
|
19
20
|
// Import config
|
|
20
|
-
import { storeConfig } from './lib/conversation-store.js';
|
|
21
|
+
import { storeConfig, initDatabase } from './lib/conversation-store.js';
|
|
21
22
|
import { authConfig } from './lib/auth-middleware.js';
|
|
22
23
|
import { listAdvancedVersions, getDefaultAdvancedVersion } from './lib/advanced-versions.js';
|
|
23
24
|
|
|
@@ -56,10 +57,14 @@ app.use(chatRoutes);
|
|
|
56
57
|
app.use(modelsRoutes);
|
|
57
58
|
app.use(conversationsRoutes);
|
|
58
59
|
app.use(healthRoutes);
|
|
60
|
+
app.use(anthropicRoutes);
|
|
59
61
|
|
|
60
62
|
// Start server with auto port detection
|
|
61
63
|
async function startServer() {
|
|
62
64
|
try {
|
|
65
|
+
// Initialize database (handles missing better-sqlite3 gracefully)
|
|
66
|
+
await initDatabase();
|
|
67
|
+
|
|
63
68
|
const port = await findAvailablePort(startPort);
|
|
64
69
|
app.set('port', port);
|
|
65
70
|
|
|
@@ -70,7 +75,8 @@ async function startServer() {
|
|
|
70
75
|
console.log(`š¦ Conversation Store: ${storeConfig.STORE_TYPE}${storeConfig.STORE_TYPE === 'sqlite' && storeConfig.isDbConnected() ? ` (${storeConfig.DB_PATH})` : ''}`);
|
|
71
76
|
console.log(`š Token Auth: ${authConfig.REQUIRE_TOKEN_AUTH ? `enabled (${authConfig.ACCESS_TOKENS_COUNT} tokens)` : 'disabled (accept all)'}`);
|
|
72
77
|
console.log(`\nš Endpoints:`);
|
|
73
|
-
console.log(` POST http://localhost:${port}/v1/chat/completions`);
|
|
78
|
+
console.log(` POST http://localhost:${port}/v1/chat/completions (OpenAI)`);
|
|
79
|
+
console.log(` POST http://localhost:${port}/v1/messages (Anthropic/Claude)`);
|
|
74
80
|
console.log(` GET http://localhost:${port}/v1/models`);
|
|
75
81
|
console.log(` GET http://localhost:${port}/v1/versions`);
|
|
76
82
|
console.log(` GET http://localhost:${port}/health`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ydc-mcp-server",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "MCP server for You.com Agents API - Express, Research, Advanced agents with multi-turn conversations, streaming, and
|
|
3
|
+
"version": "1.7.0",
|
|
4
|
+
"description": "MCP server for You.com Agents API - Express, Research, Advanced agents with multi-turn conversations, streaming, OpenAI and Anthropic/Claude API compatibility",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"bin": {
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"prepublishOnly": "echo 'Ready to publish'"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@modelcontextprotocol/sdk": "^1.25.1"
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
31
|
+
"sql.js": "^1.11.0"
|
|
31
32
|
},
|
|
32
33
|
"optionalDependencies": {
|
|
33
|
-
"better-sqlite3": "^11.7.0",
|
|
34
34
|
"chalk": "^5.6.2",
|
|
35
35
|
"cli-progress": "^3.12.0",
|
|
36
36
|
"cli-table3": "^0.6.5",
|