Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ A reverse-engineered proxy for the GitHub Copilot API that exposes it as an Open

- **OpenAI & Anthropic Compatibility**: Exposes GitHub Copilot as an OpenAI-compatible (`/v1/chat/completions`, `/v1/models`, `/v1/embeddings`) and Anthropic-compatible (`/v1/messages`) API.
- **Claude Code Integration**: Easily configure and launch [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) to use Copilot as its backend with a simple command-line flag (`--claude-code`).
- **API Key Authentication**: Secure your server with configurable API keys for client authentication, enabling safe exposure to the open web (`--api-keys`).
- **Usage Dashboard**: A web-based dashboard to monitor your Copilot API usage, view quotas, and see detailed statistics.
- **Rate Limit Control**: Manage API usage with rate-limiting options (`--rate-limit`) and a waiting mechanism (`--wait`) to prevent errors from rapid requests.
- **Manual Request Approval**: Manually approve or deny each API request for fine-grained control over usage (`--manual`).
Expand Down Expand Up @@ -162,6 +163,7 @@ The following command line options are available for the `start` command:
| --github-token | Provide GitHub token directly (must be generated using the `auth` subcommand) | none | -g |
| --claude-code | Generate a command to launch Claude Code with Copilot API config | false | -c |
| --show-token | Show GitHub and Copilot tokens on fetch and refresh | false | none |
| --api-keys | Comma-separated list of API keys for client authentication | none | -k |

### Auth Command Options

Expand Down Expand Up @@ -251,6 +253,12 @@ npx copilot-api@latest debug

# Display debug information in JSON format
npx copilot-api@latest debug --json

# Enable API key authentication for secure access
npx copilot-api@latest start --api-keys sk-key1,sk-key2,sk-key3

# Combine API keys with other options
npx copilot-api@latest start --api-keys mykey123 --rate-limit 30 --wait --port 8080
```

## Using the Usage Viewer
Expand Down Expand Up @@ -309,6 +317,19 @@ Here is an example `.claude/settings.json` file:
}
```

If you've configured API keys using the `--api-keys` flag, you'll need to include one of your configured keys as the `ANTHROPIC_AUTH_TOKEN`:

```json
{
"env": {
"ANTHROPIC_BASE_URL": "http://localhost:4141",
"ANTHROPIC_AUTH_TOKEN": "sk-mykey123",
"ANTHROPIC_MODEL": "gpt-4.1",
"ANTHROPIC_SMALL_FAST_MODEL": "gpt-4.1"
}
}
```

You can find more options here: [Claude Code settings](https://docs.anthropic.com/en/docs/claude-code/settings#environment-variables)

You can also read more about IDE integration here: [Add Claude Code to your IDE](https://docs.anthropic.com/en/docs/claude-code/ide-integrations)
Expand All @@ -331,6 +352,13 @@ bun run start

## Usage Tips

- **API Key Authentication**: Secure your API by enabling authentication with the `--api-keys` flag:
- `--api-keys key1,key2,key3`: Provide comma-separated API keys. Clients must include one of these keys in the `Authorization` header (as `Bearer <key>` or just `<key>`).
- When API keys are configured, all API endpoints (`/v1/chat/completions`, `/v1/models`, `/v1/embeddings`, `/v1/messages`) require authentication.
- Public endpoints (`/`, `/usage`, `/token`) remain accessible without authentication.
- If no API keys are configured, the server operates in open mode (backward compatible).
- Example: `npx copilot-api@latest start --api-keys sk-mykey123,sk-anotherkey456`
- Clients can then authenticate using: `Authorization: Bearer sk-mykey123`
- To avoid hitting GitHub Copilot's rate limits, you can use the following flags:
- `--manual`: Enables manual approval for each request, giving you full control over when requests are sent.
- `--rate-limit <seconds>`: Enforces a minimum time interval between requests. For example, `copilot-api start --rate-limit 30` will ensure there's at least a 30-second gap between requests.
Expand Down
45 changes: 45 additions & 0 deletions src/lib/auth-middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { Context, Next } from "hono"

import { state } from "~/lib/state"

export async function authMiddleware(c: Context, next: Next) {
// If no API keys are configured, skip authentication
if (!state.apiKeys || state.apiKeys.length === 0) {
return next()
}

// Get the Authorization header
const authHeader = c.req.header("authorization")

if (!authHeader) {
return c.json(
{
error: {
message: "Missing authorization header",
type: "invalid_request_error",
},
},
401,
)
}

// Support both "Bearer <token>" and just "<token>" formats
const token =
authHeader.startsWith("Bearer ") ? authHeader.slice(7) : authHeader

// Check if the provided token matches any configured API key
if (!state.apiKeys.includes(token)) {
return c.json(
{
error: {
message: "Invalid API key",
type: "invalid_request_error",
},
},
401,
)
}

// Authentication successful, continue to the next handler
return next()
}
4 changes: 4 additions & 0 deletions src/lib/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@ export interface State {
// Rate limiting configuration
rateLimitSeconds?: number
lastRequestTimestamp?: number

// API key authentication
apiKeys?: Array<string>
}

export const state: State = {
accountType: "individual",
manualApprove: false,
rateLimitWait: false,
showToken: false,
apiKeys: undefined,
}
19 changes: 17 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Hono } from "hono"
import { cors } from "hono/cors"
import { logger } from "hono/logger"

import { authMiddleware } from "./lib/auth-middleware"
import { completionRoutes } from "./routes/chat-completions/route"
import { embeddingRoutes } from "./routes/embeddings/route"
import { messageRoutes } from "./routes/messages/route"
Expand All @@ -16,17 +17,31 @@ server.use(cors())

server.get("/", (c) => c.text("Server running"))

// Public endpoints (no authentication required)
server.route("/usage", usageRoute)
server.route("/token", tokenRoute)

// Protected endpoints (authentication required if API keys are configured)
server.use("/chat/completions/*", authMiddleware)
server.route("/chat/completions", completionRoutes)

server.use("/models/*", authMiddleware)
server.route("/models", modelRoutes)

server.use("/embeddings/*", authMiddleware)
server.route("/embeddings", embeddingRoutes)
server.route("/usage", usageRoute)
server.route("/token", tokenRoute)

// Compatibility with tools that expect v1/ prefix
server.use("/v1/chat/completions/*", authMiddleware)
server.route("/v1/chat/completions", completionRoutes)

server.use("/v1/models/*", authMiddleware)
server.route("/v1/models", modelRoutes)

server.use("/v1/embeddings/*", authMiddleware)
server.route("/v1/embeddings", embeddingRoutes)

// Anthropic compatible endpoints
server.use("/v1/messages/*", authMiddleware)
server.route("/v1/messages", messageRoutes)
server.post("/v1/messages/count_tokens", (c) => c.json({ input_tokens: 1 }))
22 changes: 22 additions & 0 deletions src/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface RunServerOptions {
githubToken?: string
claudeCode: boolean
showToken: boolean
apiKeys?: string
}

export async function runServer(options: RunServerOptions): Promise<void> {
Expand All @@ -41,10 +42,24 @@ export async function runServer(options: RunServerOptions): Promise<void> {
state.rateLimitWait = options.rateLimitWait
state.showToken = options.showToken

// Parse and set API keys if provided
if (options.apiKeys) {
state.apiKeys = options.apiKeys
.split(",")
.map((key) => key.trim())
.filter((key) => key.length > 0)
if (state.apiKeys.length > 0) {
consola.info(
`API key authentication enabled with ${state.apiKeys.length} key(s)`,
)
}
}

await ensurePaths()
await cacheVSCodeVersion()

if (options.githubToken) {
// eslint-disable-next-line require-atomic-updates
state.githubToken = options.githubToken
consola.info("Using provided GitHub token")
} else {
Expand Down Expand Up @@ -169,6 +184,12 @@ export const start = defineCommand({
default: false,
description: "Show GitHub and Copilot tokens on fetch and refresh",
},
"api-keys": {
alias: "k",
type: "string",
description:
"Comma-separated list of API keys for client authentication (e.g., 'key1,key2,key3'). If provided, clients must send one of these keys in the Authorization header",
},
},
run({ args }) {
const rateLimitRaw = args["rate-limit"]
Expand All @@ -186,6 +207,7 @@ export const start = defineCommand({
githubToken: args["github-token"],
claudeCode: args["claude-code"],
showToken: args["show-token"],
apiKeys: args["api-keys"],
})
},
})
Loading