从[维护] 一个免费 api继续讨论:
既然上游都挂了, 那就发出来吧
纯ai
// openai_proxy.ts
import { serve, Server } from "https://deno.land/std/http/server.ts";
import { ConnInfo } from "https://deno.land/std/http/server.ts"; // 使用兼容的 ConnInfo 类型
// 缓存设置
const CACHE_TTL = 3600; // 缓存有效期(秒)
const modelsCache = {
data: null as any,
timestamp: 0,
};
// 配置
const CHAT_COMPLETION_PROXY_URL = "https://us.helicone.ai/api/llm";
const MODELS_PROXY_URL = "https://openrouter.ai/api/v1/models";
const PORT = 3000;
const SERVICE_NAME = "Shinplex Api Service";
// Deno KV 实例
let kv: Deno.Kv | null = null;
// KV 键前缀
const KV_PREFIX = "shinplex_proxy:";
const STATS_KEY = [KV_PREFIX, "statistics"];
const IP_STATS_PREFIX = [KV_PREFIX, "ip_stats"];
// 初始化 KV
async function initKv() {
if (!kv) {
try {
kv = await Deno.openKv();
console.log("Deno KV initialized.");
} catch (e) {
console.error("Failed to initialize Deno KV:", e);
// 如果 KV 初始化失败,我们将退回到内存统计(非持久化)
kv = null;
console.warn("Using in-memory statistics due to KV failure.");
}
}
}
// 获取统计数据
async function getStatistics(): Promise<{ chatCompletionRequests: number; ipRequests: Record<string, number> }> {
if (kv) {
try {
const statsEntry = await kv.get(STATS_KEY);
const ipStatsEntries = await kv.list({ prefix: IP_STATS_PREFIX });
const chatCompletionRequests = statsEntry.value as number || 0;
const ipRequests: Record<string, number> = {};
for await (const entry of ipStatsEntries) {
const ip = entry.key[entry.key.length - 1] as string;
ipRequests[ip] = entry.value as number;
}
return { chatCompletionRequests, ipRequests };
} catch (e) {
console.error("Error fetching statistics from KV:", e);
// 降级到内存统计
return { chatCompletionRequests: 0, ipRequests: {} };
}
} else {
// KV 未初始化,使用内存统计(如果需要,可以在这里定义一个内存对象)
// 为了简化,这里返回空对象,表示无法获取统计
return { chatCompletionRequests: 0, ipRequests: {} };
}
}
// 更新统计数据
async function updateStatistics(ipAddress: string) {
if (kv) {
try {
// 更新总请求数
await kv.atomic()
.mutate({
type: "sum",
key: STATS_KEY,
value: new Deno.KvU64(1n),
})
.commit();
// 更新 IP 请求数
await kv.atomic()
.mutate({
type: "sum",
key: [...IP_STATS_PREFIX, ipAddress],
value: new Deno.KvU64(1n),
})
.commit();
} catch (e) {
console.error("Error updating statistics in KV:", e);
// 忽略更新失败
}
} else {
// KV 未初始化,不执行更新
}
}
// 处理模型列表请求
async function handleModelsRequest(req: Request): Promise<Response> {
const now = Date.now();
// 检查缓存是否有效
if (modelsCache.data && now - modelsCache.timestamp < CACHE_TTL * 1000) {
return new Response(JSON.stringify(modelsCache.data), {
headers: { "Content-Type": "application/json" },
});
}
try {
// 获取新数据
const response = await fetch(MODELS_PROXY_URL, {
method: "GET",
headers: {
"Content-Type": "application/json",
// 不再透传 Auth 头
},
});
if (!response.ok) {
// 尝试读取错误信息,但不要等待太久或失败
let errorText = `Error fetching models: ${response.status}`;
try {
const errorBody = await response.text();
if (errorBody) {
errorText += ` - ${errorBody}`;
}
} catch (e) {
console.warn("Could not read error body from models request:", e);
}
throw new Error(errorText);
}
const data = await response.json();
// 更新缓存
modelsCache.data = data;
modelsCache.timestamp = now;
return new Response(JSON.stringify(modelsCache.data), {
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error handling models request:", error);
return new Response(JSON.stringify({ error: "Failed to fetch models" }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
}
// 处理聊天完成请求 - 总是向上游发送流式请求, 根据客户端需求决定返回流或非流
async function handleChatCompletionRequest(req: Request, ipAddress: string): Promise<Response> {
// 统计聊天完成请求数和 IP 请求数
updateStatistics(ipAddress);
try {
const clientBody = await req.json();
const clientExpectsStream = clientBody.stream === true;
// 总是向上游发送 stream: true
const upstreamBody = { ...clientBody, stream: true };
// 准备转发请求
const proxyReq = new Request(CHAT_COMPLETION_PROXY_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
// 不再透传 Auth 头
},
body: JSON.stringify(upstreamBody),
});
const response = await fetch(proxyReq);
if (!response.ok) {
// 尝试读取错误信息,但不要等待太久或失败
let errorText = `Proxy error: ${response.status}`;
let errorJson = null;
try {
const errorBody = await response.text();
if (errorBody) {
errorText += ` - ${errorBody}`;
// 尝试解析为 JSON, 以便返回更结构化的错误
try {
errorJson = JSON.parse(errorBody);
} catch (e) {
// Ignore JSON parsing errors, use raw text
}
}
} catch (e) {
console.warn("Could not read error body from chat completion request:", e);
}
// 如果解析到 JSON 错误, 返回 JSON 错误体, 否则返回通用错误
if (errorJson) {
return new Response(JSON.stringify(errorJson), {
status: response.status,
headers: { "Content-Type": "application/json" },
});
} else {
return new Response(
JSON.stringify({
error: {
message: `Upstream error: ${errorText}`,
type: "upstream_error",
}
}),
{
status: response.status, // 返回上游的状态码
headers: { "Content-Type": "application/json" },
}
);
}
}
// 如果客户端期望流式响应, 则直接透传流
if (clientExpectsStream) {
// 直接返回上游的响应, 包括头部和体
return response;
} else {
// 如果客户端期望非流式响应, 则处理流并拼接结果
const reader = response.body?.getReader();
if (!reader) {
throw new Error("No response body received from upstream for non-stream request");
}
const decoder = new TextDecoder();
let buffer = "";
let fullContent = ""; // 拼接所有文本内容
let firstResponse: any = null; // 存储第一个完整的响应结构(用于 header/role 等)
let finishReason: string | null = null; // 存储结束原因
try {
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() || ""; // Keep the last potentially incomplete line
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.substring(6).trim();
if (data === "[DONE]") {
break; // End of stream
}
try {
const chunk = JSON.parse(data);
// 假设 chunk 是 OpenAI 兼容的 SSE 格式
// 查找 choices 数组
if (chunk.choices && chunk.choices.length > 0) {
const choice = chunk.choices[0];
// 拼接文本内容
if (choice.delta && choice.delta.content) {
fullContent += choice.delta.content;
} else if (choice.message && choice.message.content) {
// 兼容某些非 delta 格式(虽然 OpenAI 流通常是 delta)
fullContent += choice.message.content;
}
// 存储第一个 chunk 的信息(如 id, model, created, role)
if (!firstResponse) {
firstResponse = { ...chunk }; // Copy the structure
// Clear delta/message content as we are building fullContent
if (firstResponse.choices && firstResponse.choices.length > 0) {
if (firstResponse.choices[0].delta) delete firstResponse.choices[0].delta;
if (firstResponse.choices[0].message) delete firstResponse.choices[0].message;
}
}
// 存储结束原因
if (choice.finish_reason) {
finishReason = choice.finish_reason;
}
}
} catch (e) {
console.error("Failed to parse SSE data chunk:", e, "Data:", data);
// Continue processing even if one chunk fails
}
} else if (line.startsWith(": ")) {
// Ignore comments
} else if (line.trim() !== "") {
// Handle other non-data lines if necessary, or just ignore
console.warn("Received unexpected SSE line:", line);
}
}
}
// Process any remaining data in the buffer
if (buffer.startsWith("data: ")) {
const data = buffer.substring(6).trim();
if (data !== "[DONE]") {
try {
const chunk = JSON.parse(data);
if (chunk.choices && chunk.choices.length > 0) {
const choice = chunk.choices[0];
if (choice.delta && choice.delta.content) {
fullContent += choice.delta.content;
} else if (choice.message && choice.message.content) {
fullContent += choice.message.content;
}
if (!firstResponse) {
firstResponse = { ...chunk };
if (firstResponse.choices && firstResponse.choices.length > 0) {
if (firstResponse.choices[0].delta) delete firstResponse.choices[0].delta;
if (firstResponse.choices[0].message) delete firstResponse.choices[0].message;
}
}
if (choice.finish_reason) {
finishReason = choice.finish_reason;
}
}
} catch (e) {
console.error("Failed to parse remaining SSE data chunk:", e, "Data:", data);
}
}
}
} finally {
// Ensure the reader is cancelled if not done
reader.cancel().catch(e => console.error("Failed to cancel reader:", e));
}
// 构建最终的非流式响应结构
// 模拟 OpenAI 非流式响应格式
const finalResponse: any = firstResponse || {}; // Use first chunk structure if available
// Ensure choices array exists and has at least one element
if (!finalResponse.choices || finalResponse.choices.length === 0) {
finalResponse.choices = [{}];
}
// Add the accumulated content and finish reason
finalResponse.choices[0].message = {
role: firstResponse?.choices?.[0]?.message?.role || "assistant", // Default role if not in first chunk
content: fullContent,
};
finalResponse.choices[0].finish_reason = finishReason || "stop"; // Default finish reason
// Add dummy usage if not present (OpenAI non-stream usually has it)
if (!finalResponse.usage) {
finalResponse.usage = {
prompt_tokens: 0, // Cannot easily calculate from stream
completion_tokens: 0, // Cannot easily calculate from stream
total_tokens: 0, // Cannot easily calculate from stream
};
// Optionally, add a warning that usage is not accurate for proxied stream
// finalResponse.usage.warning = "Usage tokens are estimates or not available from stream.";
}
// Add other top-level fields if missing
if (!finalResponse.id) finalResponse.id = `proxy-${Date.now()}`;
if (!finalResponse.object) finalResponse.object = "chat.completion";
if (!finalResponse.created) finalResponse.created = Math.floor(Date.now() / 1000);
// Model might be available in first chunk, otherwise leave undefined or add a placeholder
// if (!finalResponse.model) finalResponse.model = "unknown-model";
return new Response(JSON.stringify(finalResponse), {
headers: { "Content-Type": "application/json" },
});
}
} catch (error) {
console.error("Error handling chat completion request:", error);
return new Response(
JSON.stringify({
error: {
message: `Error processing request: ${error.message}`,
type: "proxy_error",
}
}),
{
status: 500,
headers: { "Content-Type": "application/json" },
}
);
}
}
// 规范化路径, 处理 /v1/v1 等重复路径问题
function normalizePath(path: string): string {
// 移除开头的斜杠
let normalized = path.startsWith("/") ? path.substring(1) : path;
// 分割路径
const segments = normalized.split("/");
// 移除重复的 v1 段
const uniqueSegments: string[] = [];
let v1Found = false;
for (const segment of segments) {
if (segment === "v1") {
if (!v1Found) {
uniqueSegments.push(segment);
v1Found = true;
}
// 跳过重复的 v1
} else {
uniqueSegments.push(segment);
}
}
// 重建路径
return "/" + uniqueSegments.join("/");
}
// HTML 模板 (与之前相同)
const HTML_TEMPLATE = (title: string, activeLink: string, content: string) => `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${title} - ${SERVICE_NAME}</title>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
<!-- Optional theme -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap-theme.min.css">
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<style>
body {
padding-top : 70px;
padding-bottom : 30px;
}
.theme-dropdown .dropdown-menu {
position : static;
display : block;
margin-bottom : 20px;
}
.theme-showcase > p > .btn {
margin : 5px 0;
}
.theme-showcase .navbar .container {
width : auto;
}
</style>
</head>
<body role="document">
<!-- Fixed navbar -->
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar"
aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">${SERVICE_NAME}</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li class="${activeLink === 'home' ? 'active' : ''}"><a href="/">Home</a></li>
<li class="${activeLink === 'view-models' ? 'active' : ''}"><a href="/view/models">View Models</a></li>
<li class="${activeLink === 'view-statistics' ? 'active' : ''}"><a href="/view/statistics">Statistics</a></li>
</ul>
</div>
<!--/.nav-collapse -->
</div>
</nav>
<div class="container theme-showcase" role="main">
${content}
</div>
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<!-- Latest compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
<script>
$(document).ready(function(){
$("#modelSearch").on("keyup", function() {
var value = $(this).val().toLowerCase();
$("#modelTableBody tr").filter(function() {
$(this).toggle($(this).text().toLowerCase().indexOf(value) > -1)
});
});
});
</script>
</body>
</html>
`;
// 处理主页请求 (与之前相同)
function handleHomeRequest(): Response {
const content = `
<div class="jumbotron">
<h1>Hi!</h1>
<p>${SERVICE_NAME} is running...</p>
</div>
<div class="jumbotron">
<h1>Api Endpoint</h1>
<p>Cherry Studio / New Api: <code>https://funny-ape-75.deno.dev</code></p>
<p>OpenAI Base Url: <code>https://funny-ape-75.deno.dev/v1</code></p>
</div>
<div class="jumbotron">
<h1>Warning!</h1>
<p>Abuse this service will let your IP address got banned FOREVER!</p>
<p>Your IP address will be recorded and will be shown public!<p>
<p>Copyright 2025 Shinplexus</p>
<p>THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>
</div>
`;
return new Response(HTML_TEMPLATE("Home", "home", content), {
headers: { "Content-Type": "text/html" },
});
}
// 处理 /view/models 请求 (与之前相同)
async function handleViewModelsRequest(): Promise<Response> {
try {
const modelsResponse = await handleModelsRequest(new Request("http://localhost/v1/models"));
if (!modelsResponse.ok) {
throw new Error("Failed to fetch models for view");
}
const modelsData = await modelsResponse.json();
const models = modelsData.data || [];
let tableHtml = `
<div class="page-header">
<h1>Available Models (${models.length})</h1>
</div>
<div class="form-group">
<input class="form-control" id="modelSearch" type="text" placeholder="Search models...">
</div>
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Context Length</th>
<th>Modalities</th>
<th>Pricing (Prompt/Completion)</th>
<th>Description</th>
</tr>
</thead>
<tbody id="modelTableBody">
`;
for (const model of models) {
const pricing = model.pricing || {};
const promptPrice = pricing.prompt === "0" ? "Free" : pricing.prompt || "N/A";
const completionPrice = pricing.completion === "0" ? "Free" : pricing.completion || "N/A";
const modalities = model.architecture?.input_modalities?.join(", ") || "N/A";
tableHtml += `
<tr>
<td>${model.id || 'N/A'}</td>
<td>${model.name || 'N/A'}</td>
<td>${model.context_length || 'N/A'}</td>
<td>${modalities}</td>
<td>${promptPrice} / ${completionPrice}</td>
<td>${model.description ? model.description.substring(0, 200) + (model.description.length > 200 ? '...' : '') : 'N/A'}</td>
</tr>
`;
}
tableHtml += `
</tbody>
</table>
`;
return new Response(HTML_TEMPLATE("View Models", "view-models", tableHtml), {
headers: { "Content-Type": "text/html" },
});
} catch (error) {
console.error("Error handling view models request:", error);
const errorContent = `
<div class="alert alert-danger" role="alert">
<h4>Error loading models</h4>
<p>${error.message}</p>
</div>
`;
return new Response(HTML_TEMPLATE("Error", "view-models", errorContent), {
status: 500,
headers: { "Content-Type": "text/html" },
});
}
}
// 处理统计页面请求
async function handleStatisticsRequest(): Promise<Response> {
const statistics = await getStatistics(); // 从 KV 获取统计数据
// 对 IP 请求数进行排序 - 注意:这里需要处理 BigInt 类型
const sortedIpRequests = Object.entries(statistics.ipRequests)
.sort(([, countA], [, countB]) => {
// 确保比较的是相同类型
const bigA = typeof countA === 'bigint' ? countA : BigInt(countA);
const bigB = typeof countB === 'bigint' ? countB : BigInt(countB);
return bigB > bigA ? 1 : bigB < bigA ? -1 : 0; // 降序排列
});
let ipTableHtml = `
<table class="table table-striped">
<thead>
<tr>
<th>Rank</th>
<th>IP Address</th>
<th>Request Count (Chat Completion)</th>
</tr>
</thead>
<tbody>
`;
if (sortedIpRequests.length === 0) {
ipTableHtml += `<tr><td colspan="3" class="text-center">No IP statistics available yet.</td></tr>`;
} else {
sortedIpRequests.forEach(([ip, count], index) => {
ipTableHtml += `
<tr>
<td>${index + 1}</td>
<td>${ip}</td>
<td>${count.toString()}</td>
</tr>
`;
});
}
ipTableHtml += `
</tbody>
</table>
`;
const content = `
<div class="page-header">
<h1>Statistics</h1>
</div>
<p>Total Chat Completion Requests: ${typeof statistics.chatCompletionRequests === 'bigint' ?
statistics.chatCompletionRequests.toString() : statistics.chatCompletionRequests}</p>
<h3>IP Request Leaderboard (Chat Completion)</h3>
${ipTableHtml}
`;
return new Response(HTML_TEMPLATE("Statistics", "view-statistics", content), {
headers: { "Content-Type": "text/html" },
});
}
// 处理请求路由
async function handleRequest(req: Request, info: ConnInfo): Promise<Response> {
const url = new URL(req.url);
// 确保 ConnInfo.remoteAddr 是 NetAddr 类型
const ipAddress = (info.remoteAddr as Deno.NetAddr)?.hostname || "unknown";
// 规范化路径, 处理 /v1/v1 等情况
const normalizedPath = normalizePath(url.pathname);
// 允许预检请求
if (req.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Max-Age": "86400",
},
});
}
// 添加 CORS 头
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};
try {
// 路由请求
let response: Response;
console.log(`Handling request for normalized path: ${normalizedPath} from ${ipAddress}`);
if (normalizedPath === "/" || normalizedPath === "/index.html") {
response = handleHomeRequest();
} else if (normalizedPath === "/view/models") {
response = await handleViewModelsRequest();
} else if (normalizedPath === "/view/statistics") {
response = await handleStatisticsRequest(); // 统计页面现在是异步的
} else if (normalizedPath === "/v1/models" || normalizedPath === "/models") {
// 不再统计模型请求数
response = await handleModelsRequest(req);
} else if (normalizedPath === "/v1/chat/completions" || normalizedPath === "/chat/completions") {
// 聊天完成请求的统计已移入 handleChatCompletionRequest
response = await handleChatCompletionRequest(req, ipAddress);
} else {
response = new Response(JSON.stringify({
error: {
message: "Not found. Available endpoints: /, /view/models, /view/statistics, /v1/models, /v1/chat/completions",
type: "invalid_request_error",
}
}), {
status: 404,
headers: { "Content-Type": "application/json" },
});
}
// 创建一个新的 Response 来添加 CORS 头, 同时保留原始响应的体, 状态码和原始头部
const newHeaders = new Headers(response.headers);
Object.entries(corsHeaders).forEach(([key, value]) => {
newHeaders.set(key, value);
});
// Special handling for stream responses: ensure SSE headers are kept
if (response.headers.get("content-type") === "text/event-stream") {
newHeaders.set("Content-Type", "text/event-stream");
newHeaders.set("Cache-Control", "no-cache");
newHeaders.set("Connection", "keep-alive");
} else if (!newHeaders.has("Content-Type")) {
// For non-stream, ensure JSON content type if not already set (e.g., HTML)
newHeaders.set("Content-Type", "application/json");
}
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newHeaders,
});
} catch (error) {
console.error("Request handling error:", error);
return new Response(
JSON.stringify({
error: {
message: `Server error: ${error.message}`,
type: "server_error",
}
}),
{
status: 500,
headers: {
"Content-Type": "application/json",
...corsHeaders,
},
}
);
}
}
// 启动服务器
console.log(`Starting ${SERVICE_NAME} on port ${PORT}...`);
// 在启动服务器前初始化 KV
initKv().then(() => {
// serve 函数的第二个参数可以是一个选项对象,其中包含 signal 或 onListen 等属性
// 要获取 ConnInfo,serve 的第一个参数需要是一个处理函数,它接收 Request 和 ConnInfo
serve(handleRequest, { port: PORT });
});
// 捕获终止信号,关闭 KV 连接
Deno.addSignalListener("SIGINT", async () => {
console.log("Received SIGINT. Closing KV connection.");
if (kv) {
await kv.close();
}
Deno.exit();
});
Deno.addSignalListener("SIGTERM", async () => {
console.log("Received SIGTERM. Closing KV connection.");
if (kv) {
await kv.close();
}
Deno.exit();
});