@nghyane/copilot-api
Version:
A wrapper around GitHub Copilot API to make it OpenAI compatible, making it usable for other tools.
5 lines (4 loc) • 17.1 kB
JavaScript
import{defineCommand as Te,runMain as Qe}from"citty";import xe from"consola";import{defineCommand as Ge}from"citty";import ae from"consola";import R from"node:fs/promises";import Se from"node:os";import V from"node:path";var W=V.join(Se.homedir(),".local","share","copilot-api"),Re=V.join(W,"github_token"),b={APP_DIR:W,GITHUB_TOKEN_PATH:Re};async function P(){await R.mkdir(b.APP_DIR,{recursive:!0}),await Ae(b.GITHUB_TOKEN_PATH)}async function Ae(e){try{await R.access(e,R.constants.W_OK)}catch{await R.writeFile(e,""),await R.chmod(e,384)}}import se from"node:fs/promises";import x from"consola";import{randomUUID as Pe}from"node:crypto";import{Agent as K}from"node:https";var Ee=new K({keepAlive:!0,maxSockets:10,maxFreeSockets:5,timeout:3e4}),He=new K({keepAlive:!0,maxSockets:15,maxFreeSockets:8,timeout:6e4}),g=()=>Ee,w=()=>He,h=()=>({"content-type":"application/json",accept:"application/json"}),z="0.26.7",Y=`copilot-chat/${z}`,Q=`GitHubCopilotChat/${z}`,X="2025-04-01",v=e=>`https://api.${e.accountType}.githubcopilot.com`,k=(e,t=!1)=>{let o={Authorization:`Bearer ${e.copilotToken}`,"content-type":h()["content-type"],"copilot-integration-id":"vscode-chat","editor-version":`vscode/${e.vsCodeVersion}`,"editor-plugin-version":Y,"user-agent":Q,"openai-intent":"conversation-panel","x-github-api-version":X,"x-request-id":Pe(),"x-vscode-user-agent-library-version":"electron-fetch"};return t&&(o["copilot-vision-request"]="true"),o},E="https://api.github.com",Z=e=>({...h(),authorization:`token ${e.githubToken}`,"editor-version":`vscode/${e.vsCodeVersion}`,"editor-plugin-version":Y,"user-agent":Q,"x-github-api-version":X,"x-vscode-user-agent-library-version":"electron-fetch"}),H="https://github.com",C="Iv1.b507a08c87ecfe98",ee=["read:user"].join(" ");var a=class extends Error{response;constructor(t,o){super(t),this.response=o,console.error(t,o)}};var r={accountType:"individual",manualApprove:!1,rateLimitWait:!1};var M=async()=>{let e=await fetch(`${E}/copilot_internal/v2/token`,{headers:Z(r),agent:g()});if(!e.ok)throw new a("Failed to get Copilot token",e);return await e.json()};async function te(){let e=await fetch(`${H}/login/device/code`,{method:"POST",headers:h(),body:JSON.stringify({client_id:C,scope:ee}),agent:g()});if(!e.ok)throw new a("Failed to get device code",e);return await e.json()}async function oe(){let e=await fetch(`${E}/user`,{headers:{authorization:`token ${r.githubToken}`,...h()},agent:g()});if(!e.ok)throw new a("Failed to get GitHub user",e);return await e.json()}import Ce from"consola";var A=e=>new Promise(t=>{setTimeout(t,e)});async function ne(e){let t=(e.interval+1)*1e3;for(;;){let o=await fetch(`${H}/login/oauth/access_token`,{method:"POST",headers:h(),body:JSON.stringify({client_id:C,device_code:e.device_code,grant_type:"urn:ietf:params:oauth:grant-type:device_code"}),agent:g()});if(!o.ok){await A(t),Ce.error("Failed to poll access token:",await o.text());continue}let n=await o.json(),{access_token:s}=n;if(s)return s;await A(t)}}var Ie=()=>se.readFile(b.GITHUB_TOKEN_PATH,"utf8"),Oe=e=>se.writeFile(b.GITHUB_TOKEN_PATH,e),ie=async()=>{let{token:e,refresh_in:t}=await M();r.copilotToken=e;let o=(t-60)*1e3;setInterval(async()=>{try{let{token:n}=await M();r.copilotToken=n}catch(n){throw x.error("Failed to refresh Copilot token:",n),n}},o)};async function I(e){try{let t=await Ie();if(t&&!e?.force){r.githubToken=t,await re();return}x.info("Getting new GitHub access token");let o=await te();x.info(`Please enter the code "${o.user_code}" in ${o.verification_uri}`);let n=await ne(o);await Oe(n),r.githubToken=n,await re()}catch(t){throw t instanceof a?(x.error("Failed to get GitHub token:",await t.response.json()),t):(x.error("Failed to get GitHub token:",t),t)}}async function re(){let e=await oe();x.info(`Logged in as ${e.login}`)}async function Ne(e){e.verbose&&(ae.level=5),await P(),await I({force:!0}),ae.success("GitHub token written to",b.GITHUB_TOKEN_PATH)}var ce=Ge({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 Ne({verbose:e.verbose})}});var $=null,pe=0,Le=5*60*1e3,O=async()=>{let e=Date.now();if($&&e<pe)return $;let t=await fetch(`${v(r)}/models`,{headers:k(r),agent:w()});if(!t.ok)throw new a("Failed to get models",t);let o=await t.json();return $=o,pe=e+Le,o};function me(e){let t=[["claude-sonnet-4-","claude-sonnet-4"],["claude-3-7-sonnet","claude-3.7-sonnet"],["claude-3-5-sonnet","claude-3.5-sonnet"],["gpt-4.1","claude-sonnet-4"]];for(let[o,n]of t)if(e.startsWith(o))return n;return e}function le(e){return e==="claude-sonnet-4"?"claude-4-sonnet":e}async function ue(){let e=await O();r.models=e}var de="1.98.1";async function Ue(){try{let t=await(await fetch("https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=visual-studio-code-bin")).text(),o=/pkgver=([0-9.]+)/;return t.match(o)?.[1]||de}catch{return de}}var fe=async()=>{r.vsCodeVersion=await Ue()};import{Hono as Ke}from"hono";import{cors as ze}from"hono/cors";import{logger as Ye}from"hono/logger";import{Hono as Fe}from"hono";import je from"consola";async function T(e,t){if(je.error("Error occurred:",t),t instanceof a){let o;try{o=await t.response.text()}catch{o=t.message}return e.json({error:{message:o,type:"error"}},t.response.status)}return e.json({error:{message:t.message,type:"error"}},500)}import{streamSSE as Be}from"hono/streaming";import qe from"consola";var ge=async()=>{if(!await qe.prompt("Accept incoming request?",{type:"confirm"}))throw new a("Request rejected",Response.json({message:"Request rejected"},{status:403}))};function D(e){return e.system||e.tools?.[0]?.input_schema||e.tool_choice&&typeof e.tool_choice=="object"||e.messages?.some(o=>Array.isArray(o.content)&&o.content.some(n=>n.type==="text"))?"anthropic":"openai"}function Me(e){if(e){if(typeof e=="string")return e;if(typeof e=="object"){if(e.type==="auto")return"auto";if(e.type==="any")return"required";if(e.type==="tool"&&e.name)return{type:"function",function:{name:e.name}}}return"auto"}}function $e(e){return e.includes("claude")?e.includes("claude-4")||e.includes("sonnet-4")?"claude-sonnet-4":e.includes("claude-3.7")||e.includes("3.7")?"claude-3.7-sonnet":"claude-3.5-sonnet":e}function he(e){if(D(e)==="openai")return e;let t=[];if(e.system){let o=Array.isArray(e.system)?e.system.map(n=>n.text).join(`
`):e.system;t.push({role:"system",content:o})}return e.messages.forEach(o=>{t.push({role:o.role,content:Array.isArray(o.content)?o.content.map(n=>n.text||n.content||"").filter(Boolean).join(`
`):o.content})}),{model:$e(e.model),messages:t,temperature:e.temperature,max_tokens:e.max_tokens,stream:e.stream,tools:e.tools?.map(o=>({type:"function",function:{name:o.name,description:o.description,parameters:o.input_schema}})),tool_choice:Me(e.tool_choice)}}function ye(e,t){let o=`msg_${Math.random().toString(36).substring(2,15)}${Math.random().toString(36).substring(2,15)}`,n=e.choices[0].message,s=[];if(n.content&&s.push({type:"text",text:n.content}),n.tool_calls)for(let i of n.tool_calls)s.push({type:"tool_use",id:i.id,name:i.function.name,input:JSON.parse(i.function.arguments)});let c="end_turn";return e.choices[0].finish_reason==="length"?c="max_tokens":e.choices[0].finish_reason==="tool_calls"&&(c="tool_use"),{id:o,type:"message",role:"assistant",content:s,model:t,stop_reason:c,usage:{input_tokens:e.usage.prompt_tokens,output_tokens:e.usage.completion_tokens}}}async function*be(e,t){let o=!0,n=!1,s=!1,c=0,i=new Map;for await(let m of e){if(c++,m.data==="[DONE]"){if(s)for(let[u]of i.entries())yield{event:"content_block_stop",data:JSON.stringify({type:"content_block_stop",index:u})};n&&(yield{event:"content_block_stop",data:JSON.stringify({type:"content_block_stop",index:s?1:0})}),yield{event:"message_delta",data:JSON.stringify({type:"message_delta",delta:{stop_reason:"end_turn",stop_sequence:null},usage:{output_tokens:0}})},yield{event:"message_stop",data:JSON.stringify({type:"message_stop"})};break}try{let u=JSON.parse(m.data),f=u.choices?.[0]?.delta?.content,_=u.choices?.[0]?.delta?.tool_calls;if(o){let p=u.usage||{input_tokens:0,output_tokens:1};yield{event:"message_start",data:JSON.stringify({type:"message_start",message:{id:`msg_${Math.random().toString(36).substring(2,15)}${Math.random().toString(36).substring(2,15)}`,type:"message",role:"assistant",content:[],model:t,stop_reason:null,stop_sequence:null,usage:{input_tokens:p.prompt_tokens||p.input_tokens||0,output_tokens:1}}})},o=!1}if(f&&f.trim()&&(n||(yield{event:"content_block_start",data:JSON.stringify({type:"content_block_start",index:s?1:0,content_block:{type:"text",text:""}})},n=!0),yield{event:"content_block_delta",data:JSON.stringify({type:"content_block_delta",index:s?1:0,delta:{type:"text_delta",text:f}})},c%3===0&&(yield{event:"ping",data:JSON.stringify({type:"ping"})})),_)for(let p of _){let y=p.index||0;if(!i.has(y)){let S=p.id||`tool_${Math.random().toString(36).substring(2,15)}`,J=p.function?.name||"unknown";i.set(y,{id:S,name:J,arguments:""}),yield{event:"content_block_start",data:JSON.stringify({type:"content_block_start",index:y,content_block:{type:"tool_use",id:S,name:J,input:{}}})},s=!0}if(p.function?.arguments){let S=i.get(y);S.arguments+=p.function.arguments,yield{event:"content_block_delta",data:JSON.stringify({type:"content_block_delta",index:y,delta:{type:"input_json_delta",partial_json:p.function.arguments}})}}}let q=u.choices?.[0]?.finish_reason;if(q){if(s)for(let[y,S]of i.entries())yield{event:"content_block_stop",data:JSON.stringify({type:"content_block_stop",index:y})};n&&(yield{event:"content_block_stop",data:JSON.stringify({type:"content_block_stop",index:s?1:0})});let p="end_turn";q==="length"?p="max_tokens":q==="tool_calls"&&(p="tool_use"),yield{event:"message_delta",data:JSON.stringify({type:"message_delta",delta:{stop_reason:p,stop_sequence:null},usage:u.usage||{output_tokens:0}})},yield{event:"message_stop",data:JSON.stringify({type:"message_stop"})};break}}catch(u){console.debug("Skipping malformed streaming event:",u)}}}import d from"consola";var G=class e{static instance;logDir="logs";enabled=process.env.ENABLE_PAYLOAD_LOGGING==="true";static getInstance(){return e.instance||(e.instance=new e),e.instance}async ensureLogDir(){if(this.enabled)try{let t=await import("node:fs/promises");await t.mkdir(this.logDir,{recursive:!0}),await t.writeFile(`${this.logDir}/.gitkeep`,"")}catch{d.debug("Log directory already exists or created")}}generateRequestId(){return`req_${Date.now()}_${Math.random().toString(36).substring(2,11)}`}getLogFileName(t,o){let n=new Date().toISOString().split("T")[0];return`${this.logDir}/${n}_${t}_${o}.json`}async logRequest(t,o){let n=this.generateRequestId();if(!this.enabled)return d.debug("Payload logging disabled, skipping request log"),n;await this.ensureLogDir();let s={timestamp:new Date().toISOString(),requestId:n,type:"request",format:o,data:t},c=this.getLogFileName(n,"request");try{await(await import("node:fs/promises")).writeFile(c,JSON.stringify(s,null,2)),d.debug(`Request logged: ${c}`)}catch(i){d.error("Failed to log request:",i)}return n}async logResponse(t,o,n){if(!this.enabled){d.debug("Payload logging disabled, skipping response log");return}let s={timestamp:new Date().toISOString(),requestId:t,type:"response",format:n,data:o},c=this.getLogFileName(t,"response");try{await(await import("node:fs/promises")).writeFile(c,JSON.stringify(s,null,2)),d.debug(`Response logged: ${c}`)}catch(i){d.error("Failed to log response:",i)}}async logError(t,o,n){if(!this.enabled){d.debug("Payload logging disabled, skipping error log");return}let s={timestamp:new Date().toISOString(),requestId:t,type:"error",format:n,data:{message:o.message,stack:o.stack,...typeof o=="object"?o:{error:o}}},c=this.getLogFileName(t,"error");try{await(await import("node:fs/promises")).writeFile(c,JSON.stringify(s,null,2)),d.debug(`Error logged: ${c}`)}catch(i){d.error("Failed to log error:",i)}}};import B from"consola";async function _e(e){if(e.rateLimitSeconds===void 0)return;let t=Date.now();if(!e.lastRequestTimestamp){e.lastRequestTimestamp=t;return}let o=(t-e.lastRequestTimestamp)/1e3;if(o>e.rateLimitSeconds){e.lastRequestTimestamp=t;return}let n=Math.ceil(e.rateLimitSeconds-o);if(!e.rateLimitWait)throw B.warn(`Rate limit exceeded. Need to wait ${n} more seconds.`),new a("Rate limit exceeded",Response.json({message:"Rate limit exceeded"},{status:429}));let s=n*1e3;B.warn(`Rate limit reached. Waiting ${n} seconds before proceeding...`),await A(s),e.lastRequestTimestamp=t,B.info("Rate limit wait completed, proceeding with request")}import{events as De}from"fetch-event-stream";async function*we(e){let t=De(e,{retry:!1});for await(let o of t)!o.data||o.data==="[DONE]"||o.data.trim()===""||(yield o)}var ve=async e=>{if(!r.copilotToken)throw new Error("Copilot token not found");let t=e.messages?.some(n=>Array.isArray(n.content)&&n.content.some(s=>s.type==="image_url"||s.type==="image")),o=await fetch(`${v(r)}/chat/completions`,{method:"POST",headers:k(r,t),body:JSON.stringify(e),agent:w()});if(!o.ok)throw new a(`Failed to create chat completions: ${await o.text()}`,o);return e.stream?we(o):o.json()};async function N(e){await _e(r);let t=await e.req.json(),o=G.getInstance(),n=D(t),s=o.logRequest(t,n),[c,i]=await Promise.all([Promise.resolve(n==="anthropic"?he(t):t),s]);c.model&&(c.model=me(c.model)),r.manualApprove&&await ge();try{let m=await ve(c);if(typeof m=="object"&&m.choices){let[f]=await Promise.all([Promise.resolve(n==="anthropic"?ye(m,t.model):m),n==="anthropic"?Promise.resolve().then(()=>{e.header("anthropic-version","2023-06-01"),e.header("anthropic-beta","messages-2023-12-15"),e.header("request-id",i)}):Promise.resolve()]);return o.logResponse(i,f,n).catch(console.error),e.json(f)}let[u]=await Promise.all([Promise.resolve(n==="anthropic"?be(m,t.model):m),n==="anthropic"?Promise.resolve().then(()=>{e.header("anthropic-version","2023-06-01"),e.header("anthropic-beta","messages-2023-12-15"),e.header("request-id",i)}):Promise.resolve()]);return Be(e,async f=>{for await(let _ of u)n==="anthropic"?await f.writeSSE({event:_.event,data:_.data}):await f.writeSSE({data:_.data})})}catch(m){throw o.logError(i,m,n).catch(console.error),m}}var L=new Fe;L.post("/",async e=>await N(e).catch(t=>T(e,t)));import{Hono as Je}from"hono";var ke=async e=>{if(!r.copilotToken)throw new Error("Copilot token not found");let t=await fetch(`${v(r)}/embeddings`,{method:"POST",headers:k(r),body:JSON.stringify(e),agent:w()});if(!t.ok)throw new a("Failed to create embeddings",t);return await t.json()};var U=new Je;U.post("/",async e=>{try{let t=await e.req.json(),o=await ke(t);return e.json(o)}catch(t){return await T(e,t)}});import{Hono as Ve}from"hono";var F=new Ve;F.post("/",N);import{Hono as We}from"hono";var j=new We;j.get("/",async e=>{try{let t=await O(),o={...t,data:t.data.map(n=>{let s={...n};return s.id=le(n.id),s})};return e.json(o)}catch(t){return await T(e,t)}});var l=new Ke;l.use(Ye());l.use(ze());l.get("/",e=>e.text("Server running"));l.route("/chat/completions",L);l.route("/models",j);l.route("/embeddings",U);l.route("/v1/chat/completions",L);l.route("/v1/models",j);l.route("/v1/embeddings",U);l.route("/v1/messages",F);async function Xe(e){e.verbose&&(xe.level=5),e.business?r.accountType="business":e.enterprise&&(r.accountType="enterprise"),r.manualApprove=e.manual,r.rateLimitSeconds=e.rateLimit,r.rateLimitWait=e.rateLimitWait,await P(),await fe(),e.githubToken?r.githubToken=e.githubToken:await I(),await ie(),await ue();let t=`http://localhost:${e.port}`;xe.box(`Server started at ${t}`),Bun.serve({fetch:l.fetch,port:e.port,idleTimeout:255,development:!1})}var Ze=Te({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"},enterprise:{type:"boolean",default:!1,description:"Use an enterprise 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 (must be generated using the `auth` subcommand)"}},run({args:e}){let t=e["rate-limit"],o=t===void 0?void 0:Number.parseInt(t,10),n=Number.parseInt(e.port,10);return Xe({port:n,verbose:e.verbose,business:e.business,enterprise:e.enterprise,manual:e.manual,rateLimit:o,rateLimitWait:!!e.wait,githubToken:e["github-token"]})}}),et=Te({meta:{name:"copilot-api",description:"A wrapper around GitHub Copilot API to make it OpenAI compatible, making it usable for other tools."},subCommands:{auth:ce,start:Ze}});await Qe(et);export{Xe as runServer};