Rene Fichtmueller 128e18b751 feat: integrate GitHub Copilot as third LLM provider via copilot-bridge
Add GitHub Copilot API proxy integration to LLM Gateway:

* Implement copilot-bridge service:
  - HTTP wrapper managing copilot-api (GitHub Copilot API proxy)
  - OpenAI-compatible /v1/chat/completions endpoint (port 3252)
  - Graceful startup and SIGTERM shutdown handling
  - Health check endpoint with service diagnostics

* Register copilot-bridge in provider fallback chain:
  - Position: After OpenAI, before free LLM APIs (tier 4)
  - Rate limit: 60 requests/min (GitHub Copilot API limit)
  - Models: gpt-4 (reasoning), gpt-3.5-turbo (medium)
  - Authentication: GitHub Copilot subscription (internal to copilot-api)

* Update PM2 ecosystem configuration:
  - Add copilot-bridge service definition (port 3252)
  - Configure COPILOT_BRIDGE_URL in gateway environment
  - Add copilot to LLM_PROVIDERS list

* Enhance deployment automation:
  - Update ensure-bridges.sh with copilot-bridge deployment
  - Copy service files from repo to /opt/copilot-bridge
  - Run npm install for copilot-api dependency

* Comprehensive documentation:
  - Expand DEPLOYMENT-BRIDGES.md with copilot-bridge section
  - Prerequisites: Node.js 20+, GitHub Copilot subscription
  - Authentication workflow: npm run auth with GitHub OAuth
  - Troubleshooting: subscription verification, auth cache reset

Provider chain now supports:
1. Ollama (local, free)
2. claude-bridge (Claude subscription)
3. openai-bridge (OpenAI subscription)
4. copilot-bridge (GitHub Copilot subscription) ← NEW
5. Free APIs: Cerebras, Groq, Mistral, NVIDIA, Cloudflare

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-04-25 12:38:30 +02:00

178 lines
4.8 KiB
JavaScript

/**
* Copilot Bridge Wrapper
*
* This wrapper manages the GitHub Copilot API proxy (copilot-api).
* The copilot-api package itself is an OpenAI-compatible proxy for GitHub Copilot.
*
* This script:
* 1. Validates GitHub authentication
* 2. Starts copilot-api on the configured port
* 3. Provides health check endpoint
* 4. Handles graceful shutdown
*/
import { execFile } from 'child_process'
import { promisify } from 'util'
import http from 'http'
const exec = promisify(execFile)
const PORT = process.env.COPILOT_BRIDGE_PORT || 3252
const COPILOT_API_PORT = parseInt(process.env.COPILOT_API_INTERNAL_PORT || '4141')
let copilotApiProcess = null
let copilotHealthy = false
/**
* Start the GitHub Copilot API proxy
* This runs: npx copilot-api@latest start --port <port>
*/
async function startCopilotAPI() {
console.log(`[${new Date().toISOString()}] Starting GitHub Copilot API proxy on port ${COPILOT_API_PORT}...`)
return new Promise((resolve, reject) => {
const args = [
'copilot-api@latest',
'start',
'--port', String(COPILOT_API_PORT),
'--verbose'
]
const child = execFile('npx', args, (err) => {
if (err && err.code !== 0) {
console.error(`[${new Date().toISOString()}] Copilot API process exited:`, err.message)
copilotHealthy = false
}
})
// Monitor output
child.stdout?.on('data', (data) => {
console.log(`[Copilot] ${data.toString().trim()}`)
if (data.toString().includes('listening') || data.toString().includes('ready')) {
copilotHealthy = true
resolve(child)
}
})
child.stderr?.on('data', (data) => {
console.error(`[Copilot Error] ${data.toString().trim()}`)
})
copilotApiProcess = child
// Timeout if copilot-api doesn't start within 30s
setTimeout(() => {
if (!copilotHealthy) {
reject(new Error('Copilot API failed to start within 30 seconds'))
}
}, 30000)
})
}
/**
* Proxy requests to copilot-api
*/
async function proxyCopilotAPI(req, res, path) {
try {
const body = await new Promise((resolve, reject) => {
let data = ''
req.on('data', chunk => data += chunk)
req.on('end', () => resolve(data))
req.on('error', reject)
})
const options = {
hostname: 'localhost',
port: COPILOT_API_PORT,
path: path,
method: req.method,
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
...req.headers
}
}
const proxyReq = http.request(options, (proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers)
proxyRes.pipe(res)
})
proxyReq.on('error', (err) => {
console.error('Proxy error:', err.message)
res.writeHead(502)
res.end(JSON.stringify({ error: 'Bad Gateway', details: err.message }))
})
proxyReq.end(body)
} catch (e) {
console.error('Proxy request error:', e.message)
res.writeHead(500)
res.end(JSON.stringify({ error: e.message }))
}
}
/**
* HTTP Server for health checks and proxying
*/
const server = http.createServer(async (req, res) => {
res.setHeader('Content-Type', 'application/json')
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
if (req.method === 'OPTIONS') {
res.writeHead(200)
res.end()
return
}
// Health check endpoint
if (req.method === 'GET' && req.url === '/health') {
res.writeHead(copilotHealthy ? 200 : 503)
res.end(JSON.stringify({
status: copilotHealthy ? 'ok' : 'starting',
provider: 'github-copilot',
version: '1.0.0',
copilot_api_port: COPILOT_API_PORT,
healthy: copilotHealthy
}))
return
}
// Proxy all other requests to copilot-api
if (req.method === 'POST' || req.method === 'GET') {
// Forward to copilot-api
proxyCopilotAPI(req, res, req.url)
return
}
res.writeHead(404)
res.end(JSON.stringify({ error: 'Not found' }))
})
// Graceful shutdown
process.on('SIGTERM', () => {
console.log(`[${new Date().toISOString()}] SIGTERM received, shutting down...`)
server.close(() => {
if (copilotApiProcess) {
copilotApiProcess.kill()
}
process.exit(0)
})
})
// Start copilot-api and then the proxy server
startCopilotAPI()
.then(() => {
server.listen(PORT, () => {
console.log(`[${new Date().toISOString()}] copilot-bridge running on port ${PORT}`)
console.log(` POST http://localhost:${PORT}/v1/chat/completions`)
console.log(` GET http://localhost:${PORT}/health`)
console.log(` GitHub Copilot API: http://localhost:${COPILOT_API_PORT}`)
})
})
.catch((err) => {
console.error(`[${new Date().toISOString()}] Failed to start:`, err.message)
process.exit(1)
})