mirror of
https://github.com/Nezumi-2711/transfer-api.git
synced 2026-09-22 05:41:55 +00:00
1160 lines
38 KiB
JavaScript
1160 lines
38 KiB
JavaScript
const DEFAULT_UPSTREAM_BASE_URL = "https://unlimited.surf";
|
|
const DEFAULT_OPENAI_MODEL = "gateway-gpt-5";
|
|
const DEFAULT_CLAUDE_MODEL = "claude-opus-4-7-20260101";
|
|
|
|
const CORS_HEADERS = {
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Access-Control-Allow-Methods": "GET,POST,PUT,PATCH,DELETE,OPTIONS",
|
|
"Access-Control-Allow-Headers": "authorization,content-type,x-api-key,anthropic-api-key,anthropic-version,anthropic-beta,openai-beta",
|
|
"Access-Control-Expose-Headers": "content-type,request-id,x-request-id",
|
|
};
|
|
|
|
export default {
|
|
async fetch(request, env) {
|
|
if (request.method === "OPTIONS") {
|
|
return new Response(null, { status: 204, headers: CORS_HEADERS });
|
|
}
|
|
|
|
const url = new URL(request.url);
|
|
const path = normalizePath(url.pathname);
|
|
|
|
try {
|
|
const authError = validateWorkerApiKey(request, env);
|
|
if (authError) return authError;
|
|
|
|
if (path === "/" || path === "/health") {
|
|
return jsonResponse(serviceInfo(request, env));
|
|
}
|
|
|
|
if (path.startsWith("/api/")) {
|
|
return proxyUpstream(request, env, path);
|
|
}
|
|
|
|
if (path === "/mcp" || path === "/v1/mcp" || path === "/anthropic/mcp" || path === "/anthropic/v1/mcp") {
|
|
return jsonResponse(mcpInfo(request));
|
|
}
|
|
|
|
if (path === "/codex" || path === "/v1/codex" || path === "/anthropic/codex" || path === "/anthropic/v1/codex") {
|
|
return textResponse(codexSetup(request), "text/plain; charset=utf-8");
|
|
}
|
|
|
|
if (path === "/v1/setup" || path === "/anthropic/setup" || path === "/anthropic/v1/setup") {
|
|
return textResponse(agentSetup(request), "text/plain; charset=utf-8");
|
|
}
|
|
|
|
if (path === "/v1/messages" || (path === "/v1/models" && looksLikeAnthropicRequest(request)) || path.startsWith("/anthropic/")) {
|
|
return handleAnthropic(request, env, path);
|
|
}
|
|
|
|
if (path.startsWith("/v1/")) {
|
|
return handleOpenAI(request, env, path);
|
|
}
|
|
|
|
return errorResponse(404, "not_found", `No route for ${path}`);
|
|
} catch (error) {
|
|
return errorResponse(500, "internal_error", error && error.message ? error.message : String(error));
|
|
}
|
|
},
|
|
};
|
|
|
|
async function handleOpenAI(request, env, path) {
|
|
if ((path === "/v1/key" || path === "/v1/auth-key" || path === "/v1/usage") && request.method === "GET") {
|
|
const rawPath = path === "/v1/usage" ? "/api/usage" : "/api/key";
|
|
return proxyUpstream(request, env, rawPath);
|
|
}
|
|
|
|
if (path === "/v1/models" && request.method === "GET") {
|
|
if (looksLikeAnthropicRequest(request)) {
|
|
return anthropicModels(request, env);
|
|
}
|
|
return openAIModels(request, env);
|
|
}
|
|
|
|
if (path === "/v1/search" && request.method === "POST") {
|
|
const body = await readJson(request);
|
|
return openAIDirectCapability(request, env, body, "/api/search");
|
|
}
|
|
|
|
if (path === "/v1/merge" && request.method === "POST") {
|
|
const body = await readJson(request);
|
|
return openAIDirectCapability(request, env, body, "/api/merge");
|
|
}
|
|
|
|
if (path === "/v1/chat/completions" && request.method === "POST") {
|
|
const body = await readJson(request);
|
|
return openAIChatCompletions(request, env, body);
|
|
}
|
|
|
|
if (path === "/v1/responses" && request.method === "POST") {
|
|
const body = await readJson(request);
|
|
return openAIResponses(request, env, body);
|
|
}
|
|
|
|
if (path === "/v1/files" && request.method === "GET") {
|
|
return jsonResponse({ object: "list", data: [], has_more: false });
|
|
}
|
|
|
|
if (path === "/v1/files" && request.method === "POST") {
|
|
return openAIFileUpload(request, env);
|
|
}
|
|
|
|
if ((path === "/v1/files/extract" || path === "/v1/attachments/extract") && request.method === "POST") {
|
|
const body = await readJson(request);
|
|
const extracted = await callUnlimitedJson(request, env, "/api/attachments/extract", body);
|
|
return jsonResponse(extracted);
|
|
}
|
|
|
|
if (path.startsWith("/v1/files/") && request.method === "GET") {
|
|
return errorResponse(404, "not_found", "This Worker is stateless. Bind KV/R2 if you need persisted OpenAI file retrieval.");
|
|
}
|
|
|
|
if (path === "/v1/embeddings" || path.startsWith("/v1/audio/") || path.startsWith("/v1/images/")) {
|
|
return errorResponse(501, "unsupported_endpoint", `${path} is not exposed by unlimited.surf and cannot be emulated faithfully.`);
|
|
}
|
|
|
|
return errorResponse(404, "not_found", `Unsupported OpenAI-compatible route ${path}`);
|
|
}
|
|
|
|
async function openAIDirectCapability(request, env, body, route) {
|
|
const model = body.model || env.DEFAULT_MODEL || DEFAULT_OPENAI_MODEL;
|
|
const created = nowSeconds();
|
|
const id = `chatcmpl_${randomId()}`;
|
|
const payload = buildUnlimitedPayload({ ...body, web_search: route === "/api/search", merge: route === "/api/merge" }, route);
|
|
|
|
if (body.stream !== false) {
|
|
const upstream = await callUnlimitedStream(request, env, route, payload);
|
|
return sseResponse(streamOpenAIChat(upstream, { id, created, model }));
|
|
}
|
|
|
|
const result = await collectUnlimitedText(request, env, route, payload);
|
|
return jsonResponse({
|
|
id,
|
|
object: "chat.completion",
|
|
created,
|
|
model,
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
message: { role: "assistant", content: result.text },
|
|
logprobs: null,
|
|
finish_reason: result.finishReason || "stop",
|
|
},
|
|
],
|
|
usage: usageFromText(payload.message || payload.query || "", result.text),
|
|
system_fingerprint: `unlimited-surf-worker:${route}`,
|
|
});
|
|
}
|
|
|
|
async function openAIChatCompletions(request, env, body) {
|
|
const model = body.model || env.DEFAULT_MODEL || DEFAULT_OPENAI_MODEL;
|
|
const created = nowSeconds();
|
|
const id = `chatcmpl_${randomId()}`;
|
|
const route = chooseUnlimitedRoute(body);
|
|
const payload = buildUnlimitedPayload(body, route);
|
|
|
|
if (body.stream) {
|
|
const upstream = await callUnlimitedStream(request, env, route, payload);
|
|
return sseResponse(streamOpenAIChat(upstream, { id, created, model }));
|
|
}
|
|
|
|
const result = await collectUnlimitedText(request, env, route, payload);
|
|
return jsonResponse({
|
|
id,
|
|
object: "chat.completion",
|
|
created,
|
|
model,
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
message: { role: "assistant", content: result.text },
|
|
logprobs: null,
|
|
finish_reason: result.finishReason || "stop",
|
|
},
|
|
],
|
|
usage: usageFromText(payload.message || "", result.text),
|
|
system_fingerprint: "unlimited-surf-worker",
|
|
});
|
|
}
|
|
|
|
async function openAIResponses(request, env, body) {
|
|
const model = body.model || env.DEFAULT_MODEL || DEFAULT_OPENAI_MODEL;
|
|
const created = nowSeconds();
|
|
const id = `resp_${randomId()}`;
|
|
const syntheticChatBody = responsesToChatBody(body, model);
|
|
const route = chooseUnlimitedRoute(syntheticChatBody);
|
|
const payload = buildUnlimitedPayload(syntheticChatBody, route);
|
|
|
|
if (body.stream) {
|
|
const upstream = await callUnlimitedStream(request, env, route, payload);
|
|
return sseResponse(streamOpenAIResponses(upstream, { id, created, model }));
|
|
}
|
|
|
|
const result = await collectUnlimitedText(request, env, route, payload);
|
|
return jsonResponse({
|
|
id,
|
|
object: "response",
|
|
created_at: created,
|
|
status: "completed",
|
|
error: null,
|
|
incomplete_details: null,
|
|
instructions: body.instructions || null,
|
|
max_output_tokens: body.max_output_tokens || body.max_tokens || null,
|
|
model,
|
|
output: [
|
|
{
|
|
id: `msg_${randomId()}`,
|
|
type: "message",
|
|
status: "completed",
|
|
role: "assistant",
|
|
content: [{ type: "output_text", text: result.text, annotations: [] }],
|
|
},
|
|
],
|
|
output_text: result.text,
|
|
parallel_tool_calls: true,
|
|
previous_response_id: body.previous_response_id || null,
|
|
reasoning: body.reasoning || null,
|
|
store: body.store || false,
|
|
temperature: body.temperature || null,
|
|
text: body.text || { format: { type: "text" } },
|
|
tool_choice: body.tool_choice || "auto",
|
|
tools: body.tools || [],
|
|
top_p: body.top_p || null,
|
|
truncation: body.truncation || "disabled",
|
|
usage: responseUsageFromText(payload.message || "", result.text),
|
|
user: body.user || null,
|
|
});
|
|
}
|
|
|
|
async function handleAnthropic(request, env, path) {
|
|
const anthPath = path.startsWith("/anthropic/") ? normalizePath(path.slice("/anthropic".length) || "/") : path;
|
|
|
|
if ((anthPath === "/v1/key" || anthPath === "/key" || anthPath === "/v1/auth-key" || anthPath === "/auth-key") && request.method === "GET") {
|
|
return proxyUpstream(request, env, "/api/key");
|
|
}
|
|
|
|
if ((anthPath === "/v1/usage" || anthPath === "/usage") && request.method === "GET") {
|
|
return proxyUpstream(request, env, "/api/usage");
|
|
}
|
|
|
|
if ((anthPath === "/v1/models" || anthPath === "/models") && request.method === "GET") {
|
|
return anthropicModels(request, env);
|
|
}
|
|
|
|
if ((anthPath === "/v1/messages" || anthPath === "/messages") && request.method === "POST") {
|
|
const body = await readJson(request);
|
|
return anthropicMessages(request, env, body);
|
|
}
|
|
|
|
if ((anthPath === "/v1/search" || anthPath === "/search") && request.method === "POST") {
|
|
const body = await readJson(request);
|
|
return anthropicDirectCapability(request, env, body, "/api/search");
|
|
}
|
|
|
|
if ((anthPath === "/v1/merge" || anthPath === "/merge") && request.method === "POST") {
|
|
const body = await readJson(request);
|
|
return anthropicDirectCapability(request, env, body, "/api/merge");
|
|
}
|
|
|
|
if (anthPath === "/v1/setup" || anthPath === "/setup") {
|
|
return textResponse(agentSetup(request), "text/plain; charset=utf-8");
|
|
}
|
|
|
|
return errorResponse(404, "not_found", `Unsupported Anthropic-compatible route ${path}`);
|
|
}
|
|
|
|
async function anthropicDirectCapability(request, env, body, route) {
|
|
const requestedModel = body.model || env.DEFAULT_CLAUDE_MODEL || DEFAULT_CLAUDE_MODEL;
|
|
const payload = buildAnthropicUnlimitedPayload({ ...body, web_search: route === "/api/search", merge: route === "/api/merge" }, route);
|
|
const id = `msg_${randomId()}`;
|
|
|
|
if (body.stream !== false) {
|
|
const upstream = await callUnlimitedStream(request, env, route, payload);
|
|
return sseResponse(streamAnthropicMessages(upstream, { id, model: requestedModel }));
|
|
}
|
|
|
|
const result = await collectUnlimitedText(request, env, route, payload);
|
|
return jsonResponse({
|
|
id,
|
|
type: "message",
|
|
role: "assistant",
|
|
model: requestedModel,
|
|
content: [{ type: "text", text: result.text }],
|
|
stop_reason: anthropicStopReason(result.finishReason),
|
|
stop_sequence: null,
|
|
usage: anthropicUsageFromText(payload.message || payload.query || "", result.text),
|
|
});
|
|
}
|
|
|
|
async function anthropicMessages(request, env, body) {
|
|
const requestedModel = body.model || env.DEFAULT_CLAUDE_MODEL || DEFAULT_CLAUDE_MODEL;
|
|
const route = chooseUnlimitedRoute(body);
|
|
const payload = buildAnthropicUnlimitedPayload(body, route);
|
|
const id = `msg_${randomId()}`;
|
|
|
|
if (body.stream) {
|
|
const upstream = await callUnlimitedStream(request, env, route, payload);
|
|
return sseResponse(streamAnthropicMessages(upstream, { id, model: requestedModel }));
|
|
}
|
|
|
|
const result = await collectUnlimitedText(request, env, route, payload);
|
|
return jsonResponse({
|
|
id,
|
|
type: "message",
|
|
role: "assistant",
|
|
model: requestedModel,
|
|
content: [{ type: "text", text: result.text }],
|
|
stop_reason: anthropicStopReason(result.finishReason),
|
|
stop_sequence: null,
|
|
usage: anthropicUsageFromText(payload.message || "", result.text),
|
|
});
|
|
}
|
|
|
|
async function openAIModels(request, env) {
|
|
const catalog = await getModelCatalog(request, env);
|
|
return jsonResponse({
|
|
object: "list",
|
|
data: catalog.map((model) => ({
|
|
id: model.id,
|
|
object: "model",
|
|
created: 0,
|
|
owned_by: model.provider || "unlimited.surf",
|
|
permission: [],
|
|
root: model.id,
|
|
parent: null,
|
|
})),
|
|
});
|
|
}
|
|
|
|
async function anthropicModels(request, env) {
|
|
const catalog = await getModelCatalog(request, env);
|
|
const claudeModels = catalog
|
|
.filter((model) => /claude|anthropic/i.test(`${model.id} ${model.name || ""} ${model.provider || ""}`))
|
|
.map((model) => toAnthropicModel(model));
|
|
|
|
return jsonResponse({
|
|
data: claudeModels.length ? claudeModels : [toAnthropicModel({ id: DEFAULT_CLAUDE_MODEL, name: "Claude Opus 4.7" })],
|
|
has_more: false,
|
|
first_id: claudeModels[0] ? claudeModels[0].id : DEFAULT_CLAUDE_MODEL,
|
|
last_id: claudeModels[claudeModels.length - 1] ? claudeModels[claudeModels.length - 1].id : DEFAULT_CLAUDE_MODEL,
|
|
});
|
|
}
|
|
|
|
async function openAIFileUpload(request, env) {
|
|
const contentType = request.headers.get("content-type") || "";
|
|
if (!contentType.includes("multipart/form-data")) {
|
|
return errorResponse(400, "invalid_request_error", "OpenAI file upload expects multipart/form-data with a file field.");
|
|
}
|
|
|
|
const form = await request.formData();
|
|
const file = form.get("file");
|
|
if (!file || typeof file === "string") {
|
|
return errorResponse(400, "invalid_request_error", "Missing multipart file field named file.");
|
|
}
|
|
|
|
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
const payload = {
|
|
name: file.name || "upload.bin",
|
|
type: file.type || "application/octet-stream",
|
|
data: bytesToBase64(bytes),
|
|
};
|
|
const extracted = await callUnlimitedJson(request, env, "/api/attachments/extract", payload);
|
|
const id = `file_${randomId()}`;
|
|
return jsonResponse({
|
|
id,
|
|
object: "file",
|
|
bytes: bytes.byteLength,
|
|
created_at: nowSeconds(),
|
|
filename: payload.name,
|
|
purpose: form.get("purpose") || "assistants",
|
|
status: extracted && extracted.success === false ? "error" : "processed",
|
|
status_details: null,
|
|
unlimited_extract: extracted,
|
|
});
|
|
}
|
|
|
|
function chooseUnlimitedRoute(body) {
|
|
if (body.models && Array.isArray(body.models) && body.models.length >= 2) return "/api/merge";
|
|
if (body.merge || body.merge_ai) return "/api/merge";
|
|
if (body.query || body.web_search || body.web_search_options || hasWebSearchTool(body.tools)) return "/api/search";
|
|
return "/api/chat";
|
|
}
|
|
|
|
function buildUnlimitedPayload(body, route) {
|
|
if (route === "/api/search") {
|
|
return {
|
|
query: body.query || latestUserText(body.messages) || inputToText(body.input) || body.prompt || "",
|
|
model: mapUpstreamModel(body.model),
|
|
effort: body.effort || reasoningEffort(body),
|
|
};
|
|
}
|
|
|
|
const message = body.message || messagesToText(body.messages) || inputToText(body.input) || body.prompt || "";
|
|
const payload = {
|
|
message,
|
|
model: mapUpstreamModel(body.model),
|
|
effort: body.effort || reasoningEffort(body),
|
|
};
|
|
|
|
if (route === "/api/merge") {
|
|
payload.models = Array.isArray(body.models) && body.models.length ? body.models.map(mapUpstreamModel) : undefined;
|
|
}
|
|
|
|
return payload;
|
|
}
|
|
|
|
function buildAnthropicUnlimitedPayload(body, route) {
|
|
if (route === "/api/search") {
|
|
return {
|
|
query: latestUserText(body.messages) || body.query || "",
|
|
model: mapUpstreamModel(body.model),
|
|
effort: body.effort || reasoningEffort(body),
|
|
};
|
|
}
|
|
|
|
const prompt = anthropicMessagesToText(body);
|
|
const payload = {
|
|
message: prompt,
|
|
model: mapUpstreamModel(body.model),
|
|
effort: body.effort || reasoningEffort(body),
|
|
};
|
|
|
|
if (route === "/api/merge") {
|
|
payload.models = Array.isArray(body.models) && body.models.length ? body.models.map(mapUpstreamModel) : undefined;
|
|
}
|
|
|
|
return payload;
|
|
}
|
|
|
|
function responsesToChatBody(body, fallbackModel) {
|
|
const messages = [];
|
|
if (body.instructions) messages.push({ role: "system", content: body.instructions });
|
|
const inputText = inputToText(body.input);
|
|
if (inputText) messages.push({ role: "user", content: inputText });
|
|
|
|
return {
|
|
...body,
|
|
model: body.model || fallbackModel,
|
|
messages,
|
|
stream: body.stream,
|
|
};
|
|
}
|
|
|
|
async function proxyUpstream(request, env, path) {
|
|
const upstreamUrl = new URL(path + new URL(request.url).search, upstreamBase(env));
|
|
const headers = new Headers(request.headers);
|
|
const key = optionalUpstreamApiKey(request, env);
|
|
if (key) headers.set("authorization", `Bearer ${key}`);
|
|
headers.delete("host");
|
|
|
|
const init = {
|
|
method: request.method,
|
|
headers,
|
|
body: request.method === "GET" || request.method === "HEAD" ? undefined : request.body,
|
|
redirect: "manual",
|
|
};
|
|
|
|
const response = await fetch(upstreamUrl, init);
|
|
return addCors(response);
|
|
}
|
|
|
|
async function callUnlimitedJson(request, env, path, payload) {
|
|
const response = await fetch(new URL(path, upstreamBase(env)), {
|
|
method: "POST",
|
|
headers: upstreamHeaders(request, env, false),
|
|
body: JSON.stringify(payload || {}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`upstream ${path} failed: ${response.status} ${await response.text()}`);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
async function callUnlimitedStream(request, env, path, payload) {
|
|
const response = await fetch(new URL(path, upstreamBase(env)), {
|
|
method: "POST",
|
|
headers: upstreamHeaders(request, env, true),
|
|
body: JSON.stringify(payload || {}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`upstream ${path} failed: ${response.status} ${await response.text()}`);
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
async function collectUnlimitedText(request, env, path, payload) {
|
|
const response = await callUnlimitedStream(request, env, path, payload);
|
|
const events = await readUnlimitedEvents(response);
|
|
let text = "";
|
|
let finishReason = "stop";
|
|
const annotations = [];
|
|
|
|
for (const event of events) {
|
|
if (typeof event.delta === "string") text += event.delta;
|
|
if (event.results) annotations.push(event.results);
|
|
if (event.finish && event.reason) finishReason = event.reason;
|
|
}
|
|
|
|
return { text, finishReason, annotations, rawEvents: events };
|
|
}
|
|
|
|
async function getModelCatalog(request, env) {
|
|
try {
|
|
const headers = new Headers();
|
|
const key = optionalUpstreamApiKey(request, env);
|
|
if (key) headers.set("Authorization", `Bearer ${key}`);
|
|
const response = await fetch(new URL("/api/models", upstreamBase(env)), { headers });
|
|
if (!response.ok) throw new Error(`models failed: ${response.status}`);
|
|
const data = await response.json();
|
|
const models = Array.isArray(data) ? data : Array.isArray(data.data) ? data.data : [];
|
|
return models.map((model) => ({
|
|
id: model.id || model.name || String(model),
|
|
name: model.name || model.id || String(model),
|
|
provider: model.provider || providerFromModel(model.id || model.name || ""),
|
|
tier: model.tier || undefined,
|
|
})).filter((model) => model.id);
|
|
} catch (_) {
|
|
return fallbackModels();
|
|
}
|
|
}
|
|
|
|
function streamOpenAIChat(upstream, meta) {
|
|
return streamUnlimitedEvents(upstream, {
|
|
start(controller) {
|
|
writeSse(controller, {
|
|
id: meta.id,
|
|
object: "chat.completion.chunk",
|
|
created: meta.created,
|
|
model: meta.model,
|
|
choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }],
|
|
});
|
|
},
|
|
delta(controller, text) {
|
|
writeSse(controller, {
|
|
id: meta.id,
|
|
object: "chat.completion.chunk",
|
|
created: meta.created,
|
|
model: meta.model,
|
|
choices: [{ index: 0, delta: { content: text }, finish_reason: null }],
|
|
});
|
|
},
|
|
finish(controller, reason) {
|
|
writeSse(controller, {
|
|
id: meta.id,
|
|
object: "chat.completion.chunk",
|
|
created: meta.created,
|
|
model: meta.model,
|
|
choices: [{ index: 0, delta: {}, finish_reason: openAIStopReason(reason) }],
|
|
});
|
|
writeRawSse(controller, "data: [DONE]\n\n");
|
|
},
|
|
});
|
|
}
|
|
|
|
function streamOpenAIResponses(upstream, meta) {
|
|
const outputId = `msg_${randomId()}`;
|
|
return streamUnlimitedEvents(upstream, {
|
|
start(controller) {
|
|
writeSseEvent(controller, "response.created", {
|
|
type: "response.created",
|
|
response: {
|
|
id: meta.id,
|
|
object: "response",
|
|
created_at: meta.created,
|
|
status: "in_progress",
|
|
model: meta.model,
|
|
output: [],
|
|
},
|
|
});
|
|
writeSseEvent(controller, "response.output_item.added", {
|
|
type: "response.output_item.added",
|
|
output_index: 0,
|
|
item: { id: outputId, type: "message", status: "in_progress", role: "assistant", content: [] },
|
|
});
|
|
writeSseEvent(controller, "response.content_part.added", {
|
|
type: "response.content_part.added",
|
|
item_id: outputId,
|
|
output_index: 0,
|
|
content_index: 0,
|
|
part: { type: "output_text", text: "", annotations: [] },
|
|
});
|
|
},
|
|
delta(controller, text) {
|
|
writeSseEvent(controller, "response.output_text.delta", {
|
|
type: "response.output_text.delta",
|
|
item_id: outputId,
|
|
output_index: 0,
|
|
content_index: 0,
|
|
delta: text,
|
|
});
|
|
},
|
|
finish(controller) {
|
|
writeSseEvent(controller, "response.output_text.done", {
|
|
type: "response.output_text.done",
|
|
item_id: outputId,
|
|
output_index: 0,
|
|
content_index: 0,
|
|
text: "",
|
|
});
|
|
writeSseEvent(controller, "response.content_part.done", {
|
|
type: "response.content_part.done",
|
|
item_id: outputId,
|
|
output_index: 0,
|
|
content_index: 0,
|
|
part: { type: "output_text", text: "", annotations: [] },
|
|
});
|
|
writeSseEvent(controller, "response.output_item.done", {
|
|
type: "response.output_item.done",
|
|
output_index: 0,
|
|
item: { id: outputId, type: "message", status: "completed", role: "assistant", content: [] },
|
|
});
|
|
writeSseEvent(controller, "response.completed", {
|
|
type: "response.completed",
|
|
response: { id: meta.id, object: "response", created_at: meta.created, status: "completed", model: meta.model },
|
|
});
|
|
writeRawSse(controller, "data: [DONE]\n\n");
|
|
},
|
|
});
|
|
}
|
|
|
|
function streamAnthropicMessages(upstream, meta) {
|
|
return streamUnlimitedEvents(upstream, {
|
|
start(controller) {
|
|
writeSseEvent(controller, "message_start", {
|
|
type: "message_start",
|
|
message: {
|
|
id: meta.id,
|
|
type: "message",
|
|
role: "assistant",
|
|
model: meta.model,
|
|
content: [],
|
|
stop_reason: null,
|
|
stop_sequence: null,
|
|
usage: { input_tokens: 0, output_tokens: 0 },
|
|
},
|
|
});
|
|
writeSseEvent(controller, "content_block_start", {
|
|
type: "content_block_start",
|
|
index: 0,
|
|
content_block: { type: "text", text: "" },
|
|
});
|
|
},
|
|
delta(controller, text) {
|
|
writeSseEvent(controller, "content_block_delta", {
|
|
type: "content_block_delta",
|
|
index: 0,
|
|
delta: { type: "text_delta", text },
|
|
});
|
|
},
|
|
finish(controller, reason) {
|
|
writeSseEvent(controller, "content_block_stop", { type: "content_block_stop", index: 0 });
|
|
writeSseEvent(controller, "message_delta", {
|
|
type: "message_delta",
|
|
delta: { stop_reason: anthropicStopReason(reason), stop_sequence: null },
|
|
usage: { output_tokens: 0 },
|
|
});
|
|
writeSseEvent(controller, "message_stop", { type: "message_stop" });
|
|
},
|
|
});
|
|
}
|
|
|
|
function streamUnlimitedEvents(upstream, handlers) {
|
|
const encoder = new TextEncoder();
|
|
const decoder = new TextDecoder();
|
|
|
|
return new ReadableStream({
|
|
async start(controller) {
|
|
let finished = false;
|
|
handlers.start && handlers.start(controller);
|
|
|
|
try {
|
|
const reader = upstream.body.getReader();
|
|
let buffer = "";
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
buffer += decoder.decode(value, { stream: true });
|
|
const lines = buffer.split(/\r?\n/);
|
|
buffer = lines.pop() || "";
|
|
|
|
for (const line of lines) {
|
|
if (!line.startsWith("data:")) continue;
|
|
const parsed = parseSseJson(line.slice(5).trim());
|
|
if (!parsed) continue;
|
|
|
|
if (typeof parsed.delta === "string" && parsed.delta.length) {
|
|
handlers.delta && handlers.delta(controller, parsed.delta, parsed);
|
|
}
|
|
|
|
if (parsed.finish || parsed.done) {
|
|
finished = true;
|
|
handlers.finish && handlers.finish(controller, parsed.reason || "stop", parsed);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!finished) handlers.finish && handlers.finish(controller, "stop", {});
|
|
} catch (error) {
|
|
controller.enqueue(encoder.encode(`event: error\ndata: ${JSON.stringify({ error: error.message || String(error) })}\n\n`));
|
|
} finally {
|
|
controller.close();
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
async function readUnlimitedEvents(response) {
|
|
const decoder = new TextDecoder();
|
|
const reader = response.body.getReader();
|
|
const events = [];
|
|
let buffer = "";
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
buffer += decoder.decode(value, { stream: true });
|
|
const lines = buffer.split(/\r?\n/);
|
|
buffer = lines.pop() || "";
|
|
for (const line of lines) {
|
|
if (!line.startsWith("data:")) continue;
|
|
const parsed = parseSseJson(line.slice(5).trim());
|
|
if (parsed) events.push(parsed);
|
|
}
|
|
}
|
|
|
|
if (buffer.startsWith("data:")) {
|
|
const parsed = parseSseJson(buffer.slice(5).trim());
|
|
if (parsed) events.push(parsed);
|
|
}
|
|
|
|
return events;
|
|
}
|
|
|
|
function writeSse(controller, data) {
|
|
writeRawSse(controller, `data: ${JSON.stringify(data)}\n\n`);
|
|
}
|
|
|
|
function writeSseEvent(controller, event, data) {
|
|
writeRawSse(controller, `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
}
|
|
|
|
function writeRawSse(controller, chunk) {
|
|
controller.enqueue(new TextEncoder().encode(chunk));
|
|
}
|
|
|
|
function sseResponse(body) {
|
|
return new Response(body, {
|
|
headers: {
|
|
...CORS_HEADERS,
|
|
"Content-Type": "text/event-stream; charset=utf-8",
|
|
"Cache-Control": "no-cache, no-transform",
|
|
"Connection": "keep-alive",
|
|
"X-Accel-Buffering": "no",
|
|
},
|
|
});
|
|
}
|
|
|
|
function jsonResponse(data, init = {}) {
|
|
return new Response(JSON.stringify(data, null, 2), {
|
|
...init,
|
|
headers: {
|
|
...CORS_HEADERS,
|
|
"Content-Type": "application/json; charset=utf-8",
|
|
"Cache-Control": "no-store",
|
|
...(init.headers || {}),
|
|
},
|
|
});
|
|
}
|
|
|
|
function textResponse(text, contentType, init = {}) {
|
|
return new Response(text, {
|
|
...init,
|
|
headers: {
|
|
...CORS_HEADERS,
|
|
"Content-Type": contentType,
|
|
"Cache-Control": "no-store",
|
|
...(init.headers || {}),
|
|
},
|
|
});
|
|
}
|
|
|
|
function errorResponse(status, code, message) {
|
|
return jsonResponse({
|
|
error: {
|
|
message,
|
|
type: code,
|
|
code,
|
|
},
|
|
}, { status });
|
|
}
|
|
|
|
function addCors(response) {
|
|
const headers = new Headers(response.headers);
|
|
for (const [key, value] of Object.entries(CORS_HEADERS)) headers.set(key, value);
|
|
return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
|
|
}
|
|
|
|
async function readJson(request) {
|
|
if (!request.body) return {};
|
|
const text = await request.text();
|
|
if (!text.trim()) return {};
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch (_) {
|
|
throw new Error("Request body must be valid JSON.");
|
|
}
|
|
}
|
|
|
|
function upstreamHeaders(request, env, wantsStream) {
|
|
const headers = new Headers();
|
|
headers.set("Authorization", `Bearer ${upstreamApiKey(request, env)}`);
|
|
headers.set("Content-Type", "application/json");
|
|
if (wantsStream) headers.set("Accept", "text/event-stream");
|
|
return headers;
|
|
}
|
|
|
|
function upstreamApiKey(request, env) {
|
|
const key = optionalUpstreamApiKey(request, env);
|
|
if (key) return key;
|
|
|
|
if (env.WORKER_API_KEY) {
|
|
throw new Error("Missing upstream API key. Set UNLIMITED_SURF_API_KEY when WORKER_API_KEY is enabled.");
|
|
}
|
|
|
|
throw new Error("Missing upstream API key. Set UNLIMITED_SURF_API_KEY or pass Authorization: Bearer <key> / x-api-key: <key>.");
|
|
}
|
|
|
|
function optionalUpstreamApiKey(request, env) {
|
|
const configured = env.UNLIMITED_SURF_API_KEY || env.API_KEY || env.AUTH_KEY;
|
|
if (configured) return configured;
|
|
|
|
if (env.WORKER_API_KEY) return "";
|
|
|
|
return clientApiKey(request);
|
|
}
|
|
|
|
function validateWorkerApiKey(request, env) {
|
|
const expected = env.WORKER_API_KEY;
|
|
if (!expected) return null;
|
|
|
|
const actual = clientApiKey(request);
|
|
if (actual && constantTimeEqual(actual, expected)) return null;
|
|
|
|
return jsonResponse({
|
|
error: {
|
|
message: "Invalid or missing Worker API key.",
|
|
type: "authentication_error",
|
|
code: "invalid_api_key",
|
|
},
|
|
}, { status: 401, headers: { "WWW-Authenticate": "Bearer" } });
|
|
}
|
|
|
|
function clientApiKey(request) {
|
|
const auth = request.headers.get("authorization") || "";
|
|
if (/^bearer\s+/i.test(auth)) return auth.replace(/^bearer\s+/i, "").trim();
|
|
|
|
const xKey = request.headers.get("x-api-key") || request.headers.get("anthropic-api-key");
|
|
return xKey ? xKey.trim() : "";
|
|
}
|
|
|
|
function constantTimeEqual(actual, expected) {
|
|
const actualText = String(actual || "");
|
|
const expectedText = String(expected || "");
|
|
if (actualText.length !== expectedText.length) return false;
|
|
|
|
let diff = 0;
|
|
for (let i = 0; i < actualText.length; i += 1) {
|
|
diff |= actualText.charCodeAt(i) ^ expectedText.charCodeAt(i);
|
|
}
|
|
return diff === 0;
|
|
}
|
|
|
|
function upstreamBase(env) {
|
|
return stripTrailingSlash(env.UPSTREAM_BASE_URL || DEFAULT_UPSTREAM_BASE_URL) + "/";
|
|
}
|
|
|
|
function normalizePath(path) {
|
|
if (!path || path === "") return "/";
|
|
const normalized = path.replace(/\/+/g, "/");
|
|
return normalized.length > 1 ? normalized.replace(/\/+$/, "") : normalized;
|
|
}
|
|
|
|
function messagesToText(messages) {
|
|
if (!Array.isArray(messages)) return "";
|
|
return messages.map((message) => {
|
|
const role = message.role || "user";
|
|
return `${role}: ${contentToText(message.content)}`;
|
|
}).filter(Boolean).join("\n\n");
|
|
}
|
|
|
|
function anthropicMessagesToText(body) {
|
|
const parts = [];
|
|
if (body.system) parts.push(`system: ${contentToText(body.system)}`);
|
|
if (Array.isArray(body.tools) && body.tools.length) {
|
|
parts.push(`available tools: ${JSON.stringify(body.tools)}`);
|
|
parts.push("If a tool is required, describe the intended tool call clearly. MCP and local tools must be executed by the client agent.");
|
|
}
|
|
if (Array.isArray(body.messages)) parts.push(messagesToText(body.messages));
|
|
return parts.filter(Boolean).join("\n\n");
|
|
}
|
|
|
|
function inputToText(input) {
|
|
if (!input) return "";
|
|
if (typeof input === "string") return input;
|
|
if (!Array.isArray(input)) return contentToText(input);
|
|
|
|
return input.map((item) => {
|
|
if (typeof item === "string") return item;
|
|
if (item.type === "message") return `${item.role || "user"}: ${contentToText(item.content)}`;
|
|
if (item.role) return `${item.role}: ${contentToText(item.content)}`;
|
|
if (item.type === "input_text" || item.type === "output_text") return item.text || "";
|
|
return contentToText(item);
|
|
}).filter(Boolean).join("\n\n");
|
|
}
|
|
|
|
function contentToText(content) {
|
|
if (content == null) return "";
|
|
if (typeof content === "string") return content;
|
|
if (Array.isArray(content)) {
|
|
return content.map((part) => contentToText(part)).filter(Boolean).join("\n");
|
|
}
|
|
if (typeof content === "object") {
|
|
if (typeof content.text === "string") return content.text;
|
|
if (typeof content.input_text === "string") return content.input_text;
|
|
if (content.type === "text" && typeof content.text === "string") return content.text;
|
|
if (content.type === "input_text" && typeof content.text === "string") return content.text;
|
|
if (content.type === "image_url") return `[image: ${content.image_url && content.image_url.url ? content.image_url.url : "attached"}]`;
|
|
if (content.type === "image") return "[image attached]";
|
|
if (content.type === "tool_result") return `[tool_result ${content.tool_use_id || ""}] ${contentToText(content.content)}`;
|
|
if (content.type === "tool_use") return `[tool_use ${content.name || "tool"}] ${JSON.stringify(content.input || {})}`;
|
|
if (content.type) return `[${content.type}] ${JSON.stringify(content)}`;
|
|
}
|
|
return String(content);
|
|
}
|
|
|
|
function latestUserText(messages) {
|
|
if (!Array.isArray(messages)) return "";
|
|
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
if ((messages[i].role || "user") === "user") return contentToText(messages[i].content);
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function hasWebSearchTool(tools) {
|
|
if (!Array.isArray(tools)) return false;
|
|
return tools.some((tool) => {
|
|
const type = tool && (tool.type || tool.name || (tool.function && tool.function.name));
|
|
return /web.?search|browser|search/i.test(String(type || ""));
|
|
});
|
|
}
|
|
|
|
function reasoningEffort(body) {
|
|
if (body.effort) return body.effort;
|
|
if (typeof body.reasoning_effort === "string") return body.reasoning_effort;
|
|
if (body.reasoning && typeof body.reasoning.effort === "string") return body.reasoning.effort;
|
|
return "medium";
|
|
}
|
|
|
|
function mapUpstreamModel(model) {
|
|
if (!model) return DEFAULT_OPENAI_MODEL;
|
|
if (model.startsWith("gateway-")) return model;
|
|
if (/^claude-/i.test(model)) return `gateway-${model.replace(/-\d{8}$/, "")}`;
|
|
if (/^gpt-/i.test(model)) return `gateway-${model}`;
|
|
if (/^gemini-/i.test(model)) return `gateway-google-${model.replace(/^gemini-/i, "")}`;
|
|
return model;
|
|
}
|
|
|
|
function toAnthropicModel(model) {
|
|
const id = model.id.startsWith("gateway-") ? model.id.replace(/^gateway-/, "") : model.id;
|
|
const versioned = /^claude-.*-\d{8}$/.test(id) ? id : anthropicVersionedId(id);
|
|
return {
|
|
id: versioned,
|
|
type: "model",
|
|
display_name: model.name || versioned,
|
|
created_at: "2026-01-01T00:00:00Z",
|
|
};
|
|
}
|
|
|
|
function anthropicVersionedId(id) {
|
|
if (/^claude-/i.test(id)) return `${id}-20260101`;
|
|
return id;
|
|
}
|
|
|
|
function providerFromModel(model) {
|
|
if (/claude|anthropic/i.test(model)) return "anthropic";
|
|
if (/gemini|google/i.test(model)) return "google";
|
|
if (/gpt|openai/i.test(model)) return "openai";
|
|
return "unlimited.surf";
|
|
}
|
|
|
|
function fallbackModels() {
|
|
return [
|
|
{ id: "gateway-gpt-5", name: "GPT-5", provider: "openai", tier: "flagship" },
|
|
{ id: "gateway-gpt-5-1", name: "GPT-5.1", provider: "openai", tier: "flagship" },
|
|
{ id: "gateway-claude-opus-4-7", name: "Claude Opus 4.7", provider: "anthropic", tier: "flagship" },
|
|
{ id: "gateway-google-2.5-pro", name: "Gemini 2.5 Pro", provider: "google", tier: "flagship" },
|
|
{ id: "gateway-gemini-3-flash", name: "Gemini 3 Flash", provider: "google", tier: "fast" },
|
|
];
|
|
}
|
|
|
|
function parseSseJson(data) {
|
|
if (!data || data === "[DONE]") return null;
|
|
try {
|
|
return JSON.parse(data);
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function openAIStopReason(reason) {
|
|
if (!reason) return "stop";
|
|
if (reason === "max_tokens") return "length";
|
|
if (reason === "tool_use") return "tool_calls";
|
|
return reason === "end_turn" ? "stop" : reason;
|
|
}
|
|
|
|
function anthropicStopReason(reason) {
|
|
if (!reason || reason === "stop") return "end_turn";
|
|
if (reason === "length") return "max_tokens";
|
|
if (reason === "tool_calls") return "tool_use";
|
|
return reason;
|
|
}
|
|
|
|
function usageFromText(input, output) {
|
|
const promptTokens = estimateTokens(input);
|
|
const completionTokens = estimateTokens(output);
|
|
return { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: promptTokens + completionTokens };
|
|
}
|
|
|
|
function responseUsageFromText(input, output) {
|
|
const inputTokens = estimateTokens(input);
|
|
const outputTokens = estimateTokens(output);
|
|
return { input_tokens: inputTokens, output_tokens: outputTokens, total_tokens: inputTokens + outputTokens };
|
|
}
|
|
|
|
function anthropicUsageFromText(input, output) {
|
|
return { input_tokens: estimateTokens(input), output_tokens: estimateTokens(output) };
|
|
}
|
|
|
|
function estimateTokens(text) {
|
|
if (!text) return 0;
|
|
return Math.max(1, Math.ceil(String(text).length / 4));
|
|
}
|
|
|
|
function nowSeconds() {
|
|
return Math.floor(Date.now() / 1000);
|
|
}
|
|
|
|
function randomId() {
|
|
const bytes = new Uint8Array(12);
|
|
crypto.getRandomValues(bytes);
|
|
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
}
|
|
|
|
function stripTrailingSlash(value) {
|
|
return String(value || "").replace(/\/+$/, "");
|
|
}
|
|
|
|
function bytesToBase64(bytes) {
|
|
let binary = "";
|
|
const chunkSize = 0x8000;
|
|
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
|
|
}
|
|
return btoa(binary);
|
|
}
|
|
|
|
function looksLikeAnthropicRequest(request) {
|
|
return request.headers.has("anthropic-version") || request.headers.has("anthropic-beta") || request.headers.has("x-api-key");
|
|
}
|
|
|
|
function serviceInfo(request, env) {
|
|
const origin = new URL(request.url).origin;
|
|
return {
|
|
ok: true,
|
|
service: "unlimited.surf OpenAI/Anthropic compatibility Worker",
|
|
upstream: stripTrailingSlash(env.UPSTREAM_BASE_URL || DEFAULT_UPSTREAM_BASE_URL),
|
|
routes: {
|
|
raw: `${origin}/api/chat, /api/search, /api/merge, /api/models, /api/key, /api/attachments/extract`,
|
|
openai: `${origin}/v1/chat/completions, /v1/responses, /v1/models, /v1/files`,
|
|
anthropic: `${origin}/v1/messages or ${origin}/anthropic/v1/messages`,
|
|
setup: `${origin}/v1/setup, /v1/codex, /v1/mcp`,
|
|
},
|
|
};
|
|
}
|
|
|
|
function agentSetup(request) {
|
|
const origin = new URL(request.url).origin;
|
|
return `Claude Code / Anthropic-compatible setup
|
|
|
|
PowerShell:
|
|
$env:ANTHROPIC_BASE_URL = "${origin}"
|
|
$env:ANTHROPIC_AUTH_TOKEN = "<your unlimited.surf key>"
|
|
$env:ANTHROPIC_API_KEY = "<your unlimited.surf key>"
|
|
$env:ANTHROPIC_MODEL = "${DEFAULT_CLAUDE_MODEL}"
|
|
claude
|
|
|
|
Bash:
|
|
export ANTHROPIC_BASE_URL="${origin}"
|
|
export ANTHROPIC_AUTH_TOKEN="<your unlimited.surf key>"
|
|
export ANTHROPIC_API_KEY="<your unlimited.surf key>"
|
|
export ANTHROPIC_MODEL="${DEFAULT_CLAUDE_MODEL}"
|
|
claude
|
|
|
|
Goose / Hermes / other agents:
|
|
Provider: Anthropic-compatible
|
|
Base URL: ${origin}
|
|
API key: <your unlimited.surf key>
|
|
Model: ${DEFAULT_CLAUDE_MODEL}
|
|
|
|
Messages endpoint: POST ${origin}/v1/messages
|
|
Models endpoint: GET ${origin}/v1/models
|
|
|
|
MCP tools run in the client/agent environment. Use this Worker as the model endpoint, then configure MCP servers in your IDE or agent.
|
|
`;
|
|
}
|
|
|
|
function codexSetup(request) {
|
|
const origin = new URL(request.url).origin;
|
|
return `Codex custom provider notes
|
|
|
|
OpenAI-compatible Chat Completions:
|
|
base_url = "${origin}/v1"
|
|
api_key = "<your unlimited.surf key>"
|
|
model = "${DEFAULT_OPENAI_MODEL}"
|
|
|
|
OpenAI Responses-compatible route for newer agents:
|
|
POST ${origin}/v1/responses
|
|
|
|
Direct smoke test:
|
|
curl ${origin}/v1/chat/completions \\
|
|
-H "Authorization: Bearer <your unlimited.surf key>" \\
|
|
-H "Content-Type: application/json" \\
|
|
-d '{"model":"${DEFAULT_OPENAI_MODEL}","messages":[{"role":"user","content":"Write a small test function."}],"stream":true}'
|
|
|
|
Anthropic-compatible agent route:
|
|
POST ${origin}/v1/messages
|
|
|
|
MCP execution remains client-side; configure MCP servers in Codex or your IDE, and point the model provider at this Worker.
|
|
`;
|
|
}
|
|
|
|
function mcpInfo(request) {
|
|
const origin = new URL(request.url).origin;
|
|
return {
|
|
supported: true,
|
|
model_endpoint: origin,
|
|
note: "MCP servers execute inside the client or agent. This Worker supplies OpenAI/Anthropic-compatible model endpoints and does not run local MCP tools in the browser or edge runtime.",
|
|
endpoints: {
|
|
openai_responses: `${origin}/v1/responses`,
|
|
openai_chat_completions: `${origin}/v1/chat/completions`,
|
|
anthropic_messages: `${origin}/v1/messages`,
|
|
setup: `${origin}/v1/setup`,
|
|
},
|
|
};
|
|
}
|