UNPKG

copilot-api

Version:

A wrapper around GitHub Copilot API to make it OpenAI compatible, making it usable for other tools.

5 lines (4 loc) 9.43 kB
#!/usr/bin/env node import{defineCommand as Z,runMain as ye}from"citty";import b from"consola";import{serve as xe}from"srvx";import{defineCommand as ie}from"citty";import S from"consola";import f from"node:fs/promises";import ee from"node:os";import E from"node:path";var G=E.join(ee.homedir(),".local","share","copilot-api"),te=E.join(G,"github_token"),m={APP_DIR:G,GITHUB_TOKEN_PATH:te};async function U(){await f.mkdir(m.APP_DIR,{recursive:!0}),await oe(m.GITHUB_TOKEN_PATH)}async function oe(e){try{await f.access(e,f.constants.W_OK)}catch{await f.writeFile(e,""),await f.chmod(e,384)}}import c from"consola";import $ from"node:fs/promises";var p=()=>({"content-type":"application/json",accept:"application/json"}),l=e=>`https://api.${e.accountType}.githubcopilot.com`,u=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",I=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"}),w="https://github.com",T="Iv1.b507a08c87ecfe98",j=["read:user"].join(" ");var i=class extends Error{response;constructor(t,r){super(t),this.response=r}};var o={accountType:"individual",manualApprove:!1,rateLimitWait:!1};var H=async()=>{let e=await fetch(`${h}/copilot_internal/v2/token`,{headers:I(o)});if(!e.ok)throw new i("Failed to get Copilot token",e);return await e.json()};async function L(){let e=await fetch(`${w}/login/device/code`,{method:"POST",headers:p(),body:JSON.stringify({client_id:T,scope:j})});if(!e.ok)throw new i("Failed to get device code",e);return await e.json()}async function B(){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 M(e){let t=(e.interval+1)*1e3;for(R.debug(`Polling access token with interval of ${t}ms`);;){let r=await fetch(`${w}/login/oauth/access_token`,{method:"POST",headers:p(),body:JSON.stringify({client_id:T,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 re=()=>$.readFile(m.GITHUB_TOKEN_PATH,"utf8"),ne=e=>$.writeFile(m.GITHUB_TOKEN_PATH,e),N=async()=>{let{token:e,refresh_in:t}=await H();o.copilotToken=e;let r=(t-60)*1e3;setInterval(async()=>{c.start("Refreshing Copilot token");try{let{token:n}=await H();o.copilotToken=n}catch(n){throw c.error("Failed to refresh Copilot token:",n),n}},r)};async function v(e){try{let t=await re();if(t&&!e?.force){o.githubToken=t,await O();return}c.info("Not logged in, getting new access token");let r=await L();c.debug("Device code response:",r),c.info(`Please enter the code "${r.user_code}" in ${r.verification_uri}`);let n=await M(r);await ne(n),o.githubToken=n,await O()}catch(t){throw t instanceof i?(c.error("Failed to get GitHub token:",await t.response.json()),t):(c.error("Failed to get GitHub token:",t),t)}}async function O(){let e=await B();c.info(`Logged in as ${e.login}`)}async function se(e){e.verbose&&(S.level=5,S.info("Verbose logging enabled")),await v({force:!0}),S.success("GitHub token written to",m.GITHUB_TOKEN_PATH)}var q=ie({meta:{name:"auth",description:"Run GitHub auth flow without running the server"},args:{verbose:{alias:"v",type:"boolean",default:!1,description:"Enable verbose logging"}},run({args:e}){return se({verbose:e.verbose})}});import ae from"consola";var k=async()=>{let e=await fetch(`${l(o)}/models`,{headers:u(o)});if(!e.ok)throw new i("Failed to get models",e);return await e.json()};async function D(){let e=await k();o.models=e,ae.info(`Available models: ${e.data.map(t=>`- ${t.id}`).join(` `)}`)}import ce from"consola";var pe="1.98.1";async function P(){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]:pe}await P();var F=async()=>{let e=await P();o.vsCodeVersion=e,ce.info(`Using VSCode version: ${e}`)};import{Hono as Te}from"hono";import{cors as ve}from"hono/cors";import{logger as ke}from"hono/logger";import{Hono as be}from"hono";import me from"consola";async function d(e,t){if(me.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 de from"consola";import{streamSSE as fe}from"hono/streaming";import le from"consola";var V=async()=>{if(!await le.prompt("Accept incoming request?",{type:"confirm"}))throw new i("Request rejected",Response.json({message:"Request rejected"},{status:403}))};var W=e=>e==null;import A from"consola";async function K(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 A.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;A.warn(`Rate limit reached. Waiting ${n} seconds before proceeding...`),await g(a),e.lastRequestTimestamp=t,A.info("Rate limit wait completed, proceeding with request")}import{countTokens as z}from"gpt-tokenizer/model/gpt-4o";var J=e=>{let t=e.filter(_=>_.role!=="assistant"),r=e.filter(_=>_.role==="assistant"),n=z(t),a=z(r);return{input:n,output:a}};import{events as ue}from"fetch-event-stream";var Q=async e=>{if(!o.copilotToken)throw new Error("Copilot token not found");let t=await fetch(`${l(o)}/chat/completions`,{method:"POST",headers:u(o),body:JSON.stringify(e)});if(!t.ok)throw new i("Failed to create chat completions",t);return e.stream?ue(t):await t.json()};async function X(e){await K(o);let t=await e.req.json();if(de.info("Current token count:",J(t.messages)),o.manualApprove&&await V(),W(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 Q(t);return ge(r)?e.json(r):fe(e,async n=>{for await(let a of r)await n.writeSSE(a)})}var ge=e=>Object.hasOwn(e,"choices");var y=new be;y.post("/",async e=>{try{return await X(e)}catch(t){return await d(e,t)}});import{Hono as he}from"hono";var Y=async e=>{if(!o.copilotToken)throw new Error("Copilot token not found");let t=await fetch(`${l(o)}/embeddings`,{method:"POST",headers:u(o),body:JSON.stringify(e)});if(!t.ok)throw new i("Failed to create embeddings",t);return await t.json()};var x=new he;x.post("/",async e=>{try{let t=await e.req.json(),r=await Y(t);return e.json(r)}catch(t){return await d(e,t)}});import{Hono as we}from"hono";var C=new we;C.get("/",async e=>{try{let t=await k();return e.json(t)}catch(t){return await d(e,t)}});var s=new Te;s.use(ke());s.use(ve());s.get("/",e=>e.text("Server running"));s.route("/chat/completions",y);s.route("/models",C);s.route("/embeddings",x);s.route("/v1/chat/completions",y);s.route("/v1/models",C);s.route("/v1/embeddings",x);async function Ce(e){e.verbose&&(b.level=5,b.info("Verbose logging enabled")),e.business&&(o.accountType="business",b.info("Using business plan GitHub account")),o.manualApprove=e.manual,o.rateLimitSeconds=e.rateLimit,o.rateLimitWait=e.rateLimitWait,await U(),await F(),e.githubToken?(o.githubToken=e.githubToken,b.info("Using provided GitHub token")):await v(),await N(),await D();let t=`http://localhost:${e.port}`;b.box(`Server started at ${t}`),xe({fetch:s.fetch,port:e.port})}var _e=Z({meta:{name:"start",description:"Start the Copilot API server"},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"},"github-token":{alias:"g",type:"string",description:"Provide GitHub token directly instead of using stored token"}},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 Ce({port:n,verbose:e.verbose,business:e.business,manual:e.manual,rateLimit:r,rateLimitWait:!!e.wait,githubToken:e["github-token"]})}}),He=Z({meta:{name:"copilot-api",description:"A wrapper around GitHub Copilot API to make it OpenAI compatible, making it usable for other tools."},subCommands:{auth:q,start:_e}});await ye(He);export{Ce as runServer};