diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6ae0591 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/src/worker.mjs b/src/worker.mjs new file mode 100644 index 0000000..26d8292 --- /dev/null +++ b/src/worker.mjs @@ -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]; +}