Skip to content

Commit 11d58fe

Browse files
authored
feat: Proxy embeddings API (#6)
* refactor: use Depends to inject and verify auth * chore(route): rename proxy to proxy_completions * docs: update README for new /v1/embeddings endpoint and improved OpenAI compatibility task: Added /v1/embeddings endpoint to API and documented usage. Updated README to clarify chat completion and embeddings usage and refactor API sections for OpenAI compatibility. Signed-off-by: Hanchin Hsieh <me@yuchanns.xyz> --------- Signed-off-by: Hanchin Hsieh <me@yuchanns.xyz>
1 parent ab3ae45 commit 11d58fe

File tree

2 files changed

+78
-28
lines changed

2 files changed

+78
-28
lines changed

README.md

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
[![Image Tags](https://ghcr-badge.yuchanns.xyz/yuchanns/copilot-openai-api/tags?ignore=latest)](https://ghcr.io/yuchanns/copilot-openai-api)
88
![Image Size](https://ghcr-badge.yuchanns.xyz/yuchanns/copilot-openai-api/size)
99

10-
A FastAPI proxy server that seamlessly turns GitHub Copilot's chat completion capabilities into OpenAI compatible API service.
10+
A FastAPI proxy server that seamlessly turns GitHub Copilot's chat completion/embeddings capabilities into OpenAI compatible API service.
1111

1212
## ✨ Key Features
1313

@@ -124,19 +124,29 @@ The Docker setup:
124124

125125
Access the chat completion endpoint:
126126
```bash
127-
curl -X POST http://localhost:9191/chat/completions \
127+
curl -X POST http://localhost:9191/v1/chat/completions \
128128
-H "Authorization: Bearer your_access_token_here" \
129129
-H "Content-Type: application/json" \
130130
-d '{
131131
"messages": [{"role": "user", "content": "Hello, Copilot!"}]
132132
}'
133133
```
134134

135-
## 🔌 API Reference
135+
Access the embeddings endpoint:
136+
```bash
137+
curl -X POST http://localhost:9191/v1/embeddings \
138+
-H "Authorization: Bearer your_access_token_here" \
139+
-H "Content-Type: application/json" \
140+
-d '{
141+
"model": "copilot-text-embedding-ada-002",
142+
"input": ["The quick brown fox", "Jumped over the lazy dog"]
143+
}'
144+
```
136145

137-
### POST /chat/completions
146+
## 🔌 API Reference
138147

139-
Proxies requests to GitHub Copilot's API.
148+
### POST /v1/chat/completions
149+
Proxies requests to GitHub Copilot's Completions API.
140150

141151
**Required Headers:**
142152
- `Authorization: Bearer <your_access_token>`
@@ -148,6 +158,44 @@ Proxies requests to GitHub Copilot's API.
148158
**Response:**
149159
- Streams responses directly from GitHub Copilot's API
150160

161+
---
162+
163+
### POST /v1/embeddings
164+
Proxies requests to Github Copilot's Embeddings API.
165+
166+
**Required Headers:**
167+
- `Authorization: Bearer <your_access_token>`
168+
- `Content-Type: application/json`
169+
170+
**Request Body:**
171+
```json
172+
{
173+
"model": "<model_name>",
174+
"input": ["string or array of strings"]
175+
}
176+
```
177+
- `model`: The name of the embedding model (e.g. `copilot-text-embedding-ada-002` or compatible name)
178+
- `input`: Accepts a string or an array of strings
179+
180+
**Response:**
181+
```json
182+
{
183+
"data": [{
184+
"embedding": [...],
185+
"index": 0,
186+
"object": "embedding"
187+
}, {
188+
"embedding": [...],
189+
"index": 1,
190+
"object": "embedding"
191+
}],
192+
"usage": {
193+
"prompt_tokens": 10,
194+
"total_tokens": 10
195+
}
196+
}
197+
```
198+
151199
## 🔒 Authentication
152200

153201
Secure your endpoints:

main.py

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@
66
import time
77

88
from contextlib import asynccontextmanager
9-
from functools import wraps
109
from pathlib import Path
11-
from typing import Any, Dict, Optional
10+
from typing import Annotated, Any, Dict, Optional
1211

1312
import aiofiles
1413
import httpx
1514

16-
from fastapi import FastAPI, HTTPException, Request
15+
from fastapi import Depends, FastAPI, HTTPException, Request, Security, status
1716
from fastapi.middleware.cors import CORSMiddleware
1817
from fastapi.responses import StreamingResponse
18+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
1919
from watchfiles import awatch
2020

2121

@@ -213,7 +213,7 @@ async def cleanup(self):
213213
# Cancel all tasks
214214
for task in self.tasks:
215215
task.cancel()
216-
216+
217217
# Wait for all tasks to complete their cancellation
218218
if self.tasks:
219219
await asyncio.gather(*self.tasks, return_exceptions=True)
@@ -237,7 +237,7 @@ async def check_stale_locks(self):
237237
pass
238238
except Exception as e:
239239
logging.error(f"Error checking stale locks: {e}")
240-
240+
241241
await asyncio.sleep(60) # Check every minute
242242

243243
async def setup_refresh_timer(self):
@@ -322,26 +322,28 @@ async def stream_response():
322322
return {"error": str(e)}
323323

324324

325-
def require_auth(func):
326-
@wraps(func)
327-
async def wrapper(request: Request, *args, **kwargs):
328-
auth_header = request.headers.get("Authorization")
329-
if not auth_header or not auth_header.startswith("Bearer "):
330-
raise HTTPException(
331-
status_code=401, detail="Missing or invalid authorization header"
332-
)
333-
334-
token = auth_header.split(" ")[1]
335-
if token != request.app.state.access_token:
336-
raise HTTPException(status_code=403, detail="Invalid access token")
325+
def verify_auth(
326+
authorization: Annotated[HTTPAuthorizationCredentials, Security(HTTPBearer())],
327+
):
328+
token = authorization.credentials
329+
if token != app.state.access_token:
330+
raise HTTPException(
331+
status_code=status.HTTP_403_FORBIDDEN,
332+
detail="Invalid access token",
333+
)
337334

338-
return await func(request, *args, **kwargs)
339335

340-
return wrapper
336+
@app.post("/chat/completions", dependencies=[Depends(verify_auth)])
337+
async def proxy_completions(request: Request):
338+
target_url = "https://api.githubcopilot.com/chat/completions"
339+
return await proxy_stream(request, target_url)
341340

342341

343-
@app.api_route("/chat/completions", methods=["POST"])
344-
@require_auth
345-
async def proxy(request: Request):
346-
target_url = "https://api.githubcopilot.com/chat/completions"
342+
@app.post("/embeddings", dependencies=[Depends(verify_auth)])
343+
async def proxy_embeddings(request: Request):
344+
target_url = "https://api.githubcopilot.com/embeddings"
347345
return await proxy_stream(request, target_url)
346+
347+
348+
# Mount self to the /v1 path
349+
app.mount("/v1", app)

0 commit comments

Comments
 (0)