Files
llmgw/internal/vscodeext/ext/lib_test.js
T
Ilker ManapandClaude Opus 5.5 38dfe268c9 VS Code gets the gateway's models from an extension the gateway serves
llmgw's own extension for VS Code, plain JavaScript with no dependencies,
packed into a .vsix by the binary at /setup/vscode/llmgw.vsix with the
serving gateway's address in it, so it needs no build, no Node and no
marketplace. It registers llmgw as a language-model provider: the chat's
model picker, agent mode and any extension that asks VS Code for a model
get every chat model the key may use, sized and with tool and vision
capabilities as the catalog says, streamed, tool calls both ways.

VS Code is a tool on the account page's Connect your editor: a key, the
install command, and an Open in VS Code link that hands the key to the
extension's secret storage. The extension's Sign In opens the page with a
state of its own; a link without it is confirmed, the gateway named, since
it decides where code is sent.

Tested in VS Code 1.138 against the PoC: 27 models listed, an answer from
qwen2.5-0.5b-instruct, and qwen3-coder-30b calling a tool and using its
result (internal/vscodeext/e2e).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:39:50 +02:00

124 lines
5.8 KiB
JavaScript

'use strict';
// node --test internal/vscodeext/ext/lib_test.js
// (VS Code's own runtime works too:
// ELECTRON_RUN_AS_NODE=1 "/Applications/Visual Studio Code.app/Contents/MacOS/Electron" --test …)
const test = require('node:test');
const assert = require('node:assert/strict');
const lib = require('./lib');
// VS Code's parts, as the provider receives them: classes in VS Code,
// told apart here by their fields.
const text = (value) => ({ value });
const call = (callId, name, input) => ({ callId, name, input });
const result = (callId, ...values) => ({ callId, content: values.map(text) });
test('gatewayBase takes what people paste', () => {
assert.equal(lib.gatewayBase('https://llm.example.com/'), 'https://llm.example.com');
assert.equal(lib.gatewayBase('https://llm.example.com/v1'), 'https://llm.example.com');
assert.equal(lib.gatewayBase('llm.example.com'), 'https://llm.example.com');
assert.equal(lib.gatewayBase('http://10.0.0.140:8080/v1/'), 'http://10.0.0.140:8080');
assert.equal(lib.gatewayBase('ftp://x'), '');
assert.equal(lib.gatewayBase(''), '');
});
test('modelInfo keeps chat models and says what the catalog says', () => {
const m = lib.modelInfo({
id: 'coder', alias_of: 'qwen3-coder-30b', context_length: 32768, max_output_tokens: 8192,
capabilities: { tools: true, vision: false },
}, true);
assert.equal(m.id, 'coder');
assert.equal(m.family, 'qwen3-coder-30b');
assert.equal(m.maxOutputTokens, 8192);
assert.equal(m.maxInputTokens, 32768 - 8192);
assert.deepEqual(m.capabilities, { toolCalling: true, imageInput: false });
assert.equal(lib.modelInfo({ id: 'bge-m3', capabilities: { embeddings: true } }, true), null);
assert.equal(lib.modelInfo({ id: 'flux', capabilities: { images: true } }, true), null);
assert.equal(lib.modelInfo({ id: 'sf3d', capabilities: { meshes: true, workflows: true } }, true), null);
assert.equal(lib.modelInfo({ id: 'plain', capabilities: {} }, false), null);
const small = lib.modelInfo({ id: 'small', context_length: 4096, capabilities: {} }, true);
assert.equal(small.maxOutputTokens, 1024, 'a quarter of a small context, not 4096 of it');
});
test('chatMessages puts tool calls and results where chat completions want them', () => {
const msgs = lib.chatMessages([
{ role: lib.USER, content: [text('Read main.go')] },
{ role: lib.ASSISTANT, content: [text('Reading it.'), call('c1', 'read_file', { path: 'main.go' })] },
{ role: lib.USER, content: [result('c1', 'package main'), text('Now summarise it.')] },
], false);
assert.deepEqual(msgs, [
{ role: 'user', content: 'Read main.go' },
{
role: 'assistant', content: 'Reading it.',
tool_calls: [{ id: 'c1', type: 'function', function: { name: 'read_file', arguments: '{"path":"main.go"}' } }],
},
{ role: 'tool', tool_call_id: 'c1', content: 'package main' },
{ role: 'user', content: 'Now summarise it.' },
]);
});
test('an assistant message of calls alone has null content', () => {
const [m] = lib.chatMessages([{ role: lib.ASSISTANT, content: [call('c1', 'ls', {})] }], false);
assert.equal(m.content, null);
assert.equal(m.tool_calls.length, 1);
});
test('images go as data URLs only to a model that takes them', () => {
const img = { mimeType: 'image/png', data: new Uint8Array([1, 2, 3]) };
const [withVision] = lib.chatMessages([{ role: lib.USER, content: [text('what is this'), img] }], true);
assert.deepEqual(withVision.content[1], { type: 'image_url', image_url: { url: 'data:image/png;base64,AQID' } });
const [without] = lib.chatMessages([{ role: lib.USER, content: [text('what is this'), img] }], false);
assert.equal(without.content, 'what is this');
});
test('requestBody asks for tools as functions, and requires one when VS Code does', () => {
const model = { id: 'coder', maxOutputTokens: 2048, capabilities: { imageInput: false } };
const body = lib.requestBody(model, [{ role: lib.USER, content: [text('hi')] }], {
tools: [{ name: 'ls', description: 'list', inputSchema: undefined }], toolMode: 2, modelOptions: { temperature: 0.2 },
});
assert.equal(body.stream, true);
assert.equal(body.max_tokens, 2048);
assert.equal(body.tool_choice, 'required');
assert.equal(body.temperature, 0.2);
assert.deepEqual(body.tools[0].function.parameters, { type: 'object', properties: {} });
});
async function* chunks(...parts) {
for (const p of parts) yield new TextEncoder().encode(p);
}
test('sseData reads events split anywhere', async () => {
const got = [];
for await (const d of lib.sseData(chunks('data: {"a":', '1}\r\n\ndata: [DO', 'NE]\n'))) got.push(d);
assert.deepEqual(got, ['{"a":1}', '[DONE]']);
});
test('Answer joins text and tool-call arguments that arrive in pieces', () => {
const a = new lib.Answer();
const ev = (delta) => JSON.stringify({ choices: [{ delta }] });
assert.equal(a.add(ev({ content: 'Hel' })), 'Hel');
assert.equal(a.add(ev({ content: 'lo' })), 'lo');
a.add(ev({ tool_calls: [{ index: 0, id: 'call_a', function: { name: 'read_file', arguments: '{"pa' } }] }));
a.add(ev({ tool_calls: [{ index: 0, function: { arguments: 'th":"x"}' } }] }));
a.add(ev({ tool_calls: [{ index: 1, id: 'call_b', function: { name: 'ls', arguments: '' } }] }));
assert.equal(a.add('[DONE]'), '');
assert.deepEqual(a.toolCalls(), [
{ id: 'call_a', name: 'read_file', input: { path: 'x' } },
{ id: 'call_b', name: 'ls', input: {} },
]);
});
test('Answer passes on an error the stream carries', () => {
const a = new lib.Answer();
assert.throws(() => a.add(JSON.stringify({ error: { message: 'budget spent' } })), /budget spent/);
});
test('errorMessage says what to do about a refused key', () => {
assert.match(lib.errorMessage(401, '{"error":{"message":"invalid key"}}'), /Sign In/);
assert.match(lib.errorMessage(429, '{"error":{"message":"rate limit"}}'), /rate limit/);
assert.match(lib.errorMessage(500, 'boom'), /500: boom/);
});