|
import { serve } from "https://deno.land/[email protected]/http/server.ts"; |
|
|
|
|
|
const API_URL = "https://mcp.scira.ai/api/chat"; |
|
const FIXED_USER_ID = "2jFMDM1A1R_XxOTxPjhwe"; |
|
const FIXED_CHAT_ID = "ZIWa36kd6MSqzw-ifXGzE"; |
|
const DEFAULT_MODEL = "qwen-qwq"; |
|
const PORT = 7860; |
|
|
|
|
|
interface Message { |
|
role: string; |
|
content: string; |
|
parts?: Array<{ |
|
type: string; |
|
text: string; |
|
}>; |
|
} |
|
|
|
interface SciraPayload { |
|
id: string; |
|
messages: Message[]; |
|
selectedModel: string; |
|
mcpServers: any[]; |
|
chatId: string; |
|
userId: string; |
|
} |
|
|
|
interface OpenAIModel { |
|
id: string; |
|
created: number; |
|
object: string; |
|
} |
|
|
|
|
|
const AVAILABLE_MODELS: OpenAIModel[] = [ |
|
{ |
|
id: "qwen-qwq", |
|
created: Date.now(), |
|
object: "model", |
|
}, |
|
{ |
|
id: "gemini-2.5-flash", |
|
created: Date.now(), |
|
object: "model", |
|
}, |
|
{ |
|
id: "gpt-4.1-mini", |
|
created: Date.now(), |
|
object: "model", |
|
}, |
|
{ |
|
id: "claude-3-7-sonnet", |
|
created: Date.now(), |
|
object: "model", |
|
}, |
|
]; |
|
|
|
|
|
function formatMessagesForScira(messages: Message[]): Message[] { |
|
return messages.map(msg => ({ |
|
role: msg.role, |
|
content: msg.content, |
|
parts: [{ |
|
type: "text", |
|
text: msg.content |
|
}] |
|
})); |
|
} |
|
|
|
|
|
function buildSciraPayload(messages: Message[], model = DEFAULT_MODEL): SciraPayload { |
|
const formattedMessages = formatMessagesForScira(messages); |
|
return { |
|
id: FIXED_CHAT_ID, |
|
messages: formattedMessages, |
|
selectedModel: model, |
|
mcpServers: [], |
|
chatId: FIXED_CHAT_ID, |
|
userId: FIXED_USER_ID |
|
}; |
|
} |
|
|
|
|
|
async function handleModelsRequest(): Promise<Response> { |
|
const response = { |
|
object: "list", |
|
data: AVAILABLE_MODELS, |
|
}; |
|
return new Response(JSON.stringify(response), { |
|
headers: { |
|
"Content-Type": "application/json", |
|
"Access-Control-Allow-Origin": "*" |
|
}, |
|
}); |
|
} |
|
|
|
|
|
async function handleChatCompletionsRequest(req: Request): Promise<Response> { |
|
const requestData = await req.json(); |
|
const { messages, model = DEFAULT_MODEL, stream = false } = requestData; |
|
|
|
const sciraPayload = buildSciraPayload(messages, model); |
|
const response = await fetch(API_URL, { |
|
method: "POST", |
|
headers: { |
|
"Content-Type": "application/json", |
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:137.0) Gecko/20100101 Firefox/137.0", |
|
"Accept": "*/*", |
|
"Referer": `https://mcp.scira.ai/chat/${FIXED_CHAT_ID}`, |
|
"Origin": "https://mcp.scira.ai", |
|
}, |
|
body: JSON.stringify(sciraPayload), |
|
}); |
|
|
|
if (stream) { |
|
return handleStreamResponse(response, model); |
|
} else { |
|
return handleRegularResponse(response, model); |
|
} |
|
} |
|
|
|
|
|
async function handleStreamResponse(response: Response, model: string): Promise<Response> { |
|
const reader = response.body!.getReader(); |
|
const encoder = new TextEncoder(); |
|
const decoder = new TextDecoder(); |
|
|
|
const id = `chatcmpl-${Date.now().toString(36)}${Math.random().toString(36).substring(2, 10)}`; |
|
const createdTime = Math.floor(Date.now() / 1000); |
|
const systemFingerprint = `fp_${Math.random().toString(36).substring(2, 12)}`; |
|
|
|
const stream = new ReadableStream({ |
|
async start(controller) { |
|
|
|
const headerEvent = { |
|
id: id, |
|
object: "chat.completion.chunk", |
|
created: createdTime, |
|
model: model, |
|
system_fingerprint: systemFingerprint, |
|
choices: [{ |
|
index: 0, |
|
delta: { role: "assistant" }, |
|
logprobs: null, |
|
finish_reason: null |
|
}] |
|
}; |
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(headerEvent)}\n\n`)); |
|
|
|
try { |
|
let buffer = ""; |
|
|
|
while (true) { |
|
const { done, value } = await reader.read(); |
|
if (done) break; |
|
|
|
|
|
buffer += decoder.decode(value, { stream: true }); |
|
|
|
|
|
const lines = buffer.split('\n'); |
|
|
|
buffer = lines.pop() || ""; |
|
|
|
|
|
for (const line of lines) { |
|
if (!line.trim()) continue; |
|
|
|
if (line.startsWith('g:')) { |
|
|
|
let content = line.slice(2).replace(/^"/, "").replace(/"$/, ""); |
|
content = content.replace(/\\n/g, "\n"); |
|
|
|
const event = { |
|
id: id, |
|
object: "chat.completion.chunk", |
|
created: createdTime, |
|
model: model, |
|
system_fingerprint: systemFingerprint, |
|
choices: [{ |
|
index: 0, |
|
delta: { reasoning_content: content }, |
|
logprobs: null, |
|
finish_reason: null |
|
}] |
|
}; |
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`)); |
|
} else if (line.startsWith('0:')) { |
|
|
|
let content = line.slice(2).replace(/^"/, "").replace(/"$/, ""); |
|
content = content.replace(/\\n/g, "\n"); |
|
|
|
const event = { |
|
id: id, |
|
object: "chat.completion.chunk", |
|
created: createdTime, |
|
model: model, |
|
system_fingerprint: systemFingerprint, |
|
choices: [{ |
|
index: 0, |
|
delta: { content: content }, |
|
logprobs: null, |
|
finish_reason: null |
|
}] |
|
}; |
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`)); |
|
} else if (line.startsWith('e:')) { |
|
|
|
try { |
|
const finishData = JSON.parse(line.slice(2)); |
|
const event = { |
|
id: id, |
|
object: "chat.completion.chunk", |
|
created: createdTime, |
|
model: model, |
|
system_fingerprint: systemFingerprint, |
|
choices: [{ |
|
index: 0, |
|
delta: {}, |
|
logprobs: null, |
|
finish_reason: finishData.finishReason || "stop" |
|
}] |
|
}; |
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`)); |
|
} catch (error) { |
|
console.error("Error parsing finish data:", error); |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
if (buffer.trim()) { |
|
const line = buffer.trim(); |
|
if (line.startsWith('g:')) { |
|
let content = line.slice(2).replace(/^"/, "").replace(/"$/, ""); |
|
content = content.replace(/\\n/g, "\n"); |
|
|
|
const event = { |
|
id: id, |
|
object: "chat.completion.chunk", |
|
created: createdTime, |
|
model: model, |
|
system_fingerprint: systemFingerprint, |
|
choices: [{ |
|
index: 0, |
|
delta: { reasoning_content: content }, |
|
logprobs: null, |
|
finish_reason: null |
|
}] |
|
}; |
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`)); |
|
} else if (line.startsWith('0:')) { |
|
let content = line.slice(2).replace(/^"/, "").replace(/"$/, ""); |
|
content = content.replace(/\\n/g, "\n"); |
|
|
|
const event = { |
|
id: id, |
|
object: "chat.completion.chunk", |
|
created: createdTime, |
|
model: model, |
|
system_fingerprint: systemFingerprint, |
|
choices: [{ |
|
index: 0, |
|
delta: { content: content }, |
|
logprobs: null, |
|
finish_reason: null |
|
}] |
|
}; |
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`)); |
|
} |
|
} |
|
} catch (error) { |
|
console.error("Stream error:", error); |
|
} finally { |
|
|
|
controller.enqueue(encoder.encode("data: [DONE]\n\n")); |
|
controller.close(); |
|
} |
|
} |
|
}); |
|
|
|
return new Response(stream, { |
|
headers: { |
|
"Content-Type": "text/event-stream", |
|
"Cache-Control": "no-cache", |
|
"Connection": "keep-alive", |
|
"Access-Control-Allow-Origin": "*", |
|
}, |
|
}); |
|
} |
|
|
|
|
|
async function handleRegularResponse(response: Response, model: string): Promise<Response> { |
|
const text = await response.text(); |
|
const lines = text.split('\n'); |
|
|
|
let content = ""; |
|
let reasoning_content = ""; |
|
let usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }; |
|
let finish_reason = "stop"; |
|
|
|
for (const line of lines) { |
|
if (!line.trim()) continue; |
|
|
|
if (line.startsWith('0:')) { |
|
|
|
let lineContent = line.slice(2).replace(/^"/, "").replace(/"$/, ""); |
|
lineContent = lineContent.replace(/\\n/g, "\n"); |
|
content += lineContent; |
|
} else if (line.startsWith('g:')) { |
|
|
|
let lineContent = line.slice(2).replace(/^"/, "").replace(/"$/, ""); |
|
lineContent = lineContent.replace(/\\n/g, "\n"); |
|
reasoning_content += lineContent; |
|
} else if (line.startsWith('e:')) { |
|
try { |
|
const finishData = JSON.parse(line.slice(2)); |
|
if (finishData.finishReason) { |
|
finish_reason = finishData.finishReason; |
|
} |
|
} catch (error) { |
|
console.error("Error parsing finish data:", error); |
|
} |
|
} else if (line.startsWith('d:')) { |
|
try { |
|
const finishData = JSON.parse(line.slice(2)); |
|
if (finishData.usage) { |
|
usage.prompt_tokens = finishData.usage.promptTokens || 0; |
|
usage.completion_tokens = finishData.usage.completionTokens || 0; |
|
usage.total_tokens = usage.prompt_tokens + usage.completion_tokens; |
|
} |
|
} catch (error) { |
|
console.error("Error parsing usage data:", error); |
|
} |
|
} |
|
} |
|
|
|
const systemFingerprint = `fp_${Math.random().toString(36).substring(2, 12)}`; |
|
const id = `chatcmpl-${Date.now().toString(36)}${Math.random().toString(36).substring(2, 10)}`; |
|
|
|
const openAIResponse = { |
|
id: id, |
|
object: "chat.completion", |
|
created: Math.floor(Date.now() / 1000), |
|
model: model, |
|
system_fingerprint: systemFingerprint, |
|
choices: [{ |
|
index: 0, |
|
message: { |
|
role: "assistant", |
|
content: content |
|
}, |
|
logprobs: null, |
|
finish_reason: finish_reason |
|
}], |
|
usage: usage |
|
}; |
|
|
|
|
|
if (reasoning_content.trim()) { |
|
openAIResponse.choices[0].message.reasoning_content = reasoning_content; |
|
} |
|
|
|
return new Response(JSON.stringify(openAIResponse), { |
|
headers: { |
|
"Content-Type": "application/json", |
|
"Access-Control-Allow-Origin": "*" |
|
}, |
|
}); |
|
} |
|
|
|
|
|
async function handler(req: Request): Promise<Response> { |
|
const url = new URL(req.url); |
|
|
|
|
|
const headers = { |
|
"Access-Control-Allow-Origin": "*", |
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS", |
|
"Access-Control-Allow-Headers": "Content-Type, Authorization", |
|
}; |
|
|
|
|
|
if (req.method === "OPTIONS") { |
|
return new Response(null, { |
|
headers, |
|
status: 204 |
|
}); |
|
} |
|
|
|
try { |
|
|
|
if (url.pathname === "/v1/models") { |
|
return handleModelsRequest(); |
|
} |
|
|
|
|
|
if (url.pathname === "/v1/chat/completions") { |
|
return handleChatCompletionsRequest(req); |
|
} |
|
|
|
|
|
return new Response( |
|
JSON.stringify({ error: "Not found" }), { |
|
status: 404, |
|
headers: { |
|
"Content-Type": "application/json", |
|
...headers |
|
}, |
|
} |
|
); |
|
} catch (error) { |
|
console.error("Error processing request:", error); |
|
return new Response( |
|
JSON.stringify({ error: error.message || "Internal server error" }), |
|
{ |
|
status: 500, |
|
headers: { |
|
"Content-Type": "application/json", |
|
...headers |
|
}, |
|
} |
|
); |
|
} |
|
} |
|
|
|
|
|
console.log(`Starting server on port ${PORT}...`); |
|
serve(handler, { port: PORT }); |
|
|