Skip to content

feat(core): connect model providers over a Unix socket - #62743

Open
divinity76 wants to merge 6 commits into
anomalyco:devfrom
divinity76:provider-unix-socket
Open

divinity76 wants to merge 6 commits into
anomalyco:devfrom
divinity76:provider-unix-socket

Conversation

@divinity76

@divinity76 divinity76 commented Nov 8, 2026 •

Copy link
Copy Markdown

Issue for this PR

OpenCode can only reach a model provider over TCP. When a local server (llama.cpp, Ollama, vLLM, a company gateway) already listens on a Unix socket, the only option today is a socat bridge that exposes a loopback TCP port. This adds a transport setting so provider traffic dials the socket directly: no port opened, no TCP handshake/overhead, no TLS encrypt/decrypt work, and access control is the socket's file permissions. OpenAI Chat Completions and the Responses API are covered on both the native and AI SDK paths.

Type of change

  • New feature

What does this PR do?

Adds provider-level settings.socketPath:

{
  "providers": {
    "local": {
      "package": "@opencode/ai/providers/openai-compatible",
      "settings": {
        "baseURL": "http://localhost/v1",
        "socketPath": "/run/llm.sock"
      },
      "models": {
        "local-model": {}
      }
    }
  }
}
  • The URL supplies the request path, method and headers; its host and port are ignored, like curl --unix-socket. It must be http:, because the transport is plain HTTP over the socket, with no TLS layer to encrypt or decrypt.
  • socketPath is a named setting, so Provider.nativeSettings strips it from provider package options.
  • Native path: RequestExecutor.withTransport is the one place that decides the transport. It builds the per-call middleware with unixSocketFetch (needed because the app graph pins its own fetch per response) and makes a configured socket win over a per-call fetch, so a new send site cannot accidentally go over TCP.
  • AI SDK path: prepareOptions picks the socket fetch over settings.fetch and outbound, keeping the existing timeout and SSE wrapper.
  • Runtime: Bun's fetch(url, { unix }). On non-Bun runtimes socketPath is rejected with a clear error instead of falling back to TCP; the first hand-written node:http fallback was removed during review because it was untested client code.
  • Redirects are returned, not followed, because a redirect target would be TCP. transport: "websocket" is forced to HTTP, and media/decision models get the setting too.

Limitation: built-in Ollama, vLLM and LM Studio auto-discovery only probes HTTP URLs, so socketPath applies to explicitly configured providers and models. This is documented.

How did you verify your code works?

  • packages/ai/test/unix-socket.test.ts: 8 tests against a real Bun.serve({ unix }), including a TCP decoy that must stay empty. Covers JSON POST, path/headers, streaming SSE before completion, abort while waiting for headers and while reading the body, redirects, https rejection, empty path, and a missing socket. Reran 10x.
  • packages/core/test/unix-socket.test.ts: production app graph, not a mock. This test fails with the first implementation (which the reviewer caught) and passes now; the TCP decoy receives nothing.
  • AI SDK path: packages/core/test/aisdk.test.ts sends a request through a socket while the model's base URL points at an unused host.
  • packages/core/test/provider.test.ts asserts socketPath is stripped from native package settings.
  • An empty socketPath is rejected at config decode; live, before the fix it silently hit a TCP decoy (HTTP 501), after the fix the decoy receives no request. A live TCP listener on the base URL port also receives nothing when a socket is configured, which exercises the provider-setting merge through ModelResolver.
  • Live: opencode run --standalone with baseURL: "http://127.0.0.1:59999/v1" (no listener on that port) plus socketPath, against a fake OpenAI-compatible streaming server on the socket. The model replied, so the request can only have gone over the socket.
  • Typecheck passed for packages/ai, packages/core, packages/schema; full packages/ai suite 2056 pass / 0 fail; docs build succeeded; oxlint clean.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Implemented with AI assistance (OpenCode agent, DeepSeek V4.1 Flash). The submitter must review, comprehend and test every line.


Added by the opencode bot (AI-generated).

Public API: adds the optional non-empty socketPath provider setting (config schema, client types) and exports unixSocketFetch from @opencode/ai/route (backwards compatible)

Copilot AI balanced review requested due to automatic review settings November 8, 2026 12:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The feature works as described on Bun, but I couldn't check it end to end through the CLI. The added tests pass. That includes the core test that sends a request through the production app graph to a real Unix socket while a TCP listener on the base URL stays empty, plus the AI SDK path test. tsc --noEmit passes for packages/ai and packages/core. I couldn't run opencode run --standalone here because the standalone server failed to start (an environment problem).

Public API: it adds an optional socketPath provider setting to the config schema and the generated client types (ProviderSettings, ConfigProviderSettings), and exports unixSocketFetch from @opencode/ai/route. Both are additions, so existing configs and callers keep working.

Concerns (inline):

  • The Node node:http fallback isn't tested.
  • The "a socket wins over a per-call fetch" rule is repeated at each request site.
  • Media and decision models are copied without their constructors.

Comment thread packages/ai/src/route/unix-socket.ts Resolved
Comment thread packages/ai/src/route/executor.ts Resolved
Comment thread packages/core/src/model-resolver.ts Resolved
@divinity76
divinity76 force-pushed the provider-unix-socket branch from eed92b5 to 32e07aa Compare November 8, 2026 12:33

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit only rewords the Unix socket paragraph in the docs. It doesn't touch the three earlier points (the untested Node fallback, the socket rule repeated at each request site, and copying models without their constructors), so those threads stay open. The public API is unchanged since the last review: the optional socketPath provider setting and the unixSocketFetch export, both backwards compatible. One small docs correction is inline.

@divinity76
divinity76 force-pushed the provider-unix-socket branch from 32e07aa to f8cdc83 Compare November 8, 2026 12:42

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The earlier points are addressed: the untested node:http fallback is gone (non-Bun runtimes now get a clear rejection, not a TCP fallback); the socket-versus-per-call-fetch rule now lives in one place, RequestExecutor.withTransport, used by every request site; media and decision models are built with their constructors; and the docs now say the socket needs write permission. I reran the unix-socket, aisdk and provider tests in packages/ai and packages/core. All pass, including the production app graph test, where the TCP decoy stays empty. tsc --noEmit passes for both packages. Public API: adds the optional socketPath provider setting (config schema and generated client types) and exports unixSocketFetch from @opencode/ai/route; both are additions, so this is backwards compatible. No further concerns.

@opencodejr

opencodejr Bot commented Nov 8, 2026

Copy link
Copy Markdown

Thanks, I checked all three at f8cdc83f3:

  • RequestExecutor.withTransport now decides in one place that a socket wins over a per-call fetch. All four request sites call it, and transport/http.ts has no socket logic left.
  • fromCatalogMedia now builds the copies with new DecisionModel(...) and new ImageModel(...).
  • The docs now say the socket needs write permission to connect.

The unix-socket, aisdk and provider tests pass, including the production app-graph test, and tsc --noEmit passes for packages/ai and packages/core. I have no further concerns.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Critical transport issues remain: empty socket paths can fall back to TCP, and configured socket paths are stripped from model settings, so normal requests may continue dialing the base URL.

2 open issues

Review effort: Lite

Comment thread packages/core/src/provider.ts Resolved
Comment thread packages/schema/src/provider.ts Resolved

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit is fine: an empty socketPath is now rejected when the config is read (socketPath must not be empty; omit it to use the base URL) instead of being ignored and sending requests to the base URL over TCP. The rest of the push is a merge of dev with no changes specific to this PR. All earlier points were already resolved. I reran the schema config test (including the new empty-path case), the unix-socket tests in packages/ai and packages/core, and the aisdk and provider tests; all pass. tsc --noEmit passes for packages/schema and packages/core. Public API: the optional socketPath provider setting (config schema and generated client types) must now be non-empty when set; the setting is new in this PR, so no existing config breaks. unixSocketFetch is still exported from @opencode/ai/route. No new concerns.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only new commit merges dev into the branch; this PR's own changes are the same as in the version I last reviewed. After the merge, the unix-socket, aisdk and provider tests in packages/ai and packages/core, and the schema config test, all still pass. The public API is unchanged since the last review: the optional, non-empty socketPath provider setting (config schema and generated client types) and the unixSocketFetch export from @opencode/ai/route, both backwards compatible. No new concerns.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A critical active finding remains because configured empty socket paths are still treated as unset, causing incorrect TCP fallback behavior in two locations.

1 open issue
2 resolved since last review

Review effort: Lite

Comment thread packages/core/src/model-resolver.ts Resolved

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit is a small, correct fix. An empty socketPath is no longer treated as unset in the model resolver, so a model with one doesn't keep a websocket transport (which would connect over TCP), and media and decision models pass it on for the socket transport to reject. This is a fallback; the config schema already rejects an empty socketPath. The new model-resolver test is small and clear. I ran the packages/core model-resolver, unix-socket, aisdk and provider tests (all pass), and tsc --noEmit passes for packages/core. No earlier points were open. The public API is unchanged since the last review: the optional, non-empty socketPath provider setting and the unixSocketFetch export from @opencode/ai/route, both backwards compatible. No new concerns.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only new commit merges dev; this PR's own changes are unchanged. The one spot where dev touched nearby code (the media transport in packages/ai/src/route/media.ts) merged cleanly. The unix-socket tests in packages/ai and packages/core, the model-resolver, aisdk and provider tests, and the schema config test all still pass, and tsc --noEmit passes for packages/ai. The public API is unchanged since the last review: the optional, non-empty socketPath provider setting (config schema and generated client types) and the unixSocketFetch export from @opencode/ai/route, both backwards compatible. No new concerns.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants