mirror of
https://gitea.com/PublicAffairs/openai-github-copilot.git
synced 2025-07-23 20:30:45 +02:00
Initial implementation as Cloudflare Worker
Inspired by: - https://github.com/haibbo/cf-openai-azure-proxy/raw/main/cf-openai-azure-proxy.js - https://github.com/CaoYunzhou/cocopilot-gpt Memo: - https://developers.google.com/search/docs/crawling-indexing/block-indexing
This commit is contained in:
13
.editorconfig
Normal file
13
.editorconfig
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# http://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.yml]
|
||||||
|
indent_style = space
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
228
src/worker.mjs
Normal file
228
src/worker.mjs
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
export default {
|
||||||
|
async fetch (request) {
|
||||||
|
if (request.method === "OPTIONS") {
|
||||||
|
return handleOPTIONS(request);
|
||||||
|
}
|
||||||
|
const url = new URL(request.url);
|
||||||
|
if (url.pathname === "/v1/models") {
|
||||||
|
return handleModels(request);
|
||||||
|
}
|
||||||
|
const auth = request.headers.get("Authorization");
|
||||||
|
let authKey = auth && auth.split(" ")[1];
|
||||||
|
if (!authKey) {
|
||||||
|
return new Response("401 Unauthorized", {
|
||||||
|
status: 401
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { token, errResponse } = await getToken(authKey);
|
||||||
|
if (errResponse) { return errResponse; }
|
||||||
|
return makeRequest(request, url.pathname, await makeHeaders(token));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleModels = async () => {
|
||||||
|
const data = {
|
||||||
|
"object": "list",
|
||||||
|
"data": [
|
||||||
|
//{"id": "text-embedding-3-large", "object": "model", "created": 1705953180, "owned_by": "system"},
|
||||||
|
{"id": "text-embedding-3-small", "object": "model", "created": 1705948997, "owned_by": "system"},
|
||||||
|
{"id": "text-embedding-ada-002", "object": "model", "created": 1671217299, "owned_by": "openai-internal"},
|
||||||
|
{"id": "gpt-3.5-turbo", "object": "model", "created": 1677610602, "owned_by": "openai"},
|
||||||
|
{"id": "gpt-4", "object": "model", "created": 1687882411, "owned_by": "openai"},
|
||||||
|
]
|
||||||
|
};
|
||||||
|
const json = JSON.stringify(data, null, 2);
|
||||||
|
return new Response(json, {
|
||||||
|
headers: {
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Robots-Tag": "noindex",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOPTIONS = async () => {
|
||||||
|
return new Response(null, {
|
||||||
|
headers: {
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "*",
|
||||||
|
"Access-Control-Allow-Headers": "*",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const chatVersion = "0.13.0";
|
||||||
|
const vscodeVersion = "1.87.0";
|
||||||
|
const apiVersion = "2023-07-07";
|
||||||
|
const getToken = async (authKey) => {
|
||||||
|
if (authKey.startsWith("sk-")) { // dumb attempt to hide real auth key from malicious web services
|
||||||
|
const decoded = atob(authKey.substring(3));
|
||||||
|
if (/^[ -~]+$/.test(decoded)) { // ascii
|
||||||
|
authKey = decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let githubapihost = "api.github.com";
|
||||||
|
if (authKey.startsWith("ccu_") || authKey.endsWith("CoCopilot")) {
|
||||||
|
githubapihost = "api.cocopilot.org";
|
||||||
|
}
|
||||||
|
let response = await fetch(`https://${githubapihost}/copilot_internal/v2/token`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Authorization": `token ${authKey}`,
|
||||||
|
"Host": githubapihost,
|
||||||
|
"Editor-Version": `vscode/${vscodeVersion}`,
|
||||||
|
"Editor-Plugin-Version": `copilot-chat/${chatVersion}`,
|
||||||
|
"User-Agent": `GitHubCopilotChat/${chatVersion}`,
|
||||||
|
"Accept": "*/*",
|
||||||
|
"Accept-Encoding": "*",
|
||||||
|
//'Accept-Encoding': 'gzip, deflate, br',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
return { errResponse: response };
|
||||||
|
}
|
||||||
|
const text = await response.text();
|
||||||
|
let token;
|
||||||
|
try {
|
||||||
|
token = JSON.parse(text)["token"];
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e.message,"\n",text);
|
||||||
|
return { errResponse: new Response(e.message + "\n" + text, { status: 400 }) };
|
||||||
|
}
|
||||||
|
if (!token) {
|
||||||
|
console.error("token not found:\n", text);
|
||||||
|
return { errResponse: new Response("token not found:\n" + text, { status: 400 }) };
|
||||||
|
}
|
||||||
|
return { token };
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeHeaders = async (token) => {
|
||||||
|
const createSha256Hash = async (input) => {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const data = encoder.encode(input);
|
||||||
|
const hash = await crypto.subtle.digest("SHA-256", data);
|
||||||
|
return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, "0")).join("");
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
"Authorization": `Bearer ${token}`,
|
||||||
|
"Host": "api.githubcopilot.com",
|
||||||
|
"X-Request-Id": crypto.randomUUID(),
|
||||||
|
"X-Github-Api-Version": apiVersion,
|
||||||
|
"Vscode-Sessionid": crypto.randomUUID() + Date.now().toString(),
|
||||||
|
"vscode-machineid": await createSha256Hash(token),
|
||||||
|
"Editor-Version": `vscode/${vscodeVersion}`,
|
||||||
|
"Editor-Plugin-Version": `copilot-chat/${chatVersion}`,
|
||||||
|
"Openai-Organization": "github-copilot",
|
||||||
|
"Copilot-Integration-Id": "vscode-chat",
|
||||||
|
"Openai-Intent": "conversation-panel",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"User-Agent": `GitHubCopilotChat/${chatVersion}`,
|
||||||
|
"Accept": "*/*",
|
||||||
|
"Accept-Encoding": "gzip, deflate, br",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeRequest = async (request, path, headers) => {
|
||||||
|
let isStream;
|
||||||
|
try {
|
||||||
|
isStream = (await request.clone().json()).stream;
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
if (path.startsWith("/v1/")) {
|
||||||
|
path = path.substring(3);
|
||||||
|
}
|
||||||
|
const response = await fetch(`https://api.githubcopilot.com${path}`, {
|
||||||
|
method: request.method,
|
||||||
|
headers,
|
||||||
|
body: request.body,
|
||||||
|
});
|
||||||
|
headers = new Headers(response.headers);
|
||||||
|
headers.set("Access-Control-Allow-Origin", "*");
|
||||||
|
let body;
|
||||||
|
if (response.ok && path === "/chat/completions" && request.method === "POST") {
|
||||||
|
if (isStream) {
|
||||||
|
body = response.body
|
||||||
|
.pipeThrough(new TextDecoderStream())
|
||||||
|
.pipeThrough(new TransformStream({ transform: cleanStream, buffer: "" }))
|
||||||
|
.pipeThrough(new TextEncoderStream());
|
||||||
|
headers.set("Content-Type", "text/event-stream");
|
||||||
|
} else {
|
||||||
|
body = clean(await response.text());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
body = response.body;
|
||||||
|
}
|
||||||
|
return new Response(body, { status: response.status, statusText: response.statusText, headers });
|
||||||
|
};
|
||||||
|
|
||||||
|
const clean = (str) => {
|
||||||
|
let json;
|
||||||
|
try {
|
||||||
|
json = JSON.parse(str);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
json.model = "gpt-4"; // stub
|
||||||
|
//json.system_fingerprint = null;
|
||||||
|
json.object = "chat.completion";
|
||||||
|
delete json.prompt_filter_results;
|
||||||
|
for (const item of json.choices) {
|
||||||
|
delete item.content_filter_results;
|
||||||
|
//item.logprobs = null;
|
||||||
|
}
|
||||||
|
return JSON.stringify(json);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanLine = (str) => {
|
||||||
|
if (str.startsWith("data: ")) {
|
||||||
|
let json;
|
||||||
|
try {
|
||||||
|
json = JSON.parse(str.substring(6));
|
||||||
|
} catch (e) {
|
||||||
|
if (str !== "data: [DONE]") {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
if (json.choices.length === 0) { return; } // json.prompt_filter_results
|
||||||
|
json.model = "gpt-4"; // stub
|
||||||
|
//json.system_fingerprint = null;
|
||||||
|
json.object = "chat.completion.chunk";
|
||||||
|
for (const item of json.choices) {
|
||||||
|
delete item.content_filter_offsets;
|
||||||
|
delete item.content_filter_results;
|
||||||
|
const delta = item.delta;
|
||||||
|
for (const k in delta) {
|
||||||
|
if (delta[k] === null ) { delete delta[k]; }
|
||||||
|
}
|
||||||
|
//delta.content = delta.content || "";
|
||||||
|
//item.logprobs = null;
|
||||||
|
//item.finish_reason = item.finish_reason || null;
|
||||||
|
}
|
||||||
|
return "data: " + JSON.stringify(json);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const delimiter = "\n\n";
|
||||||
|
async function cleanStream (chunk, controller) {
|
||||||
|
chunk = await chunk;
|
||||||
|
if (!chunk) {
|
||||||
|
if (this.buffer) {
|
||||||
|
controller.enqueue(cleanLine(this.buffer) || this.buffer);
|
||||||
|
}
|
||||||
|
controller.enqueue("\n");
|
||||||
|
controller.terminate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.buffer += chunk;
|
||||||
|
let lines = this.buffer.split(delimiter);
|
||||||
|
for (let i = 0; i < lines.length - 1; i++) {
|
||||||
|
const line = cleanLine(lines[i]);
|
||||||
|
if (line) {
|
||||||
|
controller.enqueue(line + delimiter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.buffer = lines[lines.length - 1];
|
||||||
|
}
|
Reference in New Issue
Block a user