copilot-api
Version:
A wrapper around GitHub Copilot API to make it OpenAI compatible, making it usable for other tools.
5 lines (4 loc) • 8.54 kB
JavaScript
import{defineCommand as be,runMain as we}from"citty";import x from"consola";import{serve as Te}from"srvx";import X from"consola";var p=()=>({"content-type":"application/json",accept:"application/json"}),m=e=>`https://api.${e.accountType}.githubcopilot.com`,l=e=>({Authorization:`Bearer ${e.copilotToken}`,"content-type":p()["content-type"],"copilot-integration-id":"vscode-chat","editor-version":`vscode/${e.vsCodeVersion}`,"editor-plugin-version":"copilot-chat/0.24.1","openai-intent":"conversation-panel","x-github-api-version":"2024-12-15","x-request-id":globalThis.crypto.randomUUID(),"x-vscode-user-agent-library-version":"electron-fetch"}),h="https://api.github.com",E=e=>({...p(),authorization:`token ${e.githubToken}`,"editor-version":`vscode/${e.vsCodeVersion}`,"editor-plugin-version":"copilot-chat/0.24.1","user-agent":"GitHubCopilotChat/0.24.1","x-github-api-version":"2024-12-15","x-vscode-user-agent-library-version":"electron-fetch"}),b="https://github.com",w="01ab8ac9400c4e429b23",P=["read:org","read:user","repo","user:email","workflow"].join(" ");var i=class extends Error{response;constructor(t,r){super(t),this.response=r}};var o={accountType:"individual",manualApprove:!1,rateLimitWait:!1};var T=async()=>{let e=await fetch(`${m(o)}/models`,{headers:l(o)});if(!e.ok)throw new i("Failed to get models",e);return await e.json()};async function A(){let e=await T();o.models=e,X.info(`Available models:
${e.data.map(t=>`- ${t.id}`).join(`
`)}`)}import u from"node:fs/promises";import Y from"node:os";import U from"node:path";var j=U.join(Y.homedir(),".local","share","copilot-api"),Z=U.join(j,"github_token"),f={APP_DIR:j,GITHUB_TOKEN_PATH:Z};async function L(){await u.mkdir(f.APP_DIR,{recursive:!0}),await ee(f.GITHUB_TOKEN_PATH)}async function ee(e){try{await u.access(e,u.constants.W_OK)}catch{await u.writeFile(e,""),await u.chmod(e,384)}}import c from"consola";import $ from"node:fs/promises";var C=async()=>{let e=await fetch(`${h}/copilot_internal/v2/token`,{headers:E(o)});if(!e.ok)throw new i("Failed to get Copilot token",e);return await e.json()};async function G(){let e=await fetch(`${b}/login/device/code`,{method:"POST",headers:p(),body:JSON.stringify({client_id:w,scope:P})});if(!e.ok)throw new i("Failed to get device code",e);return await e.json()}async function I(){let e=await fetch(`${h}/user`,{headers:{authorization:`token ${o.githubToken}`,...p()}});if(!e.ok)throw new i("Failed to get GitHub user",e);return await e.json()}import R from"consola";var g=e=>new Promise(t=>{setTimeout(t,e)});async function B(e){let t=(e.interval+1)*1e3;for(R.debug(`Polling access token with interval of ${t}ms`);;){let r=await fetch(`${b}/login/oauth/access_token`,{method:"POST",headers:p(),body:JSON.stringify({client_id:w,device_code:e.device_code,grant_type:"urn:ietf:params:oauth:grant-type:device_code"})});if(!r.ok){await g(t),R.error("Failed to poll access token:",await r.text());continue}let n=await r.json();R.debug("Polling access token response:",n);let{access_token:a}=n;if(a)return a;await g(t)}}var te=()=>$.readFile(f.GITHUB_TOKEN_PATH,"utf8"),oe=e=>$.writeFile(f.GITHUB_TOKEN_PATH,e),N=async()=>{let{token:e,refresh_in:t}=await C();o.copilotToken=e;let r=(t-60)*1e3;setInterval(async()=>{c.start("Refreshing Copilot token");try{let{token:n}=await C();o.copilotToken=n}catch(n){throw c.error("Failed to refresh Copilot token:",n),n}},r)};async function q(){try{let e=await te();if(e){o.githubToken=e,await M();return}c.info("Not logged in, getting new access token");let t=await G();c.debug("Device code response:",t),c.info(`Please enter the code "${t.user_code}" in ${t.verification_uri}`);let r=await B(t);await oe(r),o.githubToken=r,await M()}catch(e){throw e instanceof i?(c.error("Failed to get GitHub token:",await e.response.json()),e):(c.error("Failed to get GitHub token:",e),e)}}async function M(){let e=await I();c.info(`Logged in as ${e.login}`)}import ne from"consola";var re="1.98.1";async function S(){let t=await(await fetch("https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=visual-studio-code-bin")).text(),r=/pkgver=([0-9.]+)/,n=t.match(r);return n?n[1]:re}await S();var D=async()=>{let e=await S();o.vsCodeVersion=e,ne.info(`Using VSCode version: ${e}`)};import{Hono as fe}from"hono";import{cors as ge}from"hono/cors";import{logger as he}from"hono/logger";import{Hono as le}from"hono";import ie from"consola";async function d(e,t){if(ie.error("Error occurred:",t),t instanceof i){let r=await t.response.text();return e.json({error:{message:r,type:"error"}},t.response.status)}return e.json({error:{message:t.message,type:"error"}},500)}import pe from"consola";import{streamSSE as ce}from"hono/streaming";import se from"consola";var O=async()=>{if(!await se.prompt("Accept incoming request?",{type:"confirm"}))throw new i("Request rejected",Response.json({message:"Request rejected"},{status:403}))};var F=e=>e==null;import H from"consola";async function V(e){if(e.rateLimitSeconds===void 0)return;let t=Date.now();if(!e.lastRequestTimestamp){e.lastRequestTimestamp=t;return}let r=(t-e.lastRequestTimestamp)/1e3;if(r>e.rateLimitSeconds){e.lastRequestTimestamp=t;return}let n=Math.ceil(e.rateLimitSeconds-r);if(!e.rateLimitWait)throw H.warn(`Rate limit exceeded. Need to wait ${n} more seconds.`),new i("Rate limit exceeded",Response.json({message:"Rate limit exceeded"},{status:429}));let a=n*1e3;H.warn(`Rate limit reached. Waiting ${n} seconds before proceeding...`),await g(a),e.lastRequestTimestamp=t,H.info("Rate limit wait completed, proceeding with request")}import{countTokens as W}from"gpt-tokenizer/model/gpt-4o";var K=e=>{let t=e.filter(_=>_.role!=="assistant"),r=e.filter(_=>_.role==="assistant"),n=W(t),a=W(r);return{input:n,output:a}};import{events as ae}from"fetch-event-stream";var z=async e=>{if(!o.copilotToken)throw new Error("Copilot token not found");let t=await fetch(`${m(o)}/chat/completions`,{method:"POST",headers:l(o),body:JSON.stringify(e)});if(!t.ok)throw new i("Failed to create chat completions",t);return e.stream?ae(t):await t.json()};async function J(e){await V(o);let t=await e.req.json();if(pe.info("Current token count:",K(t.messages)),o.manualApprove&&await O(),F(t.max_tokens)){let n=o.models?.data.find(a=>a.id===t.model);t={...t,max_tokens:n?.capabilities.limits.max_output_tokens}}let r=await z(t);return me(r)?e.json(r):ce(e,async n=>{for await(let a of r)await n.writeSSE(a)})}var me=e=>Object.hasOwn(e,"choices");var y=new le;y.post("/",async e=>{try{return await J(e)}catch(t){return await d(e,t)}});import{Hono as de}from"hono";var Q=async e=>{if(!o.copilotToken)throw new Error("Copilot token not found");let t=await fetch(`${m(o)}/embeddings`,{method:"POST",headers:l(o),body:JSON.stringify(e)});if(!t.ok)throw new i("Failed to create embeddings",t);return await t.json()};var v=new de;v.post("/",async e=>{try{let t=await e.req.json(),r=await Q(t);return e.json(r)}catch(t){return await d(e,t)}});import{Hono as ue}from"hono";var k=new ue;k.get("/",async e=>{try{let t=await T();return e.json(t)}catch(t){return await d(e,t)}});var s=new fe;s.use(he());s.use(ge());s.get("/",e=>e.text("Server running"));s.route("/chat/completions",y);s.route("/models",k);s.route("/embeddings",v);s.route("/v1/chat/completions",y);s.route("/v1/models",k);s.route("/v1/embeddings",v);async function ye(e){e.verbose&&(x.level=5,x.info("Verbose logging enabled")),e.business&&(o.accountType="business",x.info("Using business plan GitHub account")),o.manualApprove=e.manual,o.rateLimitSeconds=e.rateLimit,o.rateLimitWait=e.rateLimitWait,await L(),await D(),await q(),await N(),await A();let t=`http://localhost:${e.port}`;x.box(`Server started at ${t}`),Te({fetch:s.fetch,port:e.port})}var ve=be({args:{port:{alias:"p",type:"string",default:"4141",description:"Port to listen on"},verbose:{alias:"v",type:"boolean",default:!1,description:"Enable verbose logging"},business:{type:"boolean",default:!1,description:"Use a business plan GitHub Account"},manual:{type:"boolean",default:!1,description:"Enable manual request approval"},"rate-limit":{alias:"r",type:"string",description:"Rate limit in seconds between requests"},wait:{alias:"w",type:"boolean",default:!1,description:"Wait instead of error when rate limit is hit. Has no effect if rate limit is not set"}},run({args:e}){let t=e["rate-limit"],r=t===void 0?void 0:Number.parseInt(t,10),n=Number.parseInt(e.port,10);return ye({port:n,verbose:e.verbose,business:e.business,manual:e.manual,rateLimit:r,rateLimitWait:!!e.wait})}});await we(ve);export{ye as runServer};