Features
- OpenAI-compatible.
/v1/chat/completions and /v1/models, with SSE streaming.
- Auto-discovery. Every language model registered in VS Code shows up. No configuration.
- Enforced JSON mode.
response_format is honoured server-side: strict instruction, JSON recovery from prose, schema validation, and a bounded repair retry.
- Sampling controls.
temperature, top_p, max_tokens, stop, seed and penalties are forwarded to the model.
- Image input. OpenAI
image_url parts reach vision-capable models (VS Code 1.125+).
- No silent no-ops. Anything OpenWire cannot honour is rejected or reported back in the response.
GET /v1/capabilities states what this build supports.
- Tool forwarding. Send OpenAI-format tools, get
tool_calls back, with full tool_choice support.
- XML tool call fallback. Non-streaming responses containing a
<function_calls> block are converted to tool_calls.
- Rate limiting. Configurable per-minute request cap.
- API key auth. Bearer token authentication enabled by default.
- Tight CORS defaults. Browser requests are limited to loopback origins.
- Zero dependencies. Node's built-in HTTP server, no Express.
Models
Any model registered with VS Code's Language Model API is exposed automatically. In practice that means:
- Claude. Whatever Anthropic model family/tier is currently registered by an installed extension (e.g. Opus, Sonnet, Haiku)
- GPT. Whatever OpenAI model family is currently registered by an installed extension (e.g. GPT and reasoning-tier variants)
- Gemini. Whatever Google model family is currently registered by an installed extension (e.g. Pro, Flash)
- Ollama. Only when a separate extension registers your local models with VS Code. OpenWire never talks to Ollama directly.
- Anything else registered with the VS Code Language Model API
Call GET /v1/models to see what your setup actually exposes.
Provider compatibility
OpenWire normalises differences between providers so callers get a consistent OpenAI-format response:
| Provider |
Content format |
Tool calling |
Image input |
Status |
| Claude (Anthropic) |
Array of {"type":"text","text":"..."} parts |
Native via VS Code API, plus an XML <function_calls> fallback on non-streaming responses |
Vision models |
Supported |
| GPT (OpenAI) |
Plain string |
Native tool_calls via VS Code API |
Vision models |
Supported |
| Gemini (Google) |
Plain string, or text parts tagged type: "text" |
Native via VS Code API |
Vision models |
Supported |
| Ollama (local) |
Plain string |
Depends on the model |
Depends on the model |
Only via a VS Code LM provider extension |
Image input additionally requires VS Code 1.125 or newer, whatever the provider.
Content normalisation. Message content can be a plain string or an array of parts;
assistant content may also be null when valid tool_calls are present. Text parts flatten
to a plain string. User image_url parts are carried through as binary image data rather than
dropped. Malformed messages, unknown roles, role-incompatible images, and unsupported content
parts are rejected with a 400 instead of being silently normalised or discarded.
Tool call fallback. A model can answer with a raw XML <function_calls> block instead of
a native tool call. On non-streaming requests OpenWire parses that block into standard
tool_calls and strips it from the message content. Streaming requests rely on native
tool-call parts, so an XML block can still reach the client as ordinary text.
Request parameters
OpenWire never silently ignores a parameter. Every field lands in one of four buckets, and
GET /v1/capabilities reports the split for the running build.
| Bucket |
Behaviour |
Fields |
| Honoured |
Applied and enforced by OpenWire |
model, messages, stream, stream_options, tools, tool_choice, response_format, max_completion_tokens, n |
| Forwarded |
Passed through modelOptions; support and exact semantics depend on the selected VS Code model provider |
temperature, top_p, max_tokens, stop, seed, presence_penalty, frequency_penalty |
| Rejected |
400, because honouring them partially would be misleading |
malformed messages or response_format, n greater than 1, out-of-range sampling values, unsupported or malformed JSON Schema constraints, invalid tools/tool_choice combinations, invalid or remote image inputs |
| Reported |
Accepted, but listed under x_openwire.unsupported_params in the response |
logprobs, top_logprobs, logit_bias, user, parallel_tool_calls, store, metadata, service_tier, and other OpenAI fields with no VS Code equivalent |
Set openWire.server.strictParams to true to turn the Reported bucket into 400s as well.
That gives agents a hard guarantee that unsupported fields are rejected. It cannot guarantee
that a model provider applies fields in the Forwarded bucket; check the provider's own
capabilities when those controls are required.
Invalid values are rejected rather than clamped. temperature: 9 returns a 400; it does
not quietly become 2.
prompt is honoured only by /v1/completions. Sending it to /v1/chat/completions has no
effect, so it is reported there like any other unknown field.
Streaming requests carry the same report in a metadata-only frame (choices: []) emitted
before the first content delta, so stream: true is not a way to lose it.
JSON mode
The VS Code Language Model API has no provider-level JSON mode, so OpenWire enforces
response_format itself:
- A strict instruction is appended to the conversation.
- The reply is parsed. JSON is recovered even when wrapped in prose or ```json fences.
- For
json_schema, the value is validated against the schema.
- On failure, OpenWire retries once with a harsher instruction that quotes the bad output.
- If it still cannot produce valid JSON it returns 502. It never returns prose that
claims to be JSON.
curl http://localhost:3030/v1/chat/completions \
-H "Authorization: Bearer $OPENWIRE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "<model-id>",
"messages": [{"role": "user", "content": "Rate this repo out of 10"}],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "rating",
"schema": {
"type": "object",
"required": ["score"],
"properties": {"score": {"type": "integer", "minimum": 0, "maximum": 10}}
}
}
}
}'
json_object guarantees a JSON object. A bare scalar or array is treated as a failure
and sent back through the repair retry, so JSON.parse(content).field is always safe.
Supported schema keywords. type, enum, const, required, properties,
additionalProperties (boolean or sub-schema), items (single schema), minimum,
maximum, minLength, maxLength, minItems, maxItems. Annotations such as title
and description are ignored.
Anything else returns a 400 rather than being accepted and left unenforced — including
$ref, allOf, anyOf, oneOf, patternProperties, and the tuple form of items.
Streaming. Validity cannot be judged mid-stream, so a JSON-mode request with
stream: true buffers the reply, enforces the contract, then emits it as a single content
delta followed by [DONE]. The SSE contract is preserved; the token-by-token behaviour is not.
Images
Vision requests use the standard OpenAI content-part shape:
{
"model": "<model-id>",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "What is in this screenshot?"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,iVBORw0KG..."}}
]
}]
}
- Base64
data: URIs only. Remote http(s) image URLs return a 400. OpenWire will not
fetch a URL on your behalf, because that would let any caller drive requests from your
machine into your own network.
- Supported types.
image/png, image/jpeg, image/gif, image/webp.
- Validated bytes. Base64 must be canonical and non-empty, the file signature must match
the declared MIME type, and each decoded image is limited to 8 MiB.
- Requires VS Code 1.125 or newer, where
LanguageModelDataPart reached the stable API.
On older builds an image request returns 501 rather than silently dropping the image.
Check image_input.supported in GET /v1/capabilities.
- Image payloads are large, so the request body limit defaults to 10 MB
(
openWire.server.maxRequestBodyMb).
Quick start
Install from the VS Code Marketplace, or load the .vsix yourself. The server starts on http://127.0.0.1:3030 as soon as VS Code loads the extension.
If you haven't set openWire.server.apiKey, OpenWire generates a random key each session — check the "OpenWire" output channel after the server starts to find it, or set your own fixed key in settings.
export OPENWIRE_API_KEY="<key from the OpenWire output channel, or your own configured value>"
# List available models
curl http://localhost:3030/v1/models \
-H "Authorization: Bearer $OPENWIRE_API_KEY"
# Chat completion
curl http://localhost:3030/v1/chat/completions \
-H "Authorization: Bearer $OPENWIRE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "<model-id>",
"messages": [{"role": "user", "content": "Hello"}]
}'
# Streaming
curl http://localhost:3030/v1/chat/completions \
-H "Authorization: Bearer $OPENWIRE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "<model-id>",
"messages": [{"role": "user", "content": "Explain zero-knowledge proofs"}],
"stream": true
}'
Replace <model-id> with any id from GET /v1/models. OpenWire matches on model id or family and has no built-in aliases, so an unrecognised name returns a 404 listing what is available.
Endpoints
| Method |
Path |
Description |
GET |
/health |
Health check |
GET |
/v1/models |
List available models |
GET |
/v1/models/:id |
Get specific model |
GET |
/v1/capabilities |
What this build enforces, forwards, rejects, and reports |
POST |
/v1/chat/completions |
Chat completion (streaming + non-streaming) |
POST |
/v1/completions |
Legacy completions, returned in text_completion shape |
Configuration
All settings live under openWire.server.* in VS Code:
| Setting |
Default |
Description |
autoStart |
true |
Start server when VS Code launches |
host |
127.0.0.1 |
Bind address |
port |
3030 |
Port number |
apiKey |
"" (auto-generated per session if empty) |
Bearer token required for authentication |
corsAllowedOrigins |
["http://localhost", "http://127.0.0.1", "http://[::1]"] |
Allowed browser origins for CORS |
defaultModel |
"" |
Fallback model when none specified |
defaultSystemPrompt |
"" |
Injected system prompt if none present |
maxConcurrentRequests |
4 |
Concurrent request limit |
rateLimitPerMinute |
60 |
Rate limit |
requestTimeoutSeconds |
300 |
Request timeout |
strictParams |
false |
Reject parameters OpenWire cannot honour instead of reporting them |
jsonModeMaxRetries |
1 |
Repair attempts when response_format requires JSON; integer from 0 to 3 |
maxRequestBodyMb |
10 |
Maximum request body size in MiB; integer from 1 to 100 |
enableLogging |
false |
Verbose logging |
Commands
- OpenWire: Start Server
- OpenWire: Stop Server
- OpenWire: Restart Server
- OpenWire: Toggle Server
Using with OpenClaw
OpenWire works as a model provider for OpenClaw agents. Register OpenWire as a custom provider called copilot-proxy in your ~/.openclaw/openclaw.json:
{
"models": {
"providers": {
"copilot-proxy": {
"baseUrl": "http://localhost:3030/v1",
"apiKey": "<key from the OpenWire output channel, or your own configured value>",
"api": "openai-completions",
"models": [
{
"id": "<model-id>",
"name": "<Display Name>",
"contextWindow": 128000,
"maxTokens": 8192
}
// add any other models from /v1/models
]
}
}
},
"agents": {
"defaults": {
"model": {
"primary": "copilot-proxy/<model-id>"
}
}
},
"plugins": {
"entries": {
"copilot-proxy": { "enabled": true }
}
}
}
OpenWire requires a Bearer token by default. Set a custom openWire.server.apiKey in VS Code and use the same value in OpenClaw.
Architecture
src/
extension.ts activation, commands, status bar
models/
discovery.ts model discovery, caching, dedup
routes/
chat.ts vscode binding + orchestration
content.ts message/content normalisation, image parts
params.ts sampling params, capability classification
json-mode.ts response_format enforcement
schema.ts JSON Schema subset validator
tool-calls.ts tool mapping, tool_choice, XML fallback
legacy.ts text_completion shape for /v1/completions
errors.ts RequestError with an HTTP status
server/
config.ts settings loader
gateway.ts HTTP server, routing, middleware
ui/
sidebar.ts webview sidebar panel
test/
vscode-mock.ts in-memory vscode stand-in for tests
gateway.test.ts end-to-end HTTP tests
types/
vscode-lm.d.ts type augmentations
Everything under `routes/` except `chat.ts` is free of `vscode` imports, so it is unit
tested directly. `gateway.test.ts` runs the real server over HTTP against a mocked
`vscode` module, aliased in `vitest.config.ts`.
Development
Clone the repo and install dependencies:
npm install
Start esbuild in watch mode, then press F5 in VS Code to launch the Extension
Development Host with OpenWire loaded:
npm run watch
.vscode/launch.json has two configurations. "Run Extension" builds once and
launches. "Run Extension (Watch)" starts the watcher for you. Reload the
Development Host window to pick up a rebuild.
| Command |
What it does |
npm run lint |
Type checks with tsc --noEmit |
npm run test |
Runs the Vitest suite |
npm run build |
Bundles src/ into dist/extension.js |
npm run package |
Builds a production bundle and writes a local .vsix |
npm run package fails when @types/vscode is newer than engines.vscode,
so raising the types version means raising the engine floor too. The reverse
is not checked. Raising engines.vscode on its own packages and publishes
without complaint, which leaves the types older than the VS Code version the
extension now claims to support, so bump @types/vscode to match by hand.
Releasing
CI runs lint, build and test on Node 20 and 22, and packages the extension once
on Node 20. The .vsix from that run is uploaded as a build artifact, so you
can install a branch build before it ships. It runs on every pull request
whatever base branch it targets, so stacked branches are covered too.
To cut a release:
- Bump
version in package.json and merge that to main.
- Tag the merge commit as
vX.Y.Z, matching the version you just set.
- Push the tag.
The Release workflow checks the tag against the version in package.json and
stops if they disagree. It then runs lint, build and test, packages a single
.vsix, publishes that exact file to the VS Code Marketplace, and attaches it
to a GitHub release. Publishing needs a VSCE_PAT repository secret.
License
MIT