Skip to content

Commit 5bc5eed

Browse files
Release v0.0.17: Multi-Provider WebSocket Support
1 parent 4af1ba1 commit 5bc5eed

9 files changed

Lines changed: 412 additions & 28 deletions

File tree

.DS_Store

0 Bytes
Binary file not shown.

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,19 @@ The gateway provides fully compatible endpoints for major AI providers.
129129
| Provider | Endpoint | Method | Description |
130130
|----------|----------|--------|-------------|
131131
| **OpenAI** | `/v1/chat/completions` | `POST` | Full support for streaming, tools, and JSON mode. |
132+
| **OpenAI** | `/v1/realtime` | `WS` | **Realtime API** for low-latency bidirectional chat. |
133+
| **OpenAI** | `/v1/completions` | `POST` | Legacy text completion endpoint. |
132134
| **OpenAI** | `/v1/models` | `GET` | List available models from your Copilot plan. |
133135
| **Anthropic** | `/v1/messages` | `POST` | Compatible with Anthropic SDKs (Claude). |
136+
| **Anthropic** | `/anthropic/v1/realtime` | `WS` | Realtime API using Anthropic Messages format. |
134137
| **Google** | `/v1beta/models/:model:generateContent` | `POST` | Compatible with Google Generative AI SDKs. |
138+
| **Google** | `/google/v1/realtime` | `WS` | Realtime API using Google Gemini format. |
135139
| **Llama** | `/llama/v1/chat/completions` | `POST` | Targeted support for Llama client libraries. |
140+
| **Llama** | `/llama/v1/realtime` | `WS` | Realtime API using Llama format. |
141+
| **Utilities** | `/v1/tokenize` | `POST` | Count tokens for a given string (OpenAI format). |
136142
| **Utilities** | `/v1/usage` | `GET` | Retrieve server usage statistics. |
143+
| **Utilities** | `/metrics` | `GET` | Prometheus-compatible metrics endpoint. |
144+
| **Utilities** | `/health` | `GET` | Service health check & Copilot status. |
137145
| **Utilities** | `/docs` | `GET` | **Offline Swagger UI** for interactive testing. |
138146

139147
### Interactive Documentation (Swagger UI)

dist/extension.js

Lines changed: 126 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55997,19 +55997,30 @@ var CopilotApiGateway = class {
5599755997
if (this.config.enableWebSocket) {
5599855998
const { WebSocketServer: WSServer } = await Promise.resolve().then(() => (init_wrapper(), wrapper_exports));
5599955999
this.wsServer = new WSServer({ noServer: true });
56000-
this.wsServer.on("connection", (socket) => this.handleWebSocketConnection(socket));
5600156000
this.wsServer.on("error", (error2) => {
5600256001
this.logError("WebSocket server error", error2);
5600356002
});
56003+
const wsEndpoints = [
56004+
"/v1/realtime",
56005+
// OpenAI format
56006+
"/anthropic/v1/realtime",
56007+
// Anthropic format
56008+
"/google/v1/realtime",
56009+
// Google format
56010+
"/llama/v1/realtime"
56011+
// Llama format
56012+
];
5600456013
this.httpServer.on("upgrade", (request, socket, head) => {
5600556014
if (!request.url) {
5600656015
socket.destroy();
5600756016
return;
5600856017
}
5600956018
const url2 = this.buildUrl(request.url);
56010-
if (url2.pathname === "/v1/realtime") {
56019+
const endpoint = wsEndpoints.find((ep) => url2.pathname === ep);
56020+
if (endpoint) {
5601156021
this.wsServer?.handleUpgrade(request, socket, head, (ws) => {
56012-
this.wsServer?.emit("connection", ws, request);
56022+
ws._endpoint = endpoint;
56023+
this.handleWebSocketConnection(ws, endpoint);
5601356024
});
5601456025
} else {
5601556026
socket.destroy();
@@ -56462,6 +56473,18 @@ var CopilotApiGateway = class {
5646256473
if (url2.pathname.startsWith("/v1/audio")) {
5646356474
throw new ApiError(501, "Audio processing is not supported by Copilot.", "not_implemented", "audio_not_supported");
5646456475
}
56476+
const wsEndpoints = ["/v1/realtime", "/anthropic/v1/realtime", "/google/v1/realtime", "/llama/v1/realtime"];
56477+
if (req.method === "GET" && wsEndpoints.includes(url2.pathname)) {
56478+
res.writeHead(426, { "Content-Type": "application/json", "Upgrade": "websocket" });
56479+
res.end(JSON.stringify({
56480+
error: {
56481+
message: "This is a WebSocket endpoint. Please use a WebSocket client and Upgrade header.",
56482+
type: "upgrade_required",
56483+
code: "websocket_only"
56484+
}
56485+
}));
56486+
return;
56487+
}
5646556488
throw new ApiError(404, `No route for ${req.method ?? "UNKNOWN"} ${url2.pathname}`, "not_found");
5646656489
}
5646756490
async processStreamingGoogleGenerateContent(modelId, payload, req, res, logRequestId, logRequestStart) {
@@ -57428,7 +57451,7 @@ IMPORTANT: You MUST respond with valid JSON only.No markdown, no explanation, ju
5742857451
}
5742957452
};
5743057453
}
57431-
async handleWebSocketMessage(socket, raw) {
57454+
async handleWebSocketMessage(socket, raw, endpoint) {
5743257455
const text = typeof raw === "string" ? raw : raw.toString("utf8");
5743357456
let payload;
5743457457
try {
@@ -57441,9 +57464,33 @@ IMPORTANT: You MUST respond with valid JSON only.No markdown, no explanation, ju
5744157464
socket.send(JSON.stringify({ type: "pong", timestamp: Date.now() }));
5744257465
return;
5744357466
}
57467+
if (type === "models.list") {
57468+
const models = await this.getAvailableModels();
57469+
socket.send(JSON.stringify({ type: "models.result", data: { object: "list", data: models } }));
57470+
return;
57471+
}
57472+
switch (endpoint) {
57473+
case "/v1/realtime":
57474+
case "/llama/v1/realtime":
57475+
await this.handleOpenAIWebSocketMessage(socket, payload, type, endpoint);
57476+
return;
57477+
case "/anthropic/v1/realtime":
57478+
await this.handleAnthropicWebSocketMessage(socket, payload, type);
57479+
return;
57480+
case "/google/v1/realtime":
57481+
await this.handleGoogleWebSocketMessage(socket, payload, type);
57482+
return;
57483+
default:
57484+
throw new ApiError(400, `Unknown WebSocket endpoint: ${endpoint}`, "invalid_request_error", "unknown_endpoint");
57485+
}
57486+
}
57487+
async handleOpenAIWebSocketMessage(socket, payload, type, endpoint) {
5744457488
if (!type || type === "chat.completions.create") {
5744557489
const body = payload?.data ?? payload?.request ?? payload;
57446-
const response = await this.processChatCompletion(body, { source: "websocket", endpoint: "/v1/chat/completions" });
57490+
if (endpoint === "/llama/v1/realtime" && body?.max_completion_tokens && !body?.max_tokens) {
57491+
body.max_tokens = body.max_completion_tokens;
57492+
}
57493+
const response = await this.processChatCompletion(body, { source: "websocket", endpoint });
5744757494
socket.send(JSON.stringify({ type: "chat.completion.result", data: response }));
5744857495
return;
5744957496
}
@@ -57453,19 +57500,39 @@ IMPORTANT: You MUST respond with valid JSON only.No markdown, no explanation, ju
5745357500
socket.send(JSON.stringify({ type: "completion.result", data: response }));
5745457501
return;
5745557502
}
57456-
throw new ApiError(400, `Unsupported message type: ${type} `, "invalid_request_error", "unsupported_ws_message");
57503+
throw new ApiError(400, `Unsupported OpenAI WebSocket message type: ${type}`, "invalid_request_error", "unsupported_ws_message");
5745757504
}
57458-
handleWebSocketConnection(socket) {
57505+
async handleAnthropicWebSocketMessage(socket, payload, type) {
57506+
if (!type || type === "messages.create") {
57507+
const body = payload?.data ?? payload?.request ?? payload;
57508+
const response = await this.processAnthropicMessages(body);
57509+
socket.send(JSON.stringify({ type: "message.result", data: response }));
57510+
return;
57511+
}
57512+
throw new ApiError(400, `Unsupported Anthropic WebSocket message type: ${type}`, "invalid_request_error", "unsupported_ws_message");
57513+
}
57514+
async handleGoogleWebSocketMessage(socket, payload, type) {
57515+
if (!type || type === "generateContent") {
57516+
const body = payload?.data ?? payload?.request ?? payload;
57517+
const modelId = payload?.model ?? "gemini-pro";
57518+
const response = await this.processGoogleGenerateContent(modelId, body);
57519+
socket.send(JSON.stringify({ type: "content.result", data: response }));
57520+
return;
57521+
}
57522+
throw new ApiError(400, `Unsupported Google WebSocket message type: ${type}`, "invalid_request_error", "unsupported_ws_message");
57523+
}
57524+
handleWebSocketConnection(socket, endpoint) {
5745957525
socket.send(JSON.stringify({
5746057526
type: "session.created",
5746157527
session: {
5746257528
id: (0, import_crypto.randomUUID)(),
5746357529
model: this.config.defaultModel,
57464-
created: Math.floor(Date.now() / 1e3)
57530+
created: Math.floor(Date.now() / 1e3),
57531+
endpoint
5746557532
}
5746657533
}));
5746757534
socket.on("message", (data) => {
57468-
void this.handleWebSocketMessage(socket, data).catch((error2) => {
57535+
void this.handleWebSocketMessage(socket, data, endpoint).catch((error2) => {
5746957536
if (error2 instanceof ApiError) {
5747057537
this.sendWsError(socket, error2);
5747157538
} else {
@@ -57842,7 +57909,8 @@ ${text} `;
5784257909
{ name: "Google", description: "Google Generative AI API compatible endpoints" },
5784357910
{ name: "Llama", description: "Meta Llama API compatible endpoints" },
5784457911
{ name: "Models", description: "Model information" },
57845-
{ name: "Utilities", description: "Utility endpoints" }
57912+
{ name: "Utilities", description: "Utility endpoints" },
57913+
{ name: "WebSocket", description: "Real-time WebSocket endpoints for all providers" }
5784657914
],
5784757915
paths: {
5784857916
"/v1/chat/completions": {
@@ -58190,6 +58258,54 @@ ${text} `;
5819058258
}
5819158259
}
5819258260
}
58261+
},
58262+
"/v1/realtime": {
58263+
get: {
58264+
tags: ["WebSocket"],
58265+
summary: "OpenAI Realtime WebSocket",
58266+
description: "Initiate a WebSocket connection for real-time chat completions using the OpenAI format. Requires `Upgrade: websocket` header.",
58267+
operationId: "connectOpenAIRealtime",
58268+
responses: {
58269+
"101": { description: "Switching Protocols to WebSocket" },
58270+
"426": { description: "Upgrade Required - Use WebSocket client" }
58271+
}
58272+
}
58273+
},
58274+
"/anthropic/v1/realtime": {
58275+
get: {
58276+
tags: ["WebSocket", "Anthropic"],
58277+
summary: "Anthropic Realtime WebSocket",
58278+
description: "Initiate a WebSocket connection using the Anthropic Messages API format.",
58279+
operationId: "connectAnthropicRealtime",
58280+
responses: {
58281+
"101": { description: "Switching Protocols to WebSocket" },
58282+
"426": { description: "Upgrade Required - Use WebSocket client" }
58283+
}
58284+
}
58285+
},
58286+
"/google/v1/realtime": {
58287+
get: {
58288+
tags: ["WebSocket", "Google"],
58289+
summary: "Google Realtime WebSocket",
58290+
description: "Initiate a WebSocket connection using the Google Gemini format.",
58291+
operationId: "connectGoogleRealtime",
58292+
responses: {
58293+
"101": { description: "Switching Protocols to WebSocket" },
58294+
"426": { description: "Upgrade Required - Use WebSocket client" }
58295+
}
58296+
}
58297+
},
58298+
"/llama/v1/realtime": {
58299+
get: {
58300+
tags: ["WebSocket", "Llama"],
58301+
summary: "Llama Realtime WebSocket",
58302+
description: "Initiate a WebSocket connection using the Llama/OpenAI format.",
58303+
operationId: "connectLlamaRealtime",
58304+
responses: {
58305+
"101": { description: "Switching Protocols to WebSocket" },
58306+
"426": { description: "Upgrade Required - Use WebSocket client" }
58307+
}
58308+
}
5819358309
}
5819458310
},
5819558311
components: {

dist/extension.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "GitHub Copilot API Gateway",
44
"description": "Transform GitHub Copilot into a full AI platform: OpenAI-compatible API server + Enterprise Apps Hub with Playwright Generator, Code Review, Commit Messages, and more.",
55
"icon": "media/icon.png",
6-
"version": "0.0.16",
6+
"version": "0.0.17",
77
"author": {
88
"name": "Suhaib Bin Younis",
99
"email": "vscode@suhaib.in",
@@ -289,4 +289,4 @@
289289
}
290290
}
291291
}
292-
}
292+
}

0 commit comments

Comments
 (0)