Reach a host's Ollama from a containerized coder

dda762e5a9d4 · AtlantisPleb · · parent 0af87a65f852

Reach a host's Ollama from a containerized coder

The local lane hardcoded 127.0.0.1:11434 in three places — the reply
source, model resolution, and default discovery — and inside a
container that address is the container. All three now honor the
standard OLLAMA_HOST variable, resolving against the server the
session will actually talk to; the first containerized attempt failed
in resolution ("none are installed") against a host that had the
model. The Gym adapter maps Harbor's ollama/<name> spelling onto the
CLI's --model ollama:<name> local lane, passes the host, and drops the
token requirement there — a local model spends no grant.

Proven by the third leg of the first model comparison: qwen3.8 27B on
the host's own Ollama passed the same graded task as the two cloud
lanes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GoYpb8FEmdxVErsv7ABCYi
Co-Authored-By
Claude Fable 5 <noreply@anthropic.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified bench/adapters/openagents_coder.py
  • modified packages/openagents-cli/src/cli.ts

Diff

2 files changed, +29 -5

bench/adapters/openagents_coder.py modified +19 -3

@@ -58,7 +58,7 @@ class OpenAgentsCoder(BaseInstalledAgent):

58 58
    @property
59 59
    def _token(self) -> str:
60 60
        token = os.environ.get("OPENAGENTS_TOKEN", "")
61
        if not token:
61
        if not token and not self._local_lane:
62 62
            raise ValueError(
63 63
                "OPENAGENTS_TOKEN is not set. The coder's thread lane needs a "
64 64
                "chat:account token for the server at OPENAGENTS_CODER_API_URL."

@@ -70,7 +70,17 @@ class OpenAgentsCoder(BaseInstalledAgent):

70 70
        if not self.model_name:
71 71
            return None
72 72
        # Harbor spells models provider/name; the catalog id is the name.
73
        return self.model_name.split("/", 1)[-1]
73
        # An `ollama/<name>` model selects the coder's local lane instead:
74
        # the CLI's `--model ollama:<name>` shape, answered by an Ollama
75
        # server the container reaches on its host.
76
        provider, _, name = self.model_name.partition("/")
77
        if provider == "ollama":
78
            return f"ollama:{name}"
79
        return name or provider
80
81
    @property
82
    def _local_lane(self) -> bool:
83
        return bool(self.model_name) and self.model_name.startswith("ollama/")
74 84
75 85
    @override
76 86
    async def install(self, environment: BaseEnvironment) -> None:

@@ -136,8 +146,14 @@ class OpenAgentsCoder(BaseInstalledAgent):

136 146
            "exit $status"
137 147
        )
138 148
149
        env = {"OPENAGENTS_TOKEN": self._token}
150
        if self._local_lane:
151
            env["OLLAMA_HOST"] = os.environ.get(
152
                "OPENAGENTS_CODER_OLLAMA_HOST", "http://host.docker.internal:11434"
153
            )
154
139 155
        await self.exec_as_agent(
140 156
            environment,
141 157
            command=command,
142
            env={"OPENAGENTS_TOKEN": self._token},
158
            env=env,
143 159
        )
packages/openagents-cli/src/cli.ts modified +10 -2

@@ -1965,7 +1965,7 @@ const coderCommand = Command.make(

1965 1965
      // coder without asking for one.
1966 1966
      const localModel =
1967 1967
        local && named === undefined && !offline && !resume
1968
          ? yield* Effect.promise(() => discoverOllamaModel())
1968
          ? yield* Effect.promise(() => discoverOllamaModel(process.env["OLLAMA_HOST"] || undefined))
1969 1969
          : undefined;
1970 1970
1971 1971
      if (local && named === undefined && localModel === undefined && !offline && !resume) {

@@ -2016,7 +2016,11 @@ const coderCommand = Command.make(

2016 2016
      // already a real name and needs no round trip.
2017 2017
      const resolved =
2018 2018
        wantsOllama && askedFor !== undefined && named !== undefined
2019
          ? yield* Effect.promise(() => resolveOllamaModel(askedFor))
2019
          ? yield* Effect.promise(() =>
2020
              // Same OLLAMA_HOST honor as the reply source below: resolution
2021
              // must ask the server the session will actually talk to.
2022
              resolveOllamaModel(askedFor, process.env["OLLAMA_HOST"] || undefined),
2023
            )
2020 2024
          : undefined;
2021 2025
2022 2026
      if (resolved !== undefined && resolved.model === undefined) {

@@ -2194,6 +2198,10 @@ const coderCommand = Command.make(

2194 2198
          : wantsOllama && ollamaName !== undefined
2195 2199
            ? new OllamaReplySource({
2196 2200
                model: ollamaName,
2201
                // The standard Ollama env var, honored so a session in a
2202
                // container can reach the Ollama server on its host —
2203
                // 127.0.0.1 inside a container is the container.
2204
                ...(process.env["OLLAMA_HOST"] ? { host: process.env["OLLAMA_HOST"] } : {}),
2197 2205
                ...(Option.isSome(reasoning) ? { reasoning: reasoning.value } : {}),
2198 2206
              })
2199 2207
            : (thread ?? new DummyReplySource());

This page updates live while a promote is in flight · changelog