Skip to content

fix(shared): separate host vs Docker OpenAI base URLs for local LLM - #11

Merged
adelinamart merged 2 commits into
adelinamart:mainfrom
stoichy:main
Jul 24, 2026
Merged

fix(shared): separate host vs Docker OpenAI base URLs for local LLM#11
adelinamart merged 2 commits into
adelinamart:mainfrom
stoichy:main

Conversation

@stoichy

@stoichy stoichy commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Fixes #10

Fix Summary

When running a local OpenAI-compatible server with Perception in Docker, one shared OPENAI_BASE_URL variable cannot work for everyone:

  • Inside Docker, localhost means the container, but you need host.docker.internal to reach the local server on the host.
  • On the host (Sensing MCP, Synthesis), host.docker.internal often breaks Node fetch, while localhost works.

So we were forced to pick one URL and break either Perception or Synthesis.

This change adds an optional host-side URL:

  • OPENAI_BASE_URL, used by Perception (Docker), typically http://host.docker.internal:<port>/v1
  • OPENAI_HOST_BASE_URL, preferred by Sensing / Synthesis on the host, typically http://127.0.0.1:<port>/v1

Sensing and Synthesis call resolveOpenAiBaseUrl(..., { preferHost: true }) (same for embeddings). Perception keeps using OPENAI_BASE_URL only. If OPENAI_HOST_BASE_URL is unset, host processes fall back to OPENAI_BASE_URL (host-only).

resolveEmbeddingConfig now goes through resolveOpenAiBaseUrl as well, so embeddings get the same trimming, empty-string -> default, and localhost -> 127.0.0.1 normalization as the chat path (including Perception in Docker).

CLI: robrain install writes both URLs into Sensing MCP env so a globally installed user is not stuck with only the Docker URL in mcp.json. robrain doctor prints the host-resolved URL. robrain up seeds OPENAI_HOST_BASE_URL. The CLI keeps its own resolver (no @robrain/shared dependency) but mirrors the shared preferHost contract.

BASE_URL vars are still only for optional fully-local mode.

@adelinamart

Copy link
Copy Markdown
Owner

Thanks for this — it solves a real problem cleanly. With a single repo-root .env shared by the whole stack, a fully-local setup under pnpm docker:up currently can't work: Perception (inside Docker) needs host.docker.internal:11434, while Sensing/Synthesis (host processes) need 127.0.0.1:11434, and there's only one OPENAI_BASE_URL to give them. The OPENAI_HOST_BASE_URL + preferHost split lands in exactly the right places — only Postgres and Perception run in containers, so preferHost: true on Sensing and Synthesis and not on Perception is architecturally correct.

I checked out 02b48ad locally: it's based on current main, the full workspace builds, and all tests pass (127/127 in @robrain/shared, everything else green). The || fallback also handles an empty OPENAI_HOST_BASE_URL= line correctly (falls through to OPENAI_BASE_URL), and the two new tests cover both branches. Nice touches: docs went into .env.example comments (our single-source convention) and no dependency/lockfile churn.

Two asks before merge:

1. The CLI needs the same treatment (blocking)

packages/cli/src/lib/editor.ts has a duplicate resolver, resolveOpenAiBaseUrlFromEnv() (line ~141), that only reads OPENAI_BASE_URL. Two consequences in the split setup this PR enables:

  • robrain install bakes the Docker URL into editors' MCP configs. install.ts (line ~348) picks up OPENAI_BASE_URL — which is now the host.docker.internal URL — and editor.ts (line ~212) writes it into the editor's mcp.json env for Sensing, without OPENAI_HOST_BASE_URL. Inside the robrain repo Sensing recovers because loadEnv() reads the repo .env, but a globally-installed user running Sensing in their own project only has the mcp.json env — so Sensing resolves host.docker.internal on the host and every embedding/chat call fails. Fix: when OPENAI_HOST_BASE_URL is set, propagate it into the written env too (or write the host URL as Sensing's OPENAI_BASE_URL).
  • robrain doctor becomes misleading. doctor.ts (lines ~143 and ~155) will print "not needed — local server via OPENAI_BASE_URL (http://host.docker.internal:11434/v1)" on the host, which is exactly the wrong URL to show a user debugging this setup. It should print the host-resolved URL.

While you're in there: consider having the CLI reuse shared's resolveOpenAiBaseUrl instead of keeping the duplicate in editor.ts, so the two can't drift again.

2. Please declare the embedding behavior change in the PR description

resolveEmbeddingConfig previously did its own raw handling of OPENAI_BASE_URL; it now routes through resolveOpenAiBaseUrl, which adds trimming, empty-string → default, and normalizeLoopbackUrl (localhost127.0.0.1) — for all consumers, including Perception in Docker. I'm actually in favor: it finally gives embeddings the same loopback normalization the chat path got in 2.4.3, you updated the pre-existing test honestly, and inside the container both loopback forms behave identically so there's no regression. But it's beyond what the PR title says, so please add a sentence about it to the description for the changelog.

Design alternative I considered (fine to keep your approach)

Instead of a new env var, host processes could auto-rewrite host.docker.internal127.0.0.1 as an extension of the existing normalizeLoopbackUrl. That would be zero-config for users and would fix the CLI gap automatically, since the runtime would repair whatever URL gets baked into mcp.json. I'm OK with the explicit env var — it's easier to reason about and shows up legibly in doctor output — but if you'd rather go the auto-rewrite route instead, I'd accept that too. What I don't want is both.

Minor, no action needed:

  • Perception's container receives OPENAI_HOST_BASE_URL via env_file but never reads it — harmless.
  • A direct unit test on resolveOpenAiBaseUrl({ preferHost }) in a llm test file would be nice, but the embeddings-level tests already exercise both branches, so optional.

Happy to merge once the CLI piece is in.

@adelinamart
adelinamart self-requested a review July 24, 2026 10:07
@stoichy
stoichy force-pushed the main branch 2 times, most recently from ff47495 to 02b48ad Compare July 24, 2026 12:35
…ack env

Sensing MCP configs from robrain install only had the Docker URL, so
global installs failed on the host; doctor also printed host.docker.internal.
@stoichy

stoichy commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Fixed, the CLI now propagates the host URL correctly, and doctor reports it.
Also updated the PR description.

@adelinamart
adelinamart merged commit 379b4f4 into adelinamart:main Jul 24, 2026
1 check passed
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.

Networking Conflict: OpenAI local server unreachable for Docker or Host

2 participants