Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@
All notable changes to the VS Code extension are documented here.

## [Unreleased]
## [0.8.0] — 2026-05-01
### Added — Bring-Your-Own-Endpoint (REQ-142)
- **`EndpointsClient` module** shells out to `specsmith endpoints list / test --json` and surfaces results to the host. JSON parsers (`parseEndpointsList`, `parseEndpointHealth`) and the `applyEndpointArg` bridge helper are exported pure-TS so they have direct mocha coverage.
- **`specsmith.endpoints` command.** Quick Pick over the registered endpoints with copy-id / set-default / test actions; `\u2605` marks the current default.
- **`specsmith.testEndpoint` command.** Probes `/v1/models` via `specsmith endpoints test --json` and shows a notification with latency + model count.
- **`SessionConfig.endpointId`** plumbed through `bridge.ts`. When set, the bridge appends `--endpoint <id>` to `specsmith run`, routing the LLM turn to the OpenAI-v1-compatible backend registered with `specsmith endpoints add` (vLLM, llama.cpp `server`, LM Studio, TGI, ...).
- **11 new mocha tests** in `src/test/endpoints-client.test.ts`. Total now 117 passing (was 104).

### Changed
- `package.json` version bumped from 0.7.0 to 0.8.0; description mentions BYOE OpenAI-v1 endpoints.

### Validation
- `npm run lint` (tsc --noEmit): clean.
- `npm run build` (esbuild): 279.8kb bundle.
- `npm test`: **117 passing** (+13 since 0.7.0).
## [0.7.0] — 2026-04-30
### Added — warp parity follow-up
- **Drive sidebar tree (REQ-133).** New `src/DriveTree.ts` exposes the contents of ~/.specsmith/drive as a sidebar tree. Title-bar actions trigger `specsmith drive push` / `pull`.
Expand Down
16 changes: 14 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "specsmith-vscode",
"displayName": "specsmith - AEE Workbench",
"description": "Applied Epistemic Engineering workbench. 6-tab Settings panel, AI agent sessions, execution profiles, FPGA/HDL tool support, Ollama local LLMs, and tool installer.",
"version": "0.7.0",
"description": "Applied Epistemic Engineering workbench. 6-tab Settings panel, AI agent sessions, execution profiles, FPGA/HDL tool support, Ollama local LLMs, BYOE OpenAI-v1 endpoints, and tool installer.",
"version": "0.8.0",
"publisher": "BitConcepts",
"license": "MIT",
"engines": {
Expand Down Expand Up @@ -407,6 +407,18 @@
"title": "Suggest Command",
"category": "specsmith",
"icon": "$(lightbulb)"
},
{
"command": "specsmith.endpoints",
"title": "BYOE Endpoints\u2026",
"category": "specsmith",
"icon": "$(plug)"
},
{
"command": "specsmith.testEndpoint",
"title": "Test BYOE Endpoint",
"category": "specsmith",
"icon": "$(beaker)"
}
],
"menus": {
Expand Down
183 changes: 183 additions & 0 deletions src/EndpointsClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 BitConcepts, LLC. All rights reserved.
//
// EndpointsClient — extension-side wrapper around the `specsmith endpoints`
// CLI group (REQ-142). The module is framework-free so it can be exercised
// from mocha unit tests without booting the full extension host.
//
// Notes:
// * The on-disk schema is owned by the CLI; this module never reads
// `~/.specsmith/endpoints.json` directly. Each operation shells out to
// `specsmith endpoints <subcommand> --json` so schema drift is impossible.
// * Token bytes never appear in TypeScript memory. The CLI redacts inline
// tokens before serialising the `list --json` payload.

import * as cp from 'child_process';

/** Public-facing endpoint record matching what `specsmith endpoints list --json` emits. */
export interface EndpointRecord {
id: string;
name: string;
base_url: string;
default_model: string;
verify_tls: boolean;
tags: string[];
created_at: string;
auth: {
kind: 'none' | 'bearer-inline' | 'bearer-env' | 'bearer-keyring';
token?: string; // always "***" for bearer-inline (CLI redacts)
token_env?: string;
keyring_service?: string;
keyring_user?: string;
};
}

/** Top-level list payload. */
export interface EndpointsListResult {
default_endpoint_id: string;
endpoints: EndpointRecord[];
}

/** Result of `specsmith endpoints test <id> --json`. */
export interface EndpointHealthResult {
id: string;
ok: boolean;
latency_ms: number;
models: string[];
error: string;
status_code: number | null;
}

/**
* Parse the JSON body of `specsmith endpoints list --json`.
* Tolerates a missing `endpoints` array (returns empty list).
*/
export function parseEndpointsList(raw: string): EndpointsListResult {
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
return { default_endpoint_id: '', endpoints: [] };
}
if (!parsed || typeof parsed !== 'object') {
return { default_endpoint_id: '', endpoints: [] };
}
const root = parsed as Record<string, unknown>;
const def = typeof root.default_endpoint_id === 'string' ? root.default_endpoint_id : '';
const list = Array.isArray(root.endpoints) ? root.endpoints : [];
const endpoints: EndpointRecord[] = [];
for (const item of list) {
if (!item || typeof item !== 'object') { continue; }
const obj = item as Record<string, unknown>;
const auth = (obj.auth ?? {}) as Record<string, unknown>;
endpoints.push({
id: String(obj.id ?? ''),
name: String(obj.name ?? ''),
base_url: String(obj.base_url ?? ''),
default_model: String(obj.default_model ?? ''),
verify_tls: obj.verify_tls !== false,
tags: Array.isArray(obj.tags) ? obj.tags.map(String) : [],
created_at: String(obj.created_at ?? ''),
auth: {
kind: (typeof auth.kind === 'string' && /^(none|bearer-(inline|env|keyring))$/.test(auth.kind))
? auth.kind as EndpointRecord['auth']['kind']
: 'none',
token: typeof auth.token === 'string' ? auth.token : undefined,
token_env: typeof auth.token_env === 'string' ? auth.token_env : undefined,
keyring_service: typeof auth.keyring_service === 'string' ? auth.keyring_service : undefined,
keyring_user: typeof auth.keyring_user === 'string' ? auth.keyring_user : undefined,
},
});
}
return { default_endpoint_id: def, endpoints };
}

/** Parse the JSON body of `specsmith endpoints test <id> --json`. */
export function parseEndpointHealth(raw: string): EndpointHealthResult {
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
return { id: '', ok: false, latency_ms: 0, models: [], error: raw, status_code: null };
}
if (!parsed || typeof parsed !== 'object') {
return { id: '', ok: false, latency_ms: 0, models: [], error: 'malformed response', status_code: null };
}
const obj = parsed as Record<string, unknown>;
return {
id: String(obj.id ?? ''),
ok: Boolean(obj.ok),
latency_ms: typeof obj.latency_ms === 'number' ? obj.latency_ms : 0,
models: Array.isArray(obj.models) ? obj.models.map(String) : [],
error: String(obj.error ?? ''),
status_code: typeof obj.status_code === 'number' ? obj.status_code : null,
};
}

/**
* Append `--endpoint <id>` to a bridge args list when ``endpointId`` is set.
* Pure helper, exported so the bridge plumbing has direct mocha coverage.
*/
export function applyEndpointArg(args: string[], endpointId: string | undefined): string[] {
if (endpointId && endpointId.trim()) {
return [...args, '--endpoint', endpointId.trim()];
}
return [...args];
}

/**
* Shell out to ``specsmith endpoints list --json`` synchronously.
* Returns an empty list on any failure (missing CLI, bad JSON, etc.) so the
* extension always has something to render.
*/
export function listEndpoints(execPath: string): EndpointsListResult {
try {
const r = cp.spawnSync(execPath, ['endpoints', 'list', '--json'], {
timeout: 5000,
encoding: 'utf8',
});
if (r.status !== 0 || !r.stdout) {
return { default_endpoint_id: '', endpoints: [] };
}
return parseEndpointsList(r.stdout);
} catch {
return { default_endpoint_id: '', endpoints: [] };
}
}

/**
* Shell out to ``specsmith endpoints test <id> --json`` synchronously.
*/
export function testEndpoint(
execPath: string,
endpointId: string,
timeoutSec = 5,
): EndpointHealthResult {
try {
const r = cp.spawnSync(
execPath,
['endpoints', 'test', endpointId, '--json', '--timeout', String(timeoutSec)],
{ timeout: (timeoutSec + 5) * 1000, encoding: 'utf8' },
);
if (!r.stdout) {
return {
id: endpointId,
ok: false,
latency_ms: 0,
models: [],
error: r.stderr ?? 'no output',
status_code: null,
};
}
return parseEndpointHealth(r.stdout);
} catch (err) {
return {
id: endpointId,
ok: false,
latency_ms: 0,
models: [],
error: String(err),
status_code: null,
};
}
}
6 changes: 6 additions & 0 deletions src/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,12 @@ export class SpecsmithBridge {
'--project-dir', this._config.projectDir];
if (this._config.provider) { args.push('--provider', this._config.provider); }
if (this._config.model) { args.push('--model', this._config.model); }
// REQ-142: when a registered BYOE endpoint is selected for this session,
// route the LLM turn through it. The CLI's chat/run paths short-circuit
// the auto-detect provider chain when --endpoint is supplied.
if (this._config.endpointId) {
args.push('--endpoint', this._config.endpointId);
}

const env = augmentedEnv({ ...process.env, ...this._envOverrides });
// Inject Ollama context length when using the ollama provider
Expand Down
Loading