Skip to content

Commit 29c0413

Browse files
v0.0.8: Add Prometheus metrics, HTTPS support, enhanced sidebar analytics
1 parent 0c7d960 commit 29c0413

8 files changed

Lines changed: 60830 additions & 299 deletions

File tree

dist/extension.js

Lines changed: 53493 additions & 243 deletions
Large diffs are not rendered by default.

dist/extension.js.map

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

package-lock.json

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

package.json

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "GitHub Copilot API Gateway",
44
"description": "Expose GitHub Copilot as a local OpenAI-compatible API server. Use any OpenAI SDK, curl, or HTTP client to access Copilot's language models.",
55
"icon": "media/icon.png",
6-
"version": "0.0.7",
6+
"version": "0.0.8",
77
"author": {
88
"name": "Suhaib Bin Younis",
99
"email": "vscode@suhaib.in",
@@ -26,6 +26,9 @@
2626
"categories": [
2727
"Other"
2828
],
29+
"activationEvents": [
30+
"onUri"
31+
],
2932
"main": "./dist/extension.js",
3033
"scripts": {
3134
"vscode:prepublish": "npm run package",
@@ -43,6 +46,7 @@
4346
},
4447
"dependencies": {
4548
"@modelcontextprotocol/sdk": "^1.25.1",
49+
"selfsigned": "^5.2.0",
4650
"swagger-ui-dist": "^5.31.0",
4751
"ws": "^8.18.3"
4852
},
@@ -85,6 +89,11 @@
8589
"command": "github-copilot-api-vscode.openDashboard",
8690
"title": "Copilot API: Open Dashboard",
8791
"category": "GitHub Copilot"
92+
},
93+
{
94+
"command": "github-copilot-api-vscode.createDesktopShortcut",
95+
"title": "Copilot API: Create Desktop Shortcut",
96+
"category": "GitHub Copilot"
8897
}
8998
],
9099
"menus": {
@@ -139,6 +148,21 @@
139148
"default": true,
140149
"description": "Expose the WebSocket realtime endpoint at /v1/realtime."
141150
},
151+
"githubCopilotApi.server.enableHttps": {
152+
"type": "boolean",
153+
"default": false,
154+
"description": "Enable HTTPS/TLS encryption for the API server. Auto-generates a self-signed certificate if no certificate paths are configured."
155+
},
156+
"githubCopilotApi.server.tlsCertPath": {
157+
"type": "string",
158+
"default": "",
159+
"description": "Path to the TLS/SSL certificate file (.pem or .crt)."
160+
},
161+
"githubCopilotApi.server.tlsKeyPath": {
162+
"type": "string",
163+
"default": "",
164+
"description": "Path to the TLS/SSL private key file (.pem or .key)."
165+
},
142166
"githubCopilotApi.server.autoStart": {
143167
"type": "boolean",
144168
"default": false,

src/CopilotApiGateway.ts

Lines changed: 121 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { randomUUID } from 'crypto';
22
import { createServer, type IncomingMessage, type ServerResponse } from 'http';
3+
import { createServer as createHttpsServer } from 'https';
34
import type { AddressInfo } from 'net';
45
import * as os from 'os';
56
import * as vscode from 'vscode';
@@ -56,6 +57,9 @@ export interface ApiServerConfig {
5657
enabled: boolean
5758
enableHttp: boolean
5859
enableWebSocket: boolean
60+
enableHttps: boolean
61+
tlsCertPath: string
62+
tlsKeyPath: string
5963
host: string
6064
port: number
6165
maxConcurrentRequests: number
@@ -171,6 +175,7 @@ export class CopilotApiGateway implements vscode.Disposable {
171175
private config: ApiServerConfig = getServerConfig();
172176
private disposed = false;
173177
private activeRequests = 0;
178+
private isHttps = false;
174179
private suppressRestart = false;
175180
private readonly _onDidChangeStatus = new vscode.EventEmitter<void>();
176181
public readonly onDidChangeStatus = this._onDidChangeStatus.event;
@@ -263,6 +268,7 @@ export class CopilotApiGateway implements vscode.Disposable {
263268
public async getStatus() {
264269
return {
265270
running: !!this.httpServer,
271+
isHttps: this.isHttps,
266272
config: this.config,
267273
activeRequests: this.activeRequests,
268274
networkInfo: this.getNetworkInfo(),
@@ -518,6 +524,7 @@ export class CopilotApiGateway implements vscode.Disposable {
518524
// Update stats every 5 seconds
519525
this.statsInterval = setInterval(() => {
520526
this.updateRealtimeStats();
527+
this._onDidChangeStatus.fire();
521528
}, 5000);
522529
}
523530

@@ -730,6 +737,10 @@ export class CopilotApiGateway implements vscode.Disposable {
730737
await this.updateServerConfig({ enableLogging: !this.config.enableLogging });
731738
}
732739

740+
public async toggleHttps(): Promise<void> {
741+
await this.updateServerConfig({ enableHttps: !this.config.enableHttps, enabled: true });
742+
}
743+
733744
public async setApiKey(apiKey: string): Promise<void> {
734745
const value = (apiKey ?? '').trim();
735746
await this.updateServerConfig({ apiKey: value });
@@ -809,7 +820,8 @@ export class CopilotApiGateway implements vscode.Disposable {
809820
this.updateStatusBar('starting');
810821
this._onDidChangeStatus.fire();
811822

812-
this.httpServer = createServer((req, res) => {
823+
// Create request handler function
824+
const requestHandler = (req: IncomingMessage, res: ServerResponse) => {
813825
// Track active connections for graceful shutdown
814826
this.connections.add(res);
815827
res.on('close', () => this.connections.delete(res));
@@ -842,7 +854,60 @@ export class CopilotApiGateway implements vscode.Disposable {
842854

843855
this.activeRequests++;
844856
this._onDidChangeStatus.fire();
845-
});
857+
};
858+
859+
// Create HTTP or HTTPS server based on config
860+
let isHttps = false;
861+
if (this.config.enableHttps) {
862+
try {
863+
let certData: { cert: Buffer | string; key: Buffer | string } | null = null;
864+
865+
// Check if user provided cert paths
866+
if (this.config.tlsCertPath && this.config.tlsKeyPath) {
867+
const certPath = this.config.tlsCertPath.startsWith('~')
868+
? path.join(os.homedir(), this.config.tlsCertPath.slice(1))
869+
: this.config.tlsCertPath;
870+
const keyPath = this.config.tlsKeyPath.startsWith('~')
871+
? path.join(os.homedir(), this.config.tlsKeyPath.slice(1))
872+
: this.config.tlsKeyPath;
873+
874+
if (fs.existsSync(certPath) && fs.existsSync(keyPath)) {
875+
certData = {
876+
cert: fs.readFileSync(certPath),
877+
key: fs.readFileSync(keyPath)
878+
};
879+
this.logInfo(`HTTPS enabled with certificate from ${certPath}`);
880+
}
881+
}
882+
883+
// Auto-generate self-signed cert if no cert configured
884+
if (!certData) {
885+
const selfsigned = require('selfsigned');
886+
const attrs = [{ name: 'commonName', value: 'localhost' }];
887+
const pems = selfsigned.generate(attrs, {
888+
days: 365,
889+
keySize: 2048,
890+
algorithm: 'sha256'
891+
});
892+
certData = {
893+
cert: pems.cert,
894+
key: pems.private
895+
};
896+
this.logInfo('HTTPS enabled with auto-generated self-signed certificate (valid 365 days)');
897+
}
898+
899+
this.httpServer = createHttpsServer(certData, requestHandler);
900+
isHttps = true;
901+
} catch (error) {
902+
this.logError('Failed to setup HTTPS, falling back to HTTP', error);
903+
this.httpServer = createServer(requestHandler);
904+
}
905+
} else {
906+
this.httpServer = createServer(requestHandler);
907+
}
908+
909+
// Track actual runtime protocol
910+
this.isHttps = isHttps;
846911

847912
this.httpServer.on('error', error => {
848913
this.logError('HTTP server error', error);
@@ -883,9 +948,10 @@ export class CopilotApiGateway implements vscode.Disposable {
883948

884949
const address = this.httpServer.address() as AddressInfo | null;
885950
if (address) {
886-
const location = `http://${address.address}:${address.port}`;
887-
this.logInfo(`HTTP server listening on ${location}`);
888-
this.updateStatusBar('running', `HTTP${this.config.enableWebSocket ? '+WS' : ''} on ${location}`);
951+
const protocol = isHttps ? 'https' : 'http';
952+
const location = `${protocol}://${address.address}:${address.port}`;
953+
this.logInfo(`${isHttps ? 'HTTPS' : 'HTTP'} server listening on ${location}`);
954+
this.updateStatusBar('running', `${isHttps ? 'HTTPS' : 'HTTP'}${this.config.enableWebSocket ? '+WS' : ''} on ${location}`);
889955
this._onDidChangeStatus.fire();
890956
}
891957
}
@@ -1129,6 +1195,49 @@ export class CopilotApiGateway implements vscode.Disposable {
11291195
return;
11301196
}
11311197

1198+
// Prometheus metrics endpoint
1199+
if (req.method === 'GET' && url.pathname === '/metrics') {
1200+
const uptime = Math.floor((Date.now() - this.usageStats.startTime) / 1000);
1201+
const metrics = [
1202+
'# HELP copilot_api_requests_total Total number of API requests',
1203+
'# TYPE copilot_api_requests_total counter',
1204+
`copilot_api_requests_total ${this.usageStats.totalRequests}`,
1205+
'',
1206+
'# HELP copilot_api_active_requests Current number of active requests',
1207+
'# TYPE copilot_api_active_requests gauge',
1208+
`copilot_api_active_requests ${this.activeRequests}`,
1209+
'',
1210+
'# HELP copilot_api_tokens_input_total Total input tokens consumed',
1211+
'# TYPE copilot_api_tokens_input_total counter',
1212+
`copilot_api_tokens_input_total ${this.usageStats.totalTokensIn}`,
1213+
'',
1214+
'# HELP copilot_api_tokens_output_total Total output tokens generated',
1215+
'# TYPE copilot_api_tokens_output_total counter',
1216+
`copilot_api_tokens_output_total ${this.usageStats.totalTokensOut}`,
1217+
'',
1218+
'# HELP copilot_api_uptime_seconds Server uptime in seconds',
1219+
'# TYPE copilot_api_uptime_seconds gauge',
1220+
`copilot_api_uptime_seconds ${uptime}`,
1221+
'',
1222+
'# HELP copilot_api_requests_per_minute Rate of requests per minute',
1223+
'# TYPE copilot_api_requests_per_minute gauge',
1224+
`copilot_api_requests_per_minute ${this.realtimeStats.requestsPerMinute}`,
1225+
'',
1226+
'# HELP copilot_api_latency_avg_ms Average request latency in milliseconds',
1227+
'# TYPE copilot_api_latency_avg_ms gauge',
1228+
`copilot_api_latency_avg_ms ${this.realtimeStats.avgLatencyMs}`,
1229+
'',
1230+
'# HELP copilot_api_error_rate_percent Error rate percentage',
1231+
'# TYPE copilot_api_error_rate_percent gauge',
1232+
`copilot_api_error_rate_percent ${this.realtimeStats.errorRate}`,
1233+
''
1234+
].join('\n');
1235+
1236+
res.writeHead(200, { 'Content-Type': 'text/plain; version=0.0.4; charset=utf-8' });
1237+
res.end(metrics);
1238+
return;
1239+
}
1240+
11321241
// List all models
11331242
if (req.method === 'GET' && url.pathname === '/v1/models') {
11341243
const models = await this.getAvailableModels();
@@ -3807,6 +3916,9 @@ export class CopilotApiGateway implements vscode.Disposable {
38073916
if (patch.enableWebSocket !== undefined) {
38083917
updates.push(Promise.resolve(config.update('server.enableWebSocket', patch.enableWebSocket, vscode.ConfigurationTarget.Global)));
38093918
}
3919+
if (patch.enableHttps !== undefined) {
3920+
updates.push(Promise.resolve(config.update('server.enableHttps', patch.enableHttps, vscode.ConfigurationTarget.Global)));
3921+
}
38103922
if (patch.host !== undefined) {
38113923
updates.push(Promise.resolve(config.update('server.host', patch.host, vscode.ConfigurationTarget.Global)));
38123924
}
@@ -4007,6 +4119,9 @@ function getServerConfig(): ApiServerConfig {
40074119
const enabled = configuration.get<boolean>('server.enabled', false);
40084120
const enableHttp = configuration.get<boolean>('server.enableHttp', true);
40094121
const enableWebSocket = configuration.get<boolean>('server.enableWebSocket', true);
4122+
const enableHttps = configuration.get<boolean>('server.enableHttps', false);
4123+
const tlsCertPath = configuration.get<string>('server.tlsCertPath', '').trim();
4124+
const tlsKeyPath = configuration.get<string>('server.tlsKeyPath', '').trim();
40104125
const host = configuration.get<string>('server.host', '127.0.0.1').trim() || '127.0.0.1';
40114126
const rawPort = configuration.get<number>('server.port', 3030);
40124127
const port = Number.isFinite(rawPort) ? Math.max(1, Math.floor(rawPort)) : 3030;
@@ -4065,7 +4180,7 @@ function getServerConfig(): ApiServerConfig {
40654180
const mcpEnabled = vscode.workspace.getConfiguration('githubCopilotApi.mcp').get<boolean>('enabled', true);
40664181

40674182
return {
4068-
enabled, enableHttp, enableWebSocket, host, port, maxConcurrentRequests,
4183+
enabled, enableHttp, enableWebSocket, enableHttps, tlsCertPath, tlsKeyPath, host, port, maxConcurrentRequests,
40694184
defaultModel, apiKey, enableLogging, rateLimitPerMinute, defaultSystemPrompt,
40704185
redactionPatterns, ipAllowlist, requestTimeoutSeconds, maxPayloadSizeMb, maxConnectionsPerIp,
40714186
mcpEnabled

0 commit comments

Comments
 (0)