mirror of
https://github.com/larsbaunwall/vscode-copilot-bridge.git
synced 2025-10-05 22:22:59 +00:00
Models: dynamic Copilot model listing via LM API; inference supports explicit model selection with 404 model_not_found when unavailable
Co-Authored-By: Lars Baunwall <larslb@thinkability.dk>
This commit is contained in:
parent
f37d4306bc
commit
d87fe971b9
2 changed files with 87 additions and 25 deletions
24
README.md
24
README.md
|
|
@ -4,7 +4,7 @@ Local OpenAI-compatible HTTP facade to GitHub Copilot via the VS Code Language M
|
|||
|
||||
- Endpoints (local-only, default bind 127.0.0.1):
|
||||
- POST /v1/chat/completions (SSE streaming; use "stream": false for non-streaming)
|
||||
- GET /v1/models (synthetic listing: gpt-4o-copilot)
|
||||
- GET /v1/models (dynamic listing of available Copilot models)
|
||||
- GET /healthz (ok/unavailable + vscode.version)
|
||||
|
||||
- Copilot pipe: vscode.lm.selectChatModels({ vendor: "copilot", family: "gpt-4o" }) → model.sendRequest(messages)
|
||||
|
|
@ -52,6 +52,28 @@ Optional: Packaging a VSIX
|
|||
## Enabling the Language Model API (if required)
|
||||
|
||||
If your VS Code build requires enabling proposed APIs for the Language Model API, start with:
|
||||
### Models and Selection
|
||||
|
||||
- The bridge lists available GitHub Copilot chat models using the VS Code Language Model API. Example:
|
||||
curl http://127.0.0.1:<port>/v1/models
|
||||
→ { "data": [ { "id": "gpt-4o-copilot", ... }, ... ] }
|
||||
|
||||
- To target a specific model for inference, set the "model" field in your POST body. The bridge accepts:
|
||||
- IDs returned by /v1/models (e.g., "gpt-4o-copilot")
|
||||
- A Copilot family name (e.g., "gpt-4o")
|
||||
- "copilot" to allow default selection
|
||||
|
||||
Examples:
|
||||
curl -N -H "Content-Type: application/json" \
|
||||
-d '{"model":"gpt-4o-copilot","messages":[{"role":"user","content":"hello"}]}' \
|
||||
http://127.0.0.1:<port>/v1/chat/completions
|
||||
|
||||
curl -N -H "Content-Type: application/json" \
|
||||
-d '{"model":"gpt-4o","messages":[{"role":"user","content":"hello"}]}' \
|
||||
http://127.0.0.1:<port>/v1/chat/completions
|
||||
|
||||
- If a requested model is unavailable, the bridge returns:
|
||||
404 with { "error": { "code": "model_not_found", "reason": "not_found" } }.
|
||||
- Stable VS Code: `code --enable-proposed-api thinkability.copilot-bridge`
|
||||
- VS Code Insiders or Extension Development Host (F5) also works.
|
||||
|
||||
|
|
|
|||
|
|
@ -84,23 +84,40 @@ async function startBridge() {
|
|||
}
|
||||
|
||||
if (req.method === 'GET' && req.url === '/v1/models') {
|
||||
writeJson(res, 200, { data: [{ id: 'gpt-4o-copilot', object: 'model', owned_by: 'vscode-bridge' }] });
|
||||
try {
|
||||
const models = await listCopilotModels();
|
||||
writeJson(res, 200, { data: models.map((id: string) => ({ id, object: 'model', owned_by: 'vscode-bridge' })) });
|
||||
} catch (e: any) {
|
||||
writeJson(res, 200, { data: [{ id: 'copilot', object: 'model', owned_by: 'vscode-bridge' }] });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && req.url?.startsWith('/v1/chat/completions')) {
|
||||
let model = await getModel();
|
||||
if (!model) {
|
||||
const hasLM = !!((vscode as any).lm && typeof (vscode as any).lm.selectChatModels === 'function');
|
||||
const reason = !hasLM ? 'missing_language_model_api' : (lastReason || 'copilot_model_unavailable');
|
||||
writeJson(res, 503, { error: { message: 'Copilot unavailable', type: 'server_error', code: 'copilot_unavailable', reason } });
|
||||
return;
|
||||
}
|
||||
|
||||
activeRequests++;
|
||||
if (verbose) output?.appendLine(`Request started (active=${activeRequests})`);
|
||||
try {
|
||||
const body = await readJson(req);
|
||||
const requestedModel: string | undefined = typeof body?.model === 'string' ? body.model : undefined;
|
||||
let familyOverride: string | undefined = undefined;
|
||||
if (requestedModel && /-copilot$/i.test(requestedModel)) {
|
||||
familyOverride = requestedModel.replace(/-copilot$/i, '');
|
||||
} else if (requestedModel && requestedModel.toLowerCase() === 'copilot') {
|
||||
familyOverride = undefined;
|
||||
}
|
||||
|
||||
let model = await getModel(false, familyOverride);
|
||||
const hasLM = !!((vscode as any).lm && typeof (vscode as any).lm.selectChatModels === 'function');
|
||||
if (!model && familyOverride && hasLM) {
|
||||
lastReason = 'not_found';
|
||||
writeJson(res, 404, { error: { message: 'model not found', type: 'invalid_request_error', code: 'model_not_found', reason: 'not_found' } });
|
||||
return;
|
||||
}
|
||||
if (!model) {
|
||||
const reason = !hasLM ? 'missing_language_model_api' : (lastReason || 'copilot_model_unavailable');
|
||||
writeJson(res, 503, { error: { message: 'Copilot unavailable', type: 'server_error', code: 'copilot_unavailable', reason } });
|
||||
return;
|
||||
}
|
||||
const messages = Array.isArray(body?.messages) ? body.messages : null;
|
||||
if (!messages || messages.length === 0 || !messages.every((m: any) =>
|
||||
m && typeof m.role === 'string' &&
|
||||
|
|
@ -248,14 +265,14 @@ function normalizeMessagesLM(messages: any[], histWindow: number): any[] {
|
|||
return result;
|
||||
}
|
||||
|
||||
async function getModel(force = false): Promise<any | undefined> {
|
||||
if (!force && modelCache) return modelCache;
|
||||
async function getModel(force = false, family?: string): Promise<any | undefined> {
|
||||
if (!force && modelCache && !family) return modelCache;
|
||||
const cfg = vscode.workspace.getConfiguration('bridge');
|
||||
const verbose = cfg.get<boolean>('verbose') ?? false;
|
||||
|
||||
const hasLM = !!((vscode as any).lm && typeof (vscode as any).lm.selectChatModels === 'function');
|
||||
if (!hasLM) {
|
||||
modelCache = undefined;
|
||||
if (!family) modelCache = undefined;
|
||||
lastReason = 'missing_language_model_api';
|
||||
const info = server ? server.address() : undefined;
|
||||
const bound = info && typeof info === 'object' ? `${info.address}:${info.port}` : '';
|
||||
|
|
@ -265,28 +282,34 @@ async function getModel(force = false): Promise<any | undefined> {
|
|||
}
|
||||
|
||||
try {
|
||||
let models = await (vscode as any).lm.selectChatModels({ vendor: 'copilot', family: 'gpt-4o' });
|
||||
if (!models || models.length === 0) {
|
||||
models = await (vscode as any).lm.selectChatModels({ vendor: 'copilot' });
|
||||
let models: any[] | undefined;
|
||||
if (family) {
|
||||
models = await (vscode as any).lm.selectChatModels({ vendor: 'copilot', family });
|
||||
} else {
|
||||
models = await (vscode as any).lm.selectChatModels({ vendor: 'copilot', family: 'gpt-4o' });
|
||||
if (!models || models.length === 0) {
|
||||
models = await (vscode as any).lm.selectChatModels({ vendor: 'copilot' });
|
||||
}
|
||||
}
|
||||
if (!models || models.length === 0) {
|
||||
modelCache = undefined;
|
||||
lastReason = 'copilot_model_unavailable';
|
||||
if (!family) modelCache = undefined;
|
||||
lastReason = family ? 'not_found' : 'copilot_model_unavailable';
|
||||
const info = server ? server.address() : undefined;
|
||||
const bound = info && typeof info === 'object' ? `${info.address}:${info.port}` : '';
|
||||
statusItem && (statusItem.text = `Copilot Bridge: Unavailable ${bound ? `@ ${bound}` : ''}`);
|
||||
if (verbose) output?.appendLine('No Copilot language models available.');
|
||||
if (verbose) output?.appendLine(family ? `No Copilot language models available for family="${family}".` : 'No Copilot language models available.');
|
||||
return undefined;
|
||||
}
|
||||
modelCache = models[0];
|
||||
const chosen = models[0];
|
||||
if (!family) modelCache = chosen;
|
||||
lastReason = undefined;
|
||||
const info = server ? server.address() : undefined;
|
||||
const bound = info && typeof info === 'object' ? `${info.address}:${info.port}` : '';
|
||||
statusItem && (statusItem.text = `Copilot Bridge: OK ${bound ? `@ ${bound}` : ''}`);
|
||||
if (verbose) output?.appendLine(`Copilot model selected.`);
|
||||
return modelCache;
|
||||
if (verbose) output?.appendLine(`Copilot model selected${family ? ` (family=${family})` : ''}.`);
|
||||
return chosen;
|
||||
} catch (e: any) {
|
||||
modelCache = undefined;
|
||||
if (!family) modelCache = undefined;
|
||||
const info = server ? server.address() : undefined;
|
||||
const bound = info && typeof info === 'object' ? `${info.address}:${info.port}` : '';
|
||||
statusItem && (statusItem.text = `Copilot Bridge: Unavailable ${bound ? `@ ${bound}` : ''}`);
|
||||
|
|
@ -296,15 +319,32 @@ async function getModel(force = false): Promise<any | undefined> {
|
|||
else if (code === 'RateLimited') lastReason = 'rate_limited';
|
||||
else if (code === 'NotFound') lastReason = 'not_found';
|
||||
else lastReason = 'copilot_unavailable';
|
||||
if (verbose) output?.appendLine(`LM select error: ${e.message} code=${code}`);
|
||||
if (verbose) output?.appendLine(`LM select error: ${e.message} code=${code}${family ? ` family=${family}` : ''}`);
|
||||
} else {
|
||||
lastReason = 'copilot_unavailable';
|
||||
if (verbose) output?.appendLine(`LM select error: ${e?.message || String(e)}`);
|
||||
if (verbose) output?.appendLine(`LM select error: ${e?.message || String(e)}${family ? ` family=${family}` : ''}`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function listCopilotModels(): Promise<string[]> {
|
||||
const hasLM = !!((vscode as any).lm && typeof (vscode as any).lm.selectChatModels === 'function');
|
||||
if (!hasLM) return ['copilot'];
|
||||
try {
|
||||
const models: any[] = await (vscode as any).lm.selectChatModels({ vendor: 'copilot' });
|
||||
if (!models || models.length === 0) return ['copilot'];
|
||||
const ids = models.map((m: any, idx: number) => {
|
||||
const family = (m as any)?.family || (m as any)?.modelFamily || (m as any)?.name || '';
|
||||
const norm = typeof family === 'string' && family.trim() ? family.trim().toLowerCase() : `copilot-${idx + 1}`;
|
||||
return norm.endsWith('-copilot') ? norm : `${norm}-copilot`;
|
||||
});
|
||||
return Array.from(new Set(ids));
|
||||
} catch {
|
||||
return ['copilot'];
|
||||
}
|
||||
}
|
||||
|
||||
function readJson(req: http.IncomingMessage): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let data = '';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue