copilot-api
Version:
Turn GitHub Copilot into OpenAI/Anthropic API compatible server. Usable with Claude Code!
1,244 lines (1,204 loc) • 38.3 kB
JavaScript
import { defineCommand, runMain } from "citty";
import consola from "consola";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { randomUUID } from "node:crypto";
import clipboard from "clipboardy";
import { serve } from "srvx";
import invariant from "tiny-invariant";
import { execSync } from "node:child_process";
import process$1 from "node:process";
import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { streamSSE } from "hono/streaming";
import { countTokens } from "gpt-tokenizer/model/gpt-4o";
import { events } from "fetch-event-stream";
//#region src/lib/paths.ts
const APP_DIR = path.join(os.homedir(), ".local", "share", "copilot-api");
const GITHUB_TOKEN_PATH = path.join(APP_DIR, "github_token");
const PATHS = {
APP_DIR,
GITHUB_TOKEN_PATH
};
async function ensurePaths() {
await fs.mkdir(PATHS.APP_DIR, { recursive: true });
await ensureFile(PATHS.GITHUB_TOKEN_PATH);
}
async function ensureFile(filePath) {
try {
await fs.access(filePath, fs.constants.W_OK);
} catch {
await fs.writeFile(filePath, "");
await fs.chmod(filePath, 384);
}
}
//#endregion
//#region src/lib/state.ts
const state = {
accountType: "individual",
manualApprove: false,
rateLimitWait: false,
showToken: false
};
//#endregion
//#region src/lib/api-config.ts
const standardHeaders = () => ({
"content-type": "application/json",
accept: "application/json"
});
const COPILOT_VERSION = "0.26.7";
const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`;
const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`;
const API_VERSION = "2025-04-01";
const copilotBaseUrl = (state$1) => state$1.accountType === "individual" ? "https://api.githubcopilot.com" : `https://api.${state$1.accountType}.githubcopilot.com`;
const copilotHeaders = (state$1, vision = false) => {
const headers = {
Authorization: `Bearer ${state$1.copilotToken}`,
"content-type": standardHeaders()["content-type"],
"copilot-integration-id": "vscode-chat",
"editor-version": `vscode/${state$1.vsCodeVersion}`,
"editor-plugin-version": EDITOR_PLUGIN_VERSION,
"user-agent": USER_AGENT,
"openai-intent": "conversation-panel",
"x-github-api-version": API_VERSION,
"x-request-id": randomUUID(),
"x-vscode-user-agent-library-version": "electron-fetch"
};
if (vision) headers["copilot-vision-request"] = "true";
return headers;
};
const GITHUB_API_BASE_URL = "https://api.github.com";
const githubHeaders = (state$1) => ({
...standardHeaders(),
authorization: `token ${state$1.githubToken}`,
"editor-version": `vscode/${state$1.vsCodeVersion}`,
"editor-plugin-version": EDITOR_PLUGIN_VERSION,
"user-agent": USER_AGENT,
"x-github-api-version": API_VERSION,
"x-vscode-user-agent-library-version": "electron-fetch"
});
const GITHUB_BASE_URL = "https://github.com";
const GITHUB_CLIENT_ID = "Iv1.b507a08c87ecfe98";
const GITHUB_APP_SCOPES = ["read:user"].join(" ");
//#endregion
//#region src/lib/error.ts
var HTTPError = class extends Error {
response;
constructor(message, response) {
super(message);
this.response = response;
}
};
async function forwardError(c, error) {
consola.error("Error occurred:", error);
if (error instanceof HTTPError) {
const errorText = await error.response.text();
let errorJson;
try {
errorJson = JSON.parse(errorText);
} catch {
errorJson = errorText;
}
consola.error("HTTP error:", errorJson);
return c.json({ error: {
message: errorText,
type: "error"
} }, error.response.status);
}
return c.json({ error: {
message: error.message,
type: "error"
} }, 500);
}
//#endregion
//#region src/services/github/get-copilot-token.ts
const getCopilotToken = async () => {
const response = await fetch(`${GITHUB_API_BASE_URL}/copilot_internal/v2/token`, { headers: githubHeaders(state) });
if (!response.ok) throw new HTTPError("Failed to get Copilot token", response);
return await response.json();
};
//#endregion
//#region src/services/github/get-device-code.ts
async function getDeviceCode() {
const response = await fetch(`${GITHUB_BASE_URL}/login/device/code`, {
method: "POST",
headers: standardHeaders(),
body: JSON.stringify({
client_id: GITHUB_CLIENT_ID,
scope: GITHUB_APP_SCOPES
})
});
if (!response.ok) throw new HTTPError("Failed to get device code", response);
return await response.json();
}
//#endregion
//#region src/services/github/get-user.ts
async function getGitHubUser() {
const response = await fetch(`${GITHUB_API_BASE_URL}/user`, { headers: {
authorization: `token ${state.githubToken}`,
...standardHeaders()
} });
if (!response.ok) throw new HTTPError("Failed to get GitHub user", response);
return await response.json();
}
//#endregion
//#region src/services/copilot/get-models.ts
const getModels = async () => {
const response = await fetch(`${copilotBaseUrl(state)}/models`, { headers: copilotHeaders(state) });
if (!response.ok) throw new HTTPError("Failed to get models", response);
return await response.json();
};
//#endregion
//#region src/services/get-vscode-version.ts
const FALLBACK = "1.98.1";
async function getVSCodeVersion() {
const controller = new AbortController();
const timeout = setTimeout(() => {
controller.abort();
}, 5e3);
try {
const response = await fetch("https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=visual-studio-code-bin", { signal: controller.signal });
const pkgbuild = await response.text();
const pkgverRegex = /pkgver=([0-9.]+)/;
const match = pkgbuild.match(pkgverRegex);
if (match) return match[1];
return FALLBACK;
} catch {
return FALLBACK;
} finally {
clearTimeout(timeout);
}
}
await getVSCodeVersion();
//#endregion
//#region src/lib/utils.ts
const sleep = (ms) => new Promise((resolve) => {
setTimeout(resolve, ms);
});
const isNullish = (value) => value === null || value === void 0;
async function cacheModels() {
const models = await getModels();
state.models = models;
}
const cacheVSCodeVersion = async () => {
const response = await getVSCodeVersion();
state.vsCodeVersion = response;
consola.info(`Using VSCode version: ${response}`);
};
//#endregion
//#region src/services/github/poll-access-token.ts
async function pollAccessToken(deviceCode) {
const sleepDuration = (deviceCode.interval + 1) * 1e3;
consola.debug(`Polling access token with interval of ${sleepDuration}ms`);
while (true) {
const response = await fetch(`${GITHUB_BASE_URL}/login/oauth/access_token`, {
method: "POST",
headers: standardHeaders(),
body: JSON.stringify({
client_id: GITHUB_CLIENT_ID,
device_code: deviceCode.device_code,
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
})
});
if (!response.ok) {
await sleep(sleepDuration);
consola.error("Failed to poll access token:", await response.text());
continue;
}
const json = await response.json();
consola.debug("Polling access token response:", json);
const { access_token } = json;
if (access_token) return access_token;
else await sleep(sleepDuration);
}
}
//#endregion
//#region src/lib/token.ts
const readGithubToken = () => fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8");
const writeGithubToken = (token) => fs.writeFile(PATHS.GITHUB_TOKEN_PATH, token);
const setupCopilotToken = async () => {
const { token, refresh_in } = await getCopilotToken();
state.copilotToken = token;
consola.debug("GitHub Copilot Token fetched successfully!");
if (state.showToken) consola.info("Copilot token:", token);
const refreshInterval = (refresh_in - 60) * 1e3;
setInterval(async () => {
consola.debug("Refreshing Copilot token");
try {
const { token: token$1 } = await getCopilotToken();
state.copilotToken = token$1;
consola.debug("Copilot token refreshed");
if (state.showToken) consola.info("Refreshed Copilot token:", token$1);
} catch (error) {
consola.error("Failed to refresh Copilot token:", error);
throw error;
}
}, refreshInterval);
};
async function setupGitHubToken(options) {
try {
const githubToken = await readGithubToken();
if (githubToken && !options?.force) {
state.githubToken = githubToken;
if (state.showToken) consola.info("GitHub token:", githubToken);
await logUser();
return;
}
consola.info("Not logged in, getting new access token");
const response = await getDeviceCode();
consola.debug("Device code response:", response);
consola.info(`Please enter the code "${response.user_code}" in ${response.verification_uri}`);
const token = await pollAccessToken(response);
await writeGithubToken(token);
state.githubToken = token;
if (state.showToken) consola.info("GitHub token:", token);
await logUser();
} catch (error) {
if (error instanceof HTTPError) {
consola.error("Failed to get GitHub token:", await error.response.json());
throw error;
}
consola.error("Failed to get GitHub token:", error);
throw error;
}
}
async function logUser() {
const user = await getGitHubUser();
consola.info(`Logged in as ${user.login}`);
}
//#endregion
//#region src/auth.ts
async function runAuth(options) {
if (options.verbose) {
consola.level = 5;
consola.info("Verbose logging enabled");
}
state.showToken = options.showToken;
await ensurePaths();
await setupGitHubToken({ force: true });
consola.success("GitHub token written to", PATHS.GITHUB_TOKEN_PATH);
}
const auth = defineCommand({
meta: {
name: "auth",
description: "Run GitHub auth flow without running the server"
},
args: {
verbose: {
alias: "v",
type: "boolean",
default: false,
description: "Enable verbose logging"
},
"show-token": {
type: "boolean",
default: false,
description: "Show GitHub token on auth"
}
},
run({ args }) {
return runAuth({
verbose: args.verbose,
showToken: args["show-token"]
});
}
});
//#endregion
//#region src/services/github/get-copilot-usage.ts
const getCopilotUsage = async () => {
const response = await fetch(`${GITHUB_API_BASE_URL}/copilot_internal/user`, { headers: githubHeaders(state) });
if (!response.ok) throw new HTTPError("Failed to get Copilot usage", response);
return await response.json();
};
//#endregion
//#region src/check-usage.ts
const checkUsage = defineCommand({
meta: {
name: "check-usage",
description: "Show current GitHub Copilot usage/quota information"
},
async run() {
await ensurePaths();
await setupGitHubToken();
try {
const usage = await getCopilotUsage();
const premium = usage.quota_snapshots.premium_interactions;
const premiumTotal = premium.entitlement;
const premiumUsed = premiumTotal - premium.remaining;
const premiumPercentUsed = premiumTotal > 0 ? premiumUsed / premiumTotal * 100 : 0;
const premiumPercentRemaining = premium.percent_remaining;
function summarizeQuota(name, snap) {
if (!snap) return `${name}: N/A`;
const total = snap.entitlement;
const used = total - snap.remaining;
const percentUsed = total > 0 ? used / total * 100 : 0;
const percentRemaining = snap.percent_remaining;
return `${name}: ${used}/${total} used (${percentUsed.toFixed(1)}% used, ${percentRemaining.toFixed(1)}% remaining)`;
}
const premiumLine = `Premium: ${premiumUsed}/${premiumTotal} used (${premiumPercentUsed.toFixed(1)}% used, ${premiumPercentRemaining.toFixed(1)}% remaining)`;
const chatLine = summarizeQuota("Chat", usage.quota_snapshots.chat);
const completionsLine = summarizeQuota("Completions", usage.quota_snapshots.completions);
consola.box(`Copilot Usage (plan: ${usage.copilot_plan})\nQuota resets: ${usage.quota_reset_date}\n\nQuotas:\n ${premiumLine}\n ${chatLine}\n ${completionsLine}`);
} catch (err) {
consola.error("Failed to fetch Copilot usage:", err);
process.exit(1);
}
}
});
//#endregion
//#region src/debug.ts
async function getPackageVersion() {
try {
const packageJsonPath = new URL("../package.json", import.meta.url).pathname;
const packageJson = JSON.parse(await fs.readFile(packageJsonPath));
return packageJson.version;
} catch {
return "unknown";
}
}
function getRuntimeInfo() {
const isBun = typeof Bun !== "undefined";
return {
name: isBun ? "bun" : "node",
version: isBun ? Bun.version : process.version.slice(1),
platform: os.platform(),
arch: os.arch()
};
}
async function checkTokenExists() {
try {
const stats = await fs.stat(PATHS.GITHUB_TOKEN_PATH);
if (!stats.isFile()) return false;
const content = await fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8");
return content.trim().length > 0;
} catch {
return false;
}
}
async function getDebugInfo() {
const [version, tokenExists] = await Promise.all([getPackageVersion(), checkTokenExists()]);
return {
version,
runtime: getRuntimeInfo(),
paths: {
APP_DIR: PATHS.APP_DIR,
GITHUB_TOKEN_PATH: PATHS.GITHUB_TOKEN_PATH
},
tokenExists
};
}
function printDebugInfoPlain(info) {
consola.info(`copilot-api debug
Version: ${info.version}
Runtime: ${info.runtime.name} ${info.runtime.version} (${info.runtime.platform} ${info.runtime.arch})
Paths:
- APP_DIR: ${info.paths.APP_DIR}
- GITHUB_TOKEN_PATH: ${info.paths.GITHUB_TOKEN_PATH}
Token exists: ${info.tokenExists ? "Yes" : "No"}`);
}
function printDebugInfoJson(info) {
console.log(JSON.stringify(info, null, 2));
}
async function runDebug(options) {
const debugInfo = await getDebugInfo();
if (options.json) printDebugInfoJson(debugInfo);
else printDebugInfoPlain(debugInfo);
}
const debug = defineCommand({
meta: {
name: "debug",
description: "Print debug information about the application"
},
args: { json: {
type: "boolean",
default: false,
description: "Output debug information as JSON"
} },
run({ args }) {
return runDebug({ json: args.json });
}
});
//#endregion
//#region src/lib/shell.ts
function getShell() {
const { platform, ppid, env } = process$1;
if (platform === "win32") {
try {
const command = `wmic process get ParentProcessId,Name | findstr "${ppid}"`;
const parentProcess = execSync(command, { stdio: "pipe" }).toString();
if (parentProcess.toLowerCase().includes("powershell.exe")) return "powershell";
} catch {
return "cmd";
}
return "cmd";
} else {
const shellPath = env.SHELL;
if (shellPath) {
if (shellPath.endsWith("zsh")) return "zsh";
if (shellPath.endsWith("fish")) return "fish";
if (shellPath.endsWith("bash")) return "bash";
}
return "sh";
}
}
/**
* Generates a copy-pasteable script to set multiple environment variables
* and run a subsequent command.
* @param {EnvVars} envVars - An object of environment variables to set.
* @param {string} commandToRun - The command to run after setting the variables.
* @returns {string} The formatted script string.
*/
function generateEnvScript(envVars, commandToRun = "") {
const shell = getShell();
const filteredEnvVars = Object.entries(envVars).filter(([, value]) => value !== void 0);
let commandBlock;
switch (shell) {
case "powershell":
commandBlock = filteredEnvVars.map(([key, value]) => `$env:${key} = ${value}`).join("; ");
break;
case "cmd":
commandBlock = filteredEnvVars.map(([key, value]) => `set ${key}=${value}`).join(" & ");
break;
case "fish":
commandBlock = filteredEnvVars.map(([key, value]) => `set -gx ${key} ${value}`).join("; ");
break;
default: {
const assignments = filteredEnvVars.map(([key, value]) => `${key}=${value}`).join(" ");
commandBlock = filteredEnvVars.length > 0 ? `export ${assignments}` : "";
break;
}
}
if (commandBlock && commandToRun) {
const separator = shell === "cmd" ? " & " : " && ";
return `${commandBlock}${separator}${commandToRun}`;
}
return commandBlock || commandToRun;
}
//#endregion
//#region src/lib/approval.ts
const awaitApproval = async () => {
const response = await consola.prompt(`Accept incoming request?`, { type: "confirm" });
if (!response) throw new HTTPError("Request rejected", Response.json({ message: "Request rejected" }, { status: 403 }));
};
//#endregion
//#region src/lib/rate-limit.ts
async function checkRateLimit(state$1) {
if (state$1.rateLimitSeconds === void 0) return;
const now = Date.now();
if (!state$1.lastRequestTimestamp) {
state$1.lastRequestTimestamp = now;
return;
}
const elapsedSeconds = (now - state$1.lastRequestTimestamp) / 1e3;
if (elapsedSeconds > state$1.rateLimitSeconds) {
state$1.lastRequestTimestamp = now;
return;
}
const waitTimeSeconds = Math.ceil(state$1.rateLimitSeconds - elapsedSeconds);
if (!state$1.rateLimitWait) {
consola.warn(`Rate limit exceeded. Need to wait ${waitTimeSeconds} more seconds.`);
throw new HTTPError("Rate limit exceeded", Response.json({ message: "Rate limit exceeded" }, { status: 429 }));
}
const waitTimeMs = waitTimeSeconds * 1e3;
consola.warn(`Rate limit reached. Waiting ${waitTimeSeconds} seconds before proceeding...`);
await sleep(waitTimeMs);
state$1.lastRequestTimestamp = now;
consola.info("Rate limit wait completed, proceeding with request");
}
//#endregion
//#region src/lib/tokenizer.ts
const getTokenCount = (messages) => {
const simplifiedMessages = messages.map((message) => {
let content = "";
if (typeof message.content === "string") content = message.content;
else if (Array.isArray(message.content)) content = message.content.filter((part) => part.type === "text").map((part) => part.text).join("");
return {
...message,
content
};
});
let inputMessages = simplifiedMessages.filter((message) => {
return message.role !== "tool";
});
let outputMessages = [];
const lastMessage = simplifiedMessages.at(-1);
if (lastMessage?.role === "assistant") {
inputMessages = simplifiedMessages.slice(0, -1);
outputMessages = [lastMessage];
}
const inputTokens = countTokens(inputMessages);
const outputTokens = countTokens(outputMessages);
return {
input: inputTokens,
output: outputTokens
};
};
//#endregion
//#region src/services/copilot/create-chat-completions.ts
const createChatCompletions = async (payload) => {
if (!state.copilotToken) throw new Error("Copilot token not found");
const enableVision = payload.messages.some((x) => typeof x.content !== "string" && x.content?.some((x$1) => x$1.type === "image_url"));
const isAgentCall = payload.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
const headers = {
...copilotHeaders(state, enableVision),
"X-Initiator": isAgentCall ? "agent" : "user"
};
const response = await fetch(`${copilotBaseUrl(state)}/chat/completions`, {
method: "POST",
headers,
body: JSON.stringify(payload)
});
if (!response.ok) {
consola.error("Failed to create chat completions", response);
throw new HTTPError("Failed to create chat completions", response);
}
if (payload.stream) return events(response);
return await response.json();
};
//#endregion
//#region src/routes/chat-completions/handler.ts
async function handleCompletion$1(c) {
await checkRateLimit(state);
let payload = await c.req.json();
consola.debug("Request payload:", JSON.stringify(payload).slice(-400));
consola.info("Current token count:", getTokenCount(payload.messages));
if (state.manualApprove) await awaitApproval();
if (isNullish(payload.max_tokens)) {
const selectedModel = state.models?.data.find((model) => model.id === payload.model);
payload = {
...payload,
max_tokens: selectedModel?.capabilities.limits.max_output_tokens
};
consola.debug("Set max_tokens to:", JSON.stringify(payload.max_tokens));
}
const response = await createChatCompletions(payload);
if (isNonStreaming$1(response)) {
consola.debug("Non-streaming response:", JSON.stringify(response));
return c.json(response);
}
consola.debug("Streaming response");
return streamSSE(c, async (stream) => {
for await (const chunk of response) {
consola.debug("Streaming chunk:", JSON.stringify(chunk));
await stream.writeSSE(chunk);
}
});
}
const isNonStreaming$1 = (response) => Object.hasOwn(response, "choices");
//#endregion
//#region src/routes/chat-completions/route.ts
const completionRoutes = new Hono();
completionRoutes.post("/", async (c) => {
try {
return await handleCompletion$1(c);
} catch (error) {
return await forwardError(c, error);
}
});
//#endregion
//#region src/services/copilot/create-embeddings.ts
const createEmbeddings = async (payload) => {
if (!state.copilotToken) throw new Error("Copilot token not found");
const response = await fetch(`${copilotBaseUrl(state)}/embeddings`, {
method: "POST",
headers: copilotHeaders(state),
body: JSON.stringify(payload)
});
if (!response.ok) throw new HTTPError("Failed to create embeddings", response);
return await response.json();
};
//#endregion
//#region src/routes/embeddings/route.ts
const embeddingRoutes = new Hono();
embeddingRoutes.post("/", async (c) => {
try {
const paylod = await c.req.json();
const response = await createEmbeddings(paylod);
return c.json(response);
} catch (error) {
return await forwardError(c, error);
}
});
//#endregion
//#region src/routes/messages/utils.ts
function mapOpenAIStopReasonToAnthropic(finishReason) {
if (finishReason === null) return null;
const stopReasonMap = {
stop: "end_turn",
length: "max_tokens",
tool_calls: "tool_use",
content_filter: "end_turn"
};
return stopReasonMap[finishReason];
}
//#endregion
//#region src/routes/messages/non-stream-translation.ts
function translateToOpenAI(payload) {
return {
model: translateModelName(payload.model),
messages: translateAnthropicMessagesToOpenAI(payload.messages, payload.system),
max_tokens: payload.max_tokens,
stop: payload.stop_sequences,
stream: payload.stream,
temperature: payload.temperature,
top_p: payload.top_p,
user: payload.metadata?.user_id,
tools: translateAnthropicToolsToOpenAI(payload.tools),
tool_choice: translateAnthropicToolChoiceToOpenAI(payload.tool_choice)
};
}
function translateModelName(model) {
if (model.startsWith("claude-sonnet-4-")) return model.replace(/^claude-sonnet-4-.*/, "claude-sonnet-4");
else if (model.startsWith("claude-opus-")) return model.replace(/^claude-opus-4-.*/, "claude-opus-4");
return model;
}
function translateAnthropicMessagesToOpenAI(anthropicMessages, system) {
const systemMessages = handleSystemPrompt(system);
const otherMessages = anthropicMessages.flatMap((message) => message.role === "user" ? handleUserMessage(message) : handleAssistantMessage(message));
return [...systemMessages, ...otherMessages];
}
function handleSystemPrompt(system) {
if (!system) return [];
if (typeof system === "string") return [{
role: "system",
content: system
}];
else {
const systemText = system.map((block) => block.text).join("\n\n");
return [{
role: "system",
content: systemText
}];
}
}
function handleUserMessage(message) {
const newMessages = [];
if (Array.isArray(message.content)) {
const toolResultBlocks = message.content.filter((block) => block.type === "tool_result");
const otherBlocks = message.content.filter((block) => block.type !== "tool_result");
for (const block of toolResultBlocks) newMessages.push({
role: "tool",
tool_call_id: block.tool_use_id,
content: mapContent(block.content)
});
if (otherBlocks.length > 0) newMessages.push({
role: "user",
content: mapContent(otherBlocks)
});
} else newMessages.push({
role: "user",
content: mapContent(message.content)
});
return newMessages;
}
function handleAssistantMessage(message) {
if (!Array.isArray(message.content)) return [{
role: "assistant",
content: mapContent(message.content)
}];
const toolUseBlocks = message.content.filter((block) => block.type === "tool_use");
const textBlocks = message.content.filter((block) => block.type === "text");
const thinkingBlocks = message.content.filter((block) => block.type === "thinking");
const allTextContent = [...textBlocks.map((b) => b.text), ...thinkingBlocks.map((b) => b.thinking)].join("\n\n");
return toolUseBlocks.length > 0 ? [{
role: "assistant",
content: allTextContent || null,
tool_calls: toolUseBlocks.map((toolUse) => ({
id: toolUse.id,
type: "function",
function: {
name: toolUse.name,
arguments: JSON.stringify(toolUse.input)
}
}))
}] : [{
role: "assistant",
content: mapContent(message.content)
}];
}
function mapContent(content) {
if (typeof content === "string") return content;
if (!Array.isArray(content)) return null;
const hasImage = content.some((block) => block.type === "image");
if (!hasImage) return content.filter((block) => block.type === "text" || block.type === "thinking").map((block) => block.type === "text" ? block.text : block.thinking).join("\n\n");
const contentParts = [];
for (const block of content) switch (block.type) {
case "text":
contentParts.push({
type: "text",
text: block.text
});
break;
case "thinking":
contentParts.push({
type: "text",
text: block.thinking
});
break;
case "image":
contentParts.push({
type: "image_url",
image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` }
});
break;
}
return contentParts;
}
function translateAnthropicToolsToOpenAI(anthropicTools) {
if (!anthropicTools) return void 0;
return anthropicTools.map((tool) => ({
type: "function",
function: {
name: tool.name,
description: tool.description,
parameters: tool.input_schema
}
}));
}
function translateAnthropicToolChoiceToOpenAI(anthropicToolChoice) {
if (!anthropicToolChoice) return void 0;
switch (anthropicToolChoice.type) {
case "auto": return "auto";
case "any": return "required";
case "tool":
if (anthropicToolChoice.name) return {
type: "function",
function: { name: anthropicToolChoice.name }
};
return void 0;
case "none": return "none";
default: return void 0;
}
}
function translateToAnthropic(response) {
const allTextBlocks = [];
const allToolUseBlocks = [];
let stopReason = null;
stopReason = response.choices[0]?.finish_reason ?? stopReason;
for (const choice of response.choices) {
const textBlocks = getAnthropicTextBlocks(choice.message.content);
const toolUseBlocks = getAnthropicToolUseBlocks(choice.message.tool_calls);
allTextBlocks.push(...textBlocks);
allToolUseBlocks.push(...toolUseBlocks);
if (choice.finish_reason === "tool_calls" || stopReason === "stop") stopReason = choice.finish_reason;
}
return {
id: response.id,
type: "message",
role: "assistant",
model: response.model,
content: [...allTextBlocks, ...allToolUseBlocks],
stop_reason: mapOpenAIStopReasonToAnthropic(stopReason),
stop_sequence: null,
usage: {
input_tokens: response.usage?.prompt_tokens ?? 0,
output_tokens: response.usage?.completion_tokens ?? 0
}
};
}
function getAnthropicTextBlocks(messageContent) {
if (typeof messageContent === "string") return [{
type: "text",
text: messageContent
}];
if (Array.isArray(messageContent)) return messageContent.filter((part) => part.type === "text").map((part) => ({
type: "text",
text: part.text
}));
return [];
}
function getAnthropicToolUseBlocks(toolCalls) {
if (!toolCalls) return [];
return toolCalls.map((toolCall) => ({
type: "tool_use",
id: toolCall.id,
name: toolCall.function.name,
input: JSON.parse(toolCall.function.arguments)
}));
}
//#endregion
//#region src/routes/messages/stream-translation.ts
function isToolBlockOpen(state$1) {
if (!state$1.contentBlockOpen) return false;
return Object.values(state$1.toolCalls).some((tc) => tc.anthropicBlockIndex === state$1.contentBlockIndex);
}
function translateChunkToAnthropicEvents(chunk, state$1) {
const events$1 = [];
if (chunk.choices.length === 0) return events$1;
const choice = chunk.choices[0];
const { delta } = choice;
if (!state$1.messageStartSent) {
events$1.push({
type: "message_start",
message: {
id: chunk.id,
type: "message",
role: "assistant",
content: [],
model: chunk.model,
stop_reason: null,
stop_sequence: null,
usage: {
input_tokens: chunk.usage?.prompt_tokens ?? 0,
output_tokens: 0
}
}
});
state$1.messageStartSent = true;
}
if (delta.content) {
if (isToolBlockOpen(state$1)) {
events$1.push({
type: "content_block_stop",
index: state$1.contentBlockIndex
});
state$1.contentBlockIndex++;
state$1.contentBlockOpen = false;
}
if (!state$1.contentBlockOpen) {
events$1.push({
type: "content_block_start",
index: state$1.contentBlockIndex,
content_block: {
type: "text",
text: ""
}
});
state$1.contentBlockOpen = true;
}
events$1.push({
type: "content_block_delta",
index: state$1.contentBlockIndex,
delta: {
type: "text_delta",
text: delta.content
}
});
}
if (delta.tool_calls) for (const toolCall of delta.tool_calls) {
if (toolCall.id && toolCall.function?.name) {
if (state$1.contentBlockOpen) {
events$1.push({
type: "content_block_stop",
index: state$1.contentBlockIndex
});
state$1.contentBlockIndex++;
state$1.contentBlockOpen = false;
}
const anthropicBlockIndex = state$1.contentBlockIndex;
state$1.toolCalls[toolCall.index] = {
id: toolCall.id,
name: toolCall.function.name,
anthropicBlockIndex
};
events$1.push({
type: "content_block_start",
index: anthropicBlockIndex,
content_block: {
type: "tool_use",
id: toolCall.id,
name: toolCall.function.name,
input: {}
}
});
state$1.contentBlockOpen = true;
}
if (toolCall.function?.arguments) {
const toolCallInfo = state$1.toolCalls[toolCall.index];
if (toolCallInfo) events$1.push({
type: "content_block_delta",
index: toolCallInfo.anthropicBlockIndex,
delta: {
type: "input_json_delta",
partial_json: toolCall.function.arguments
}
});
}
}
if (choice.finish_reason) {
if (state$1.contentBlockOpen) {
events$1.push({
type: "content_block_stop",
index: state$1.contentBlockIndex
});
state$1.contentBlockOpen = false;
}
events$1.push({
type: "message_delta",
delta: {
stop_reason: mapOpenAIStopReasonToAnthropic(choice.finish_reason),
stop_sequence: null
},
usage: {
input_tokens: chunk.usage?.prompt_tokens ?? 0,
output_tokens: chunk.usage?.completion_tokens ?? 0,
...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
}
}, { type: "message_stop" });
}
return events$1;
}
//#endregion
//#region src/routes/messages/handler.ts
async function handleCompletion(c) {
await checkRateLimit(state);
const anthropicPayload = await c.req.json();
consola.debug("Anthropic request payload:", JSON.stringify(anthropicPayload));
const openAIPayload = translateToOpenAI(anthropicPayload);
consola.debug("Translated OpenAI request payload:", JSON.stringify(openAIPayload));
if (state.manualApprove) await awaitApproval();
const response = await createChatCompletions(openAIPayload);
if (isNonStreaming(response)) {
consola.debug("Non-streaming response from Copilot:", JSON.stringify(response).slice(-400));
const anthropicResponse = translateToAnthropic(response);
consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
return c.json(anthropicResponse);
}
consola.debug("Streaming response from Copilot");
return streamSSE(c, async (stream) => {
const streamState = {
messageStartSent: false,
contentBlockIndex: 0,
contentBlockOpen: false,
toolCalls: {}
};
for await (const rawEvent of response) {
consola.debug("Copilot raw stream event:", JSON.stringify(rawEvent));
if (rawEvent.data === "[DONE]") break;
if (!rawEvent.data) continue;
const chunk = JSON.parse(rawEvent.data);
const events$1 = translateChunkToAnthropicEvents(chunk, streamState);
for (const event of events$1) {
consola.debug("Translated Anthropic event:", JSON.stringify(event));
await stream.writeSSE({
event: event.type,
data: JSON.stringify(event)
});
}
}
});
}
const isNonStreaming = (response) => Object.hasOwn(response, "choices");
//#endregion
//#region src/routes/messages/route.ts
const messageRoutes = new Hono();
messageRoutes.post("/", async (c) => {
try {
return await handleCompletion(c);
} catch (error) {
return await forwardError(c, error);
}
});
//#endregion
//#region src/routes/models/route.ts
const modelRoutes = new Hono();
modelRoutes.get("/", async (c) => {
try {
if (!state.models) await cacheModels();
const models = state.models?.data.map((model) => ({
id: model.id,
object: "model",
type: "model",
created: 0,
created_at: (/* @__PURE__ */ new Date(0)).toISOString(),
owned_by: model.vendor,
display_name: model.name
}));
return c.json({
object: "list",
data: models,
has_more: false
});
} catch (error) {
return await forwardError(c, error);
}
});
//#endregion
//#region src/routes/token/route.ts
const tokenRoute = new Hono();
tokenRoute.get("/", (c) => {
try {
return c.json({ token: state.copilotToken });
} catch (error) {
console.error("Error fetching token:", error);
return c.json({
error: "Failed to fetch token",
token: null
}, 500);
}
});
//#endregion
//#region src/routes/usage/route.ts
const usageRoute = new Hono();
usageRoute.get("/", async (c) => {
try {
const usage = await getCopilotUsage();
return c.json(usage);
} catch (error) {
console.error("Error fetching Copilot usage:", error);
return c.json({ error: "Failed to fetch Copilot usage" }, 500);
}
});
//#endregion
//#region src/server.ts
const server = new Hono();
server.use(logger());
server.use(cors());
server.get("/", (c) => c.text("Server running"));
server.route("/chat/completions", completionRoutes);
server.route("/models", modelRoutes);
server.route("/embeddings", embeddingRoutes);
server.route("/usage", usageRoute);
server.route("/token", tokenRoute);
server.route("/v1/chat/completions", completionRoutes);
server.route("/v1/models", modelRoutes);
server.route("/v1/embeddings", embeddingRoutes);
server.route("/v1/messages", messageRoutes);
server.post("/v1/messages/count_tokens", (c) => c.json({ input_tokens: 1 }));
//#endregion
//#region src/start.ts
async function runServer(options) {
if (options.verbose) {
consola.level = 5;
consola.info("Verbose logging enabled");
}
state.accountType = options.accountType;
if (options.accountType !== "individual") consola.info(`Using ${options.accountType} plan GitHub account`);
state.manualApprove = options.manual;
state.rateLimitSeconds = options.rateLimit;
state.rateLimitWait = options.rateLimitWait;
state.showToken = options.showToken;
await ensurePaths();
await cacheVSCodeVersion();
if (options.githubToken) {
state.githubToken = options.githubToken;
consola.info("Using provided GitHub token");
} else await setupGitHubToken();
await setupCopilotToken();
await cacheModels();
consola.info(`Available models: \n${state.models?.data.map((model) => `- ${model.id}`).join("\n")}`);
const serverUrl = `http://localhost:${options.port}`;
if (options.claudeCode) {
invariant(state.models, "Models should be loaded by now");
const selectedModel = await consola.prompt("Select a model to use with Claude Code", {
type: "select",
options: state.models.data.map((model) => model.id)
});
const selectedSmallModel = await consola.prompt("Select a small model to use with Claude Code", {
type: "select",
options: state.models.data.map((model) => model.id)
});
const command = generateEnvScript({
ANTHROPIC_BASE_URL: serverUrl,
ANTHROPIC_AUTH_TOKEN: "dummy",
ANTHROPIC_MODEL: selectedModel,
ANTHROPIC_SMALL_FAST_MODEL: selectedSmallModel
}, "claude");
try {
clipboard.writeSync(command);
consola.success("Copied Claude Code command to clipboard!");
} catch {
consola.warn("Failed to copy to clipboard. Here is the Claude Code command:");
consola.log(command);
}
}
consola.box(`🌐 Usage Viewer: https://ericc-ch.github.io/copilot-api?endpoint=${serverUrl}/usage`);
serve({
fetch: server.fetch,
port: options.port
});
}
const start = defineCommand({
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: false,
description: "Enable verbose logging"
},
"account-type": {
alias: "a",
type: "string",
default: "individual",
description: "Account type to use (individual, business, enterprise)"
},
manual: {
type: "boolean",
default: false,
description: "Enable manual request approval"
},
"rate-limit": {
alias: "r",
type: "string",
description: "Rate limit in seconds between requests"
},
wait: {
alias: "w",
type: "boolean",
default: false,
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)"
},
"claude-code": {
alias: "c",
type: "boolean",
default: false,
description: "Generate a command to launch Claude Code with Copilot API config"
},
"show-token": {
type: "boolean",
default: false,
description: "Show GitHub and Copilot tokens on fetch and refresh"
}
},
run({ args }) {
const rateLimitRaw = args["rate-limit"];
const rateLimit = rateLimitRaw === void 0 ? void 0 : Number.parseInt(rateLimitRaw, 10);
return runServer({
port: Number.parseInt(args.port, 10),
verbose: args.verbose,
accountType: args["account-type"],
manual: args.manual,
rateLimit,
rateLimitWait: args.wait,
githubToken: args["github-token"],
claudeCode: args["claude-code"],
showToken: args["show-token"]
});
}
});
//#endregion
//#region src/main.ts
const main = defineCommand({
meta: {
name: "copilot-api",
description: "A wrapper around GitHub Copilot API to make it OpenAI compatible, making it usable for other tools."
},
subCommands: {
auth,
start,
"check-usage": checkUsage,
debug
}
});
await runMain(main);
//#endregion
export { };
//# sourceMappingURL=main.js.map