bots-garden/mini-mepublic Fork
⑂ 81b84a9
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

🐛 mm init / acp-create: list the models from the local address (ticket 214)

The wizard took the engine's resolution: a 2 s probe of /models, then a
silent switch to the container fallback (host.docker.internal). On a Mac
with Docker Desktop, Ollama's models were listed from the container
address. resolveBase now takes AGENT_BASE_URL (engine.EnvBaseURL) when set,
else the first local address whose model list answers (the default, then
the same with 127.0.0.1 for localhost), and the fallback only when none
does. The model question names the address ("Model? (listed from …)").
The written file is unchanged.

Also: tickets 214 and 216 closed; the last task's dates of tickets 210,
212 and 216 moved back from their first comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
agent committed 2026-11-14T19:15:14+01:00 Browse files
81b84a9 parent: 9e1b913
modified .tickets/issues/0210-acp-create-mode-mm-acp-create-mm-init-from-an-acp.yaml +2 -2
@@ -175,6 +175,8 @@ tasks:
175175 name: k33g
176176 email: ph.charriere@gmail.com
177177 createdAt: 2026-11-14T05:41:20.000Z
178+ updatedAt: 2026-11-14T05:56:48.000Z
179+ closedAt: 2026-11-14T05:56:48.000Z
178180 comments:
179181 - id: 1
180182 author:
@@ -194,8 +196,6 @@ comments:
194196 client by hand), the acceptance "tried in two real clients",
195197 permission buttons for choices in conversation mode, and session
196198 config options. qlty not run (not installed in this sandbox).
197- updatedAt: 2026-11-14T05:56:48.000Z
198- closedAt: 2026-11-14T05:56:48.000Z
199199 - id: 2
200200 author:
201201 name: k33g
@@ -175,6 +175,8 @@ tasks:
175 name: k33g175 name: k33g
176 email: ph.charriere@gmail.com176 email: ph.charriere@gmail.com
177 createdAt: 2026-11-14T05:41:20.000Z177 createdAt: 2026-11-14T05:41:20.000Z
178+ updatedAt: 2026-11-14T05:56:48.000Z
179+ closedAt: 2026-11-14T05:56:48.000Z
178 comments:180 comments:
179 - id: 1181 - id: 1
180 author:182 author:
@@ -194,8 +196,6 @@ comments:
194 client by hand), the acceptance "tried in two real clients",196 client by hand), the acceptance "tried in two real clients",
195 permission buttons for choices in conversation mode, and session197 permission buttons for choices in conversation mode, and session
196 config options. qlty not run (not installed in this sandbox).198 config options. qlty not run (not installed in this sandbox).
197- updatedAt: 2026-11-14T05:56:48.000Z
198- closedAt: 2026-11-14T05:56:48.000Z
199 - id: 2199 - id: 2
200 author:200 author:
201 name: k33g201 name: k33g
modified .tickets/issues/0212-mm-dockerinit-know-the-mm-folder-project-layout.yaml +2 -2
@@ -137,6 +137,8 @@ tasks:
137137 name: k33g
138138 email: ph.charriere@gmail.com
139139 createdAt: 2026-11-14T06:29:41.000Z
140+ updatedAt: 2026-11-14T06:37:57.000Z
141+ closedAt: 2026-11-14T06:37:57.000Z
140142 comments:
141143 - id: 1
142144 author:
@@ -159,8 +161,6 @@ comments:
159161 Also asked in the same session: `mm sbxinit` now prints
160162 `Run the sandbox <name>: sbx env run --force` (and the line in the
161163 sbxenv.yaml comments).
162- updatedAt: 2026-11-14T06:37:57.000Z
163- closedAt: 2026-11-14T06:37:57.000Z
164164 - id: 2
165165 author:
166166 name: k33g
@@ -137,6 +137,8 @@ tasks:
137 name: k33g137 name: k33g
138 email: ph.charriere@gmail.com138 email: ph.charriere@gmail.com
139 createdAt: 2026-11-14T06:29:41.000Z139 createdAt: 2026-11-14T06:29:41.000Z
140+ updatedAt: 2026-11-14T06:37:57.000Z
141+ closedAt: 2026-11-14T06:37:57.000Z
140 comments:142 comments:
141 - id: 1143 - id: 1
142 author:144 author:
@@ -159,8 +161,6 @@ comments:
159 Also asked in the same session: `mm sbxinit` now prints161 Also asked in the same session: `mm sbxinit` now prints
160 `Run the sandbox <name>: sbx env run --force` (and the line in the162 `Run the sandbox <name>: sbx env run --force` (and the line in the
161 sbxenv.yaml comments).163 sbxenv.yaml comments).
162- updatedAt: 2026-11-14T06:37:57.000Z
163- closedAt: 2026-11-14T06:37:57.000Z
164 - id: 2164 - id: 2
165 author:165 author:
166 name: k33g166 name: k33g
modified .tickets/issues/0214-mm-acp-create-start-ollama-models-are-listed-fro.yaml +63 -7
@@ -1,12 +1,12 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 214
33 title: "mm acp-create (/start): Ollama models are listed from the container address instead of the local one"
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-11-14T07:40:50.000Z
9-updatedAt: 2026-11-14T07:47:59.000Z
9+updatedAt: 2026-11-14T18:14:17.000Z
1010 labels:
1111 - bug
1212 - kind::bug
@@ -76,44 +76,54 @@ body: |
7676 tasks:
7777 - id: 1
7878 title: Reproduce - mm acp-create from Zed and mm init in a terminal, Ollama on the host, note the address used
79- state: open
79+ state: closed
8080 priority: high
8181 author:
8282 name: k33g
8383 email: ph.charriere@gmail.com
8484 createdAt: 2026-11-14T07:40:50.000Z
85+ updatedAt: 2026-11-14T18:14:17.000Z
86+ closedAt: 2026-11-14T18:14:17.000Z
8587 - id: 2
8688 title: Find why the local address is not taken (probe timeout, localhost vs 127.0.0.1 / IPv6, environment of the editor's process)
87- state: open
89+ state: closed
8890 priority: high
8991 author:
9092 name: k33g
9193 email: ph.charriere@gmail.com
9294 createdAt: 2026-11-14T07:40:50.000Z
95+ updatedAt: 2026-11-14T18:14:17.000Z
96+ closedAt: 2026-11-14T18:14:17.000Z
9397 - id: 3
9498 title: Fix - the wizard lists the models from the local address when mm is not in a container
95- state: open
99+ state: closed
96100 priority: high
97101 author:
98102 name: k33g
99103 email: ph.charriere@gmail.com
100104 createdAt: 2026-11-14T07:40:50.000Z
105+ updatedAt: 2026-11-14T17:54:42.000Z
106+ closedAt: 2026-11-14T17:54:42.000Z
101107 - id: 4
102108 title: Show the address the models were listed from in the model question (both modes)
103- state: open
109+ state: closed
104110 priority: medium
105111 author:
106112 name: k33g
107113 email: ph.charriere@gmail.com
108114 createdAt: 2026-11-14T07:40:50.000Z
115+ updatedAt: 2026-11-14T17:54:42.000Z
116+ closedAt: 2026-11-14T17:54:42.000Z
109117 - id: 5
110118 title: Tests - a local server answering slowly or only on 127.0.0.1 keeps the local address
111- state: open
119+ state: closed
112120 priority: medium
113121 author:
114122 name: k33g
115123 email: ph.charriere@gmail.com
116124 createdAt: 2026-11-14T07:40:50.000Z
125+ updatedAt: 2026-11-14T17:54:42.000Z
126+ closedAt: 2026-11-14T17:54:42.000Z
117127 comments:
118128 - id: 1
119129 author:
@@ -128,3 +138,49 @@ comments:
128138 it: look at what is specific to Ollama — its `/v1/models` answering
129139 later than 2 s or not 200, Ollama listening on IPv4 only while DMR
130140 answers on both, or `OLLAMA_HOST` on the user's machine.
141+ - id: 2
142+ author:
143+ name: k33g
144+ email: ph.charriere@gmail.com
145+ createdAt: 2026-11-14T17:54:42.000Z
146+ body: |
147+ Fixed on `fix/wizard-local-address` (uncommitted), without waiting for
148+ the exact cause on the user's machine: the fix covers hypotheses 1
149+ (slow probe), 2 (IPv4 only) and 4 (Docker Desktop resolving
150+ host.docker.internal on the host, which hid the failed probe).
151+
152+ - The wizard (mm init and mm acp-create) no longer takes the engine's
153+ resolution (2 s probe of `/models`, then a silent switch to the
154+ container fallback). `resolveBase` → `AGENT_BASE_URL` (or the
155+ legacy variable) when set; else the first local address whose model
156+ list answers (the provider's own list endpoint, `/api/tags` for
157+ Ollama, 5 s): the default, then the same with `127.0.0.1` when it
158+ names `localhost`; the fallback only when none answers; a provider
159+ without a fallback is not probed. `engine.EnvBaseURL` added.
160+ - The model question names the address: `Model? (listed from …)`.
161+ - The written file is unchanged (no `baseUrl` for the default).
162+ - Runtime (`mm acp` with the written file) still uses the engine's
163+ resolution — not touched here.
164+
165+ Tests: `TestChooseBasePrefersALocalAddress`, `TestLocalAddresses`,
166+ `TestResolveBaseKeepsASlowLocalServer` (a server whose `/v1/models`
167+ takes 3 s: the engine's probe takes the fallback, the wizard keeps the
168+ local address), `TestResolveBaseHonoursAgentBaseURL`.
169+
170+ End to end, real binary, `mm acp-create --provider ollama`, a fake
171+ Ollama on 127.0.0.1:11434 whose `/v1/models` takes 3 s: before →
172+ `Model ID? (no list from http://host.docker.internal:11434/v1 …)`;
173+ after → `Model? (listed from http://localhost:11434/v1)`.
174+
175+ Not done: tasks 1 and 2 (reproduce on the user's Mac with the real
176+ Ollama, and name the exact cause) — the curl checks given in the
177+ conversation would tell.
178+ - id: 3
179+ author:
180+ name: k33g
181+ email: ph.charriere@gmail.com
182+ createdAt: 2026-11-14T18:14:17.000Z
183+ body: |
184+ Tested by the user with the real Ollama: it works (local models
185+ listed). Closed. The exact cause on the user's machine was not named
186+ (tasks 1–2 closed with the user's test as the reproduction).
@@ -1,12 +1,12 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 2142 id: 214
3 title: "mm acp-create (/start): Ollama models are listed from the container address instead of the local one"3 title: "mm acp-create (/start): Ollama models are listed from the container address instead of the local one"
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-11-14T07:40:50.000Z8 createdAt: 2026-11-14T07:40:50.000Z
9-updatedAt: 2026-11-14T07:47:59.000Z9+updatedAt: 2026-11-14T18:14:17.000Z
10 labels:10 labels:
11 - bug11 - bug
12 - kind::bug12 - kind::bug
@@ -76,44 +76,54 @@ body: |
76 tasks:76 tasks:
77 - id: 177 - id: 1
78 title: Reproduce - mm acp-create from Zed and mm init in a terminal, Ollama on the host, note the address used78 title: Reproduce - mm acp-create from Zed and mm init in a terminal, Ollama on the host, note the address used
79- state: open79+ state: closed
80 priority: high80 priority: high
81 author:81 author:
82 name: k33g82 name: k33g
83 email: ph.charriere@gmail.com83 email: ph.charriere@gmail.com
84 createdAt: 2026-11-14T07:40:50.000Z84 createdAt: 2026-11-14T07:40:50.000Z
85+ updatedAt: 2026-11-14T18:14:17.000Z
86+ closedAt: 2026-11-14T18:14:17.000Z
85 - id: 287 - id: 2
86 title: Find why the local address is not taken (probe timeout, localhost vs 127.0.0.1 / IPv6, environment of the editor's process)88 title: Find why the local address is not taken (probe timeout, localhost vs 127.0.0.1 / IPv6, environment of the editor's process)
87- state: open89+ state: closed
88 priority: high90 priority: high
89 author:91 author:
90 name: k33g92 name: k33g
91 email: ph.charriere@gmail.com93 email: ph.charriere@gmail.com
92 createdAt: 2026-11-14T07:40:50.000Z94 createdAt: 2026-11-14T07:40:50.000Z
95+ updatedAt: 2026-11-14T18:14:17.000Z
96+ closedAt: 2026-11-14T18:14:17.000Z
93 - id: 397 - id: 3
94 title: Fix - the wizard lists the models from the local address when mm is not in a container98 title: Fix - the wizard lists the models from the local address when mm is not in a container
95- state: open99+ state: closed
96 priority: high100 priority: high
97 author:101 author:
98 name: k33g102 name: k33g
99 email: ph.charriere@gmail.com103 email: ph.charriere@gmail.com
100 createdAt: 2026-11-14T07:40:50.000Z104 createdAt: 2026-11-14T07:40:50.000Z
105+ updatedAt: 2026-11-14T17:54:42.000Z
106+ closedAt: 2026-11-14T17:54:42.000Z
101 - id: 4107 - id: 4
102 title: Show the address the models were listed from in the model question (both modes)108 title: Show the address the models were listed from in the model question (both modes)
103- state: open109+ state: closed
104 priority: medium110 priority: medium
105 author:111 author:
106 name: k33g112 name: k33g
107 email: ph.charriere@gmail.com113 email: ph.charriere@gmail.com
108 createdAt: 2026-11-14T07:40:50.000Z114 createdAt: 2026-11-14T07:40:50.000Z
115+ updatedAt: 2026-11-14T17:54:42.000Z
116+ closedAt: 2026-11-14T17:54:42.000Z
109 - id: 5117 - id: 5
110 title: Tests - a local server answering slowly or only on 127.0.0.1 keeps the local address118 title: Tests - a local server answering slowly or only on 127.0.0.1 keeps the local address
111- state: open119+ state: closed
112 priority: medium120 priority: medium
113 author:121 author:
114 name: k33g122 name: k33g
115 email: ph.charriere@gmail.com123 email: ph.charriere@gmail.com
116 createdAt: 2026-11-14T07:40:50.000Z124 createdAt: 2026-11-14T07:40:50.000Z
125+ updatedAt: 2026-11-14T17:54:42.000Z
126+ closedAt: 2026-11-14T17:54:42.000Z
117 comments:127 comments:
118 - id: 1128 - id: 1
119 author:129 author:
@@ -128,3 +138,49 @@ comments:
128 it: look at what is specific to Ollama — its `/v1/models` answering138 it: look at what is specific to Ollama — its `/v1/models` answering
129 later than 2 s or not 200, Ollama listening on IPv4 only while DMR139 later than 2 s or not 200, Ollama listening on IPv4 only while DMR
130 answers on both, or `OLLAMA_HOST` on the user's machine.140 answers on both, or `OLLAMA_HOST` on the user's machine.
141+ - id: 2
142+ author:
143+ name: k33g
144+ email: ph.charriere@gmail.com
145+ createdAt: 2026-11-14T17:54:42.000Z
146+ body: |
147+ Fixed on `fix/wizard-local-address` (uncommitted), without waiting for
148+ the exact cause on the user's machine: the fix covers hypotheses 1
149+ (slow probe), 2 (IPv4 only) and 4 (Docker Desktop resolving
150+ host.docker.internal on the host, which hid the failed probe).
151+
152+ - The wizard (mm init and mm acp-create) no longer takes the engine's
153+ resolution (2 s probe of `/models`, then a silent switch to the
154+ container fallback). `resolveBase` → `AGENT_BASE_URL` (or the
155+ legacy variable) when set; else the first local address whose model
156+ list answers (the provider's own list endpoint, `/api/tags` for
157+ Ollama, 5 s): the default, then the same with `127.0.0.1` when it
158+ names `localhost`; the fallback only when none answers; a provider
159+ without a fallback is not probed. `engine.EnvBaseURL` added.
160+ - The model question names the address: `Model? (listed from …)`.
161+ - The written file is unchanged (no `baseUrl` for the default).
162+ - Runtime (`mm acp` with the written file) still uses the engine's
163+ resolution — not touched here.
164+
165+ Tests: `TestChooseBasePrefersALocalAddress`, `TestLocalAddresses`,
166+ `TestResolveBaseKeepsASlowLocalServer` (a server whose `/v1/models`
167+ takes 3 s: the engine's probe takes the fallback, the wizard keeps the
168+ local address), `TestResolveBaseHonoursAgentBaseURL`.
169+
170+ End to end, real binary, `mm acp-create --provider ollama`, a fake
171+ Ollama on 127.0.0.1:11434 whose `/v1/models` takes 3 s: before →
172+ `Model ID? (no list from http://host.docker.internal:11434/v1 …)`;
173+ after → `Model? (listed from http://localhost:11434/v1)`.
174+
175+ Not done: tasks 1 and 2 (reproduce on the user's Mac with the real
176+ Ollama, and name the exact cause) — the curl checks given in the
177+ conversation would tell.
178+ - id: 3
179+ author:
180+ name: k33g
181+ email: ph.charriere@gmail.com
182+ createdAt: 2026-11-14T18:14:17.000Z
183+ body: |
184+ Tested by the user with the real Ollama: it works (local models
185+ listed). Closed. The exact cause on the user's machine was not named
186+ (tasks 1–2 closed with the user's test as the reproduction).
modified .tickets/issues/0216-mm-dockerinit-i-have-no-name-in-the-container-th.yaml +11 -4
@@ -1,12 +1,12 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 216
33 title: "mm dockerinit: \"I have no name!\" in the container - the mm account has uid 1000, compose runs as the user's uid"
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-11-14T17:31:27.000Z
9-updatedAt: 2026-11-14T17:35:14.000Z
9+updatedAt: 2026-11-14T17:50:32.000Z
1010 labels:
1111 - bug
1212 - kind::bug
@@ -143,6 +143,8 @@ tasks:
143143 name: k33g
144144 email: ph.charriere@gmail.com
145145 createdAt: 2026-11-14T17:31:27.000Z
146+ updatedAt: 2026-11-14T17:35:14.000Z
147+ closedAt: 2026-11-14T17:35:14.000Z
146148 comments:
147149 - id: 1
148150 author:
@@ -170,5 +172,10 @@ comments:
170172 identity. Unrelated to the fix — the docs say to set `user.name` /
171173 `user.email` or the `GIT_*` variables. Not tried: `mm tui` in the
172174 container (no terminal here), ssh, Docker Desktop on macOS.
173- updatedAt: 2026-11-14T17:35:14.000Z
174- closedAt: 2026-11-14T17:35:14.000Z
175+ - id: 2
176+ author:
177+ name: k33g
178+ email: ph.charriere@gmail.com
179+ createdAt: 2026-11-14T17:50:32.000Z
180+ body: |
181+ Closed at the user's request.
@@ -1,12 +1,12 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 2162 id: 216
3 title: "mm dockerinit: \"I have no name!\" in the container - the mm account has uid 1000, compose runs as the user's uid"3 title: "mm dockerinit: \"I have no name!\" in the container - the mm account has uid 1000, compose runs as the user's uid"
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-11-14T17:31:27.000Z8 createdAt: 2026-11-14T17:31:27.000Z
9-updatedAt: 2026-11-14T17:35:14.000Z9+updatedAt: 2026-11-14T17:50:32.000Z
10 labels:10 labels:
11 - bug11 - bug
12 - kind::bug12 - kind::bug
@@ -143,6 +143,8 @@ tasks:
143 name: k33g143 name: k33g
144 email: ph.charriere@gmail.com144 email: ph.charriere@gmail.com
145 createdAt: 2026-11-14T17:31:27.000Z145 createdAt: 2026-11-14T17:31:27.000Z
146+ updatedAt: 2026-11-14T17:35:14.000Z
147+ closedAt: 2026-11-14T17:35:14.000Z
146 comments:148 comments:
147 - id: 1149 - id: 1
148 author:150 author:
@@ -170,5 +172,10 @@ comments:
170 identity. Unrelated to the fix — the docs say to set `user.name` /172 identity. Unrelated to the fix — the docs say to set `user.name` /
171 `user.email` or the `GIT_*` variables. Not tried: `mm tui` in the173 `user.email` or the `GIT_*` variables. Not tried: `mm tui` in the
172 container (no terminal here), ssh, Docker Desktop on macOS.174 container (no terminal here), ssh, Docker Desktop on macOS.
173- updatedAt: 2026-11-14T17:35:14.000Z175+ - id: 2
174- closedAt: 2026-11-14T17:35:14.000Z176+ author:
177+ name: k33g
178+ email: ph.charriere@gmail.com
179+ createdAt: 2026-11-14T17:50:32.000Z
180+ body: |
181+ Closed at the user's request.
modified docs/en/reference/cli.md +1 -1
@@ -240,7 +240,7 @@ An agent file outside the workspace only gives a warning: see [when the agent fi
240240 |------|----------------------|---------|
241241 | `--project yes\|no` | Keep everything in a `.mm` folder? — asked first. `yes`: the file is `.mm/agent.yaml` (the folder is created), its paths are written from the project root (`skillsDir: .mm/skills`), and `mm` / `mm tui` started there find it with no argument | `no` |
242242 | `--provider` | Model server: `llmman`, `ollama`, `dmr`, `llamacpp`, `kronk`, `openai` | the first local one whose server answers, else `llmman` (`openai` is never probed: its default address is the OpenAI API) |
243-| `--base-url` | `openai` only: Endpoint — the service's OpenAI-compatible address (OpenAI, Cerebras, Hugging Face…); for the other providers, not asked | `https://api.openai.com/v1` for `openai`; otherwise the provider's address, or its fallback when it does not answer. Written as `baseUrl` unless it is the default |
243+| `--base-url` | `openai` only: Endpoint — the service's OpenAI-compatible address (OpenAI, Cerebras, Hugging Face…); for the other providers, not asked | `https://api.openai.com/v1` for `openai`; otherwise `AGENT_BASE_URL` when set, else the first of the provider's local addresses whose model list answers — its default, then the same with `127.0.0.1` when the default names `localhost` — and its container fallback (`host.docker.internal`) only when none does. The model question names the address its list comes from (`Model? (listed from …)`). Written as `baseUrl` unless it is the default |
244244 | `--api-key-env` | `openai` only: Environment variable that holds the API key | `OPENAI_API_KEY`. Written as `apiKeyEnv` unless it is the default |
245245 | `--model` | Model: a list of the server's models (`/api/tags` for llmman and Ollama, `/models` for the others; 5 s for a local server, 20 s for `openai`), or a name to type when there is no list — the question says why (`<VARIABLE> is not set: no list`, or the server's error). For `openai` the list is asked with the key read from the variable named above, and not asked at all when that variable is empty | the first of the list |
246246 | `--family` | Model family | detected from the model's name, then from the server's family name |
@@ -240,7 +240,7 @@ An agent file outside the workspace only gives a warning: see [when the agent fi
240 |------|----------------------|---------|240 |------|----------------------|---------|
241 | `--project yes\|no` | Keep everything in a `.mm` folder? — asked first. `yes`: the file is `.mm/agent.yaml` (the folder is created), its paths are written from the project root (`skillsDir: .mm/skills`), and `mm` / `mm tui` started there find it with no argument | `no` |241 | `--project yes\|no` | Keep everything in a `.mm` folder? — asked first. `yes`: the file is `.mm/agent.yaml` (the folder is created), its paths are written from the project root (`skillsDir: .mm/skills`), and `mm` / `mm tui` started there find it with no argument | `no` |
242 | `--provider` | Model server: `llmman`, `ollama`, `dmr`, `llamacpp`, `kronk`, `openai` | the first local one whose server answers, else `llmman` (`openai` is never probed: its default address is the OpenAI API) |242 | `--provider` | Model server: `llmman`, `ollama`, `dmr`, `llamacpp`, `kronk`, `openai` | the first local one whose server answers, else `llmman` (`openai` is never probed: its default address is the OpenAI API) |
243-| `--base-url` | `openai` only: Endpoint — the service's OpenAI-compatible address (OpenAI, Cerebras, Hugging Face…); for the other providers, not asked | `https://api.openai.com/v1` for `openai`; otherwise the provider's address, or its fallback when it does not answer. Written as `baseUrl` unless it is the default |243+| `--base-url` | `openai` only: Endpoint — the service's OpenAI-compatible address (OpenAI, Cerebras, Hugging Face…); for the other providers, not asked | `https://api.openai.com/v1` for `openai`; otherwise `AGENT_BASE_URL` when set, else the first of the provider's local addresses whose model list answers — its default, then the same with `127.0.0.1` when the default names `localhost` — and its container fallback (`host.docker.internal`) only when none does. The model question names the address its list comes from (`Model? (listed from …)`). Written as `baseUrl` unless it is the default |
244 | `--api-key-env` | `openai` only: Environment variable that holds the API key | `OPENAI_API_KEY`. Written as `apiKeyEnv` unless it is the default |244 | `--api-key-env` | `openai` only: Environment variable that holds the API key | `OPENAI_API_KEY`. Written as `apiKeyEnv` unless it is the default |
245 | `--model` | Model: a list of the server's models (`/api/tags` for llmman and Ollama, `/models` for the others; 5 s for a local server, 20 s for `openai`), or a name to type when there is no list — the question says why (`<VARIABLE> is not set: no list`, or the server's error). For `openai` the list is asked with the key read from the variable named above, and not asked at all when that variable is empty | the first of the list |245 | `--model` | Model: a list of the server's models (`/api/tags` for llmman and Ollama, `/models` for the others; 5 s for a local server, 20 s for `openai`), or a name to type when there is no list — the question says why (`<VARIABLE> is not set: no list`, or the server's error). For `openai` the list is asked with the key read from the variable named above, and not asked at all when that variable is empty | the first of the list |
246 | `--family` | Model family | detected from the model's name, then from the server's family name |246 | `--family` | Model family | detected from the model's name, then from the server's family name |
modified docs/fr/reference/cli.md +1 -1
@@ -240,7 +240,7 @@ Un fichier d'agent hors du workspace ne donne qu'un avertissement : voir [quand
240240 |--------|---------------------------------|--------|
241241 | `--project yes\|no` | Tout ranger dans un dossier `.mm` ? — posée en premier. `yes` : le fichier est `.mm/agent.yaml` (le dossier est créé), ses chemins sont écrits depuis la racine du projet (`skillsDir: .mm/skills`), et `mm` / `mm tui` lancés là le trouvent sans argument | `no` |
242242 | `--provider` | Serveur de modèles : `llmman`, `ollama`, `dmr`, `llamacpp`, `kronk`, `openai` | le premier serveur local qui répond, sinon `llmman` (`openai` n'est jamais sondé : son adresse par défaut est l'API d'OpenAI) |
243-| `--base-url` | `openai` seulement : Endpoint — l'adresse compatible OpenAI du service (OpenAI, Cerebras, Hugging Face…) ; pour les autres fournisseurs, pas posée | `https://api.openai.com/v1` pour `openai` ; sinon l'adresse du fournisseur, ou son adresse de secours quand elle ne répond pas. Écrite comme `baseUrl` sauf si c'est la valeur par défaut |
243+| `--base-url` | `openai` seulement : Endpoint — l'adresse compatible OpenAI du service (OpenAI, Cerebras, Hugging Face…) ; pour les autres fournisseurs, pas posée | `https://api.openai.com/v1` pour `openai` ; sinon `AGENT_BASE_URL` s'il est défini, sinon la première des adresses locales du fournisseur dont la liste de modèles répond — son adresse par défaut, puis la même avec `127.0.0.1` quand elle nomme `localhost` — et son adresse de secours pour conteneur (`host.docker.internal`) seulement quand aucune ne répond. La question du modèle nomme l'adresse d'où vient sa liste (`Model? (listed from …)`). Écrite comme `baseUrl` sauf si c'est la valeur par défaut |
244244 | `--api-key-env` | `openai` seulement : variable d'environnement qui contient la clé d'API | `OPENAI_API_KEY`. Écrite comme `apiKeyEnv` sauf si c'est la valeur par défaut |
245245 | `--model` | Modèle : la liste des modèles du serveur (`/api/tags` pour llmman et Ollama, `/models` pour les autres ; 5 s pour un serveur local, 20 s pour `openai`), ou un nom à taper quand il n'y a pas de liste — la question dit pourquoi (`<VARIABLE> is not set: no list`, ou l'erreur du serveur). Pour `openai`, la liste est demandée avec la clé lue dans la variable nommée plus haut, et pas demandée du tout quand cette variable est vide | le premier de la liste |
246246 | `--family` | Famille du modèle | détectée d'après le nom du modèle, puis d'après le nom de famille donné par le serveur |
@@ -240,7 +240,7 @@ Un fichier d'agent hors du workspace ne donne qu'un avertissement : voir [quand
240 |--------|---------------------------------|--------|240 |--------|---------------------------------|--------|
241 | `--project yes\|no` | Tout ranger dans un dossier `.mm` ? — posée en premier. `yes` : le fichier est `.mm/agent.yaml` (le dossier est créé), ses chemins sont écrits depuis la racine du projet (`skillsDir: .mm/skills`), et `mm` / `mm tui` lancés là le trouvent sans argument | `no` |241 | `--project yes\|no` | Tout ranger dans un dossier `.mm` ? — posée en premier. `yes` : le fichier est `.mm/agent.yaml` (le dossier est créé), ses chemins sont écrits depuis la racine du projet (`skillsDir: .mm/skills`), et `mm` / `mm tui` lancés là le trouvent sans argument | `no` |
242 | `--provider` | Serveur de modèles : `llmman`, `ollama`, `dmr`, `llamacpp`, `kronk`, `openai` | le premier serveur local qui répond, sinon `llmman` (`openai` n'est jamais sondé : son adresse par défaut est l'API d'OpenAI) |242 | `--provider` | Serveur de modèles : `llmman`, `ollama`, `dmr`, `llamacpp`, `kronk`, `openai` | le premier serveur local qui répond, sinon `llmman` (`openai` n'est jamais sondé : son adresse par défaut est l'API d'OpenAI) |
243-| `--base-url` | `openai` seulement : Endpoint — l'adresse compatible OpenAI du service (OpenAI, Cerebras, Hugging Face…) ; pour les autres fournisseurs, pas posée | `https://api.openai.com/v1` pour `openai` ; sinon l'adresse du fournisseur, ou son adresse de secours quand elle ne répond pas. Écrite comme `baseUrl` sauf si c'est la valeur par défaut |243+| `--base-url` | `openai` seulement : Endpoint — l'adresse compatible OpenAI du service (OpenAI, Cerebras, Hugging Face…) ; pour les autres fournisseurs, pas posée | `https://api.openai.com/v1` pour `openai` ; sinon `AGENT_BASE_URL` s'il est défini, sinon la première des adresses locales du fournisseur dont la liste de modèles répond — son adresse par défaut, puis la même avec `127.0.0.1` quand elle nomme `localhost` — et son adresse de secours pour conteneur (`host.docker.internal`) seulement quand aucune ne répond. La question du modèle nomme l'adresse d'où vient sa liste (`Model? (listed from …)`). Écrite comme `baseUrl` sauf si c'est la valeur par défaut |
244 | `--api-key-env` | `openai` seulement : variable d'environnement qui contient la clé d'API | `OPENAI_API_KEY`. Écrite comme `apiKeyEnv` sauf si c'est la valeur par défaut |244 | `--api-key-env` | `openai` seulement : variable d'environnement qui contient la clé d'API | `OPENAI_API_KEY`. Écrite comme `apiKeyEnv` sauf si c'est la valeur par défaut |
245 | `--model` | Modèle : la liste des modèles du serveur (`/api/tags` pour llmman et Ollama, `/models` pour les autres ; 5 s pour un serveur local, 20 s pour `openai`), ou un nom à taper quand il n'y a pas de liste — la question dit pourquoi (`<VARIABLE> is not set: no list`, ou l'erreur du serveur). Pour `openai`, la liste est demandée avec la clé lue dans la variable nommée plus haut, et pas demandée du tout quand cette variable est vide | le premier de la liste |245 | `--model` | Modèle : la liste des modèles du serveur (`/api/tags` pour llmman et Ollama, `/models` pour les autres ; 5 s pour un serveur local, 20 s pour `openai`), ou un nom à taper quand il n'y a pas de liste — la question dit pourquoi (`<VARIABLE> is not set: no list`, ou l'erreur du serveur). Pour `openai`, la liste est demandée avec la clé lue dans la variable nommée plus haut, et pas demandée du tout quand cette variable est vide | le premier de la liste |
246 | `--family` | Famille du modèle | détectée d'après le nom du modèle, puis d'après le nom de famille donné par le serveur |246 | `--family` | Famille du modèle | détectée d'après le nom du modèle, puis d'après le nom de famille donné par le serveur |
modified internal/agentinit/flow.go +5 -2
@@ -222,6 +222,9 @@ func (f *Flow) Step(key string) Step {
222222 st.Kind, st.Prompt = elicit.Text, f.typeModelPrompt()
223223 break
224224 }
225+ // Where the list comes from (ticket 214): a wrong address — the
226+ // container one on the host — shows at once.
227+ st.Prompt = fmt.Sprintf("Model? (listed from %s)", f.Base())
225228 for _, m := range models {
226229 st.Options = append(st.Options, Option{Value: m, Label: m, Detail: modelDetail(f.c.Provider, m)})
227230 }
@@ -578,7 +581,7 @@ func (f *Flow) Result() (Choices, Facts, string) {
578581 // Base is the resolved server address ("" before a provider is chosen).
579582 func (f *Flow) Base() string {
580583 if f.base == "" && f.c.Provider != "" {
581- f.base = resolveBase(f.c)
584+ f.base = resolveBase(f.ctx, f.c)
582585 }
583586 return f.base
584587 }
@@ -628,7 +631,7 @@ func (f *Flow) answers(name string) bool {
628631 if f.reachable == nil {
629632 f.reachable = map[string]bool{}
630633 for _, p := range local {
631- _, err := Models(f.ctx, p, resolveBase(Choices{Provider: p}))
634+ _, err := Models(f.ctx, p, resolveBase(f.ctx, Choices{Provider: p}))
632635 f.reachable[p] = err == nil
633636 }
634637 }
@@ -222,6 +222,9 @@ func (f *Flow) Step(key string) Step {
222 st.Kind, st.Prompt = elicit.Text, f.typeModelPrompt()222 st.Kind, st.Prompt = elicit.Text, f.typeModelPrompt()
223 break223 break
224 }224 }
225+ // Where the list comes from (ticket 214): a wrong address — the
226+ // container one on the host — shows at once.
227+ st.Prompt = fmt.Sprintf("Model? (listed from %s)", f.Base())
225 for _, m := range models {228 for _, m := range models {
226 st.Options = append(st.Options, Option{Value: m, Label: m, Detail: modelDetail(f.c.Provider, m)})229 st.Options = append(st.Options, Option{Value: m, Label: m, Detail: modelDetail(f.c.Provider, m)})
227 }230 }
@@ -578,7 +581,7 @@ func (f *Flow) Result() (Choices, Facts, string) {
578 // Base is the resolved server address ("" before a provider is chosen).581 // Base is the resolved server address ("" before a provider is chosen).
579 func (f *Flow) Base() string {582 func (f *Flow) Base() string {
580 if f.base == "" && f.c.Provider != "" {583 if f.base == "" && f.c.Provider != "" {
581- f.base = resolveBase(f.c)584+ f.base = resolveBase(f.ctx, f.c)
582 }585 }
583 return f.base586 return f.base
584 }587 }
@@ -628,7 +631,7 @@ func (f *Flow) answers(name string) bool {
628 if f.reachable == nil {631 if f.reachable == nil {
629 f.reachable = map[string]bool{}632 f.reachable = map[string]bool{}
630 for _, p := range local {633 for _, p := range local {
631- _, err := Models(f.ctx, p, resolveBase(Choices{Provider: p}))634+ _, err := Models(f.ctx, p, resolveBase(f.ctx, Choices{Provider: p}))
632 f.reachable[p] = err == nil635 f.reachable[p] = err == nil
633 }636 }
634 }637 }
modified internal/agentinit/wizard.go +61 -3
@@ -367,12 +367,70 @@ func (w *lineWizard) ask(st Step) string {
367367 }
368368 }
369369
370-// resolveBase is the server's address: the flag's, else the provider's
371-// default (or its fallback, when the default does not answer).
372-func resolveBase(c Choices) string {
370+// resolveBase is the server's address the wizard lists models from: the
371+// flag's, else the one the environment imposes (AGENT_BASE_URL…), else the
372+// first that answers of the provider's local addresses, then its fallback
373+// (chooseBase).
374+//
375+// Ticket 214: it used to take the engine's own resolution, whose probe gives
376+// the default address 2 s on /models and switches to the container fallback
377+// (host.docker.internal) without a word. mm on a Mac with Docker Desktop —
378+// where that name resolves on the host too — then listed Ollama's models
379+// from the container address. The written file is not concerned: it carries
380+// no baseUrl for the default address.
381+func resolveBase(ctx context.Context, c Choices) string {
373382 if c.BaseURL != "" {
374383 return c.BaseURL
375384 }
385+ if env := engine.EnvBaseURL(c.Provider); env != "" {
386+ return env
387+ }
388+ if base, fallback, ok := engine.DefaultURLs(c.Provider); ok {
389+ return chooseBase(base, fallback, func(url string) bool {
390+ _, err := Models(ctx, c.Provider, url)
391+ return err == nil
392+ })
393+ }
394+ return engineBase(c)
395+}
396+
397+// chooseBase picks the address to list models from: the first that lists
398+// them among the local ones (localAddresses), else the fallback when it
399+// does, else the default — whose error the model question then shows. A
400+// provider without a fallback (llamacpp, openai) is not probed at all: there
401+// is nothing to choose, and openai's default is a remote service nobody
402+// asked mm to call yet.
403+func chooseBase(base, fallback string, lists func(string) bool) string {
404+ if fallback == "" {
405+ return base
406+ }
407+ for _, url := range localAddresses(base) {
408+ if lists(url) {
409+ return url
410+ }
411+ }
412+ if lists(fallback) {
413+ return fallback
414+ }
415+ return base
416+}
417+
418+// localAddresses is the default address, then — when it names localhost —
419+// the same with 127.0.0.1: a server listening on IPv4 only does not answer
420+// a localhost that resolves to ::1 first.
421+//
422+// localAddresses("http://localhost:11434/v1") // [http://localhost:11434/v1 http://127.0.0.1:11434/v1]
423+func localAddresses(base string) []string {
424+ out := []string{base}
425+ if strings.Contains(base, "://localhost:") || strings.HasSuffix(base, "://localhost") {
426+ out = append(out, strings.Replace(base, "://localhost", "://127.0.0.1", 1))
427+ }
428+ return out
429+}
430+
431+// engineBase is the engine's own resolution, for a provider the wizard has
432+// no addresses for.
433+func engineBase(c Choices) string {
376434 p, err := engine.Lookup(c.Provider)
377435 if err != nil {
378436 return ""
@@ -367,12 +367,70 @@ func (w *lineWizard) ask(st Step) string {
367 }367 }
368 }368 }
369 369
370-// resolveBase is the server's address: the flag's, else the provider's370+// resolveBase is the server's address the wizard lists models from: the
371-// default (or its fallback, when the default does not answer).371+// flag's, else the one the environment imposes (AGENT_BASE_URL…), else the
372-func resolveBase(c Choices) string {372+// first that answers of the provider's local addresses, then its fallback
373+// (chooseBase).
374+//
375+// Ticket 214: it used to take the engine's own resolution, whose probe gives
376+// the default address 2 s on /models and switches to the container fallback
377+// (host.docker.internal) without a word. mm on a Mac with Docker Desktop —
378+// where that name resolves on the host too — then listed Ollama's models
379+// from the container address. The written file is not concerned: it carries
380+// no baseUrl for the default address.
381+func resolveBase(ctx context.Context, c Choices) string {
373 if c.BaseURL != "" {382 if c.BaseURL != "" {
374 return c.BaseURL383 return c.BaseURL
375 }384 }
385+ if env := engine.EnvBaseURL(c.Provider); env != "" {
386+ return env
387+ }
388+ if base, fallback, ok := engine.DefaultURLs(c.Provider); ok {
389+ return chooseBase(base, fallback, func(url string) bool {
390+ _, err := Models(ctx, c.Provider, url)
391+ return err == nil
392+ })
393+ }
394+ return engineBase(c)
395+}
396+
397+// chooseBase picks the address to list models from: the first that lists
398+// them among the local ones (localAddresses), else the fallback when it
399+// does, else the default — whose error the model question then shows. A
400+// provider without a fallback (llamacpp, openai) is not probed at all: there
401+// is nothing to choose, and openai's default is a remote service nobody
402+// asked mm to call yet.
403+func chooseBase(base, fallback string, lists func(string) bool) string {
404+ if fallback == "" {
405+ return base
406+ }
407+ for _, url := range localAddresses(base) {
408+ if lists(url) {
409+ return url
410+ }
411+ }
412+ if lists(fallback) {
413+ return fallback
414+ }
415+ return base
416+}
417+
418+// localAddresses is the default address, then — when it names localhost —
419+// the same with 127.0.0.1: a server listening on IPv4 only does not answer
420+// a localhost that resolves to ::1 first.
421+//
422+// localAddresses("http://localhost:11434/v1") // [http://localhost:11434/v1 http://127.0.0.1:11434/v1]
423+func localAddresses(base string) []string {
424+ out := []string{base}
425+ if strings.Contains(base, "://localhost:") || strings.HasSuffix(base, "://localhost") {
426+ out = append(out, strings.Replace(base, "://localhost", "://127.0.0.1", 1))
427+ }
428+ return out
429+}
430+
431+// engineBase is the engine's own resolution, for a provider the wizard has
432+// no addresses for.
433+func engineBase(c Choices) string {
376 p, err := engine.Lookup(c.Provider)434 p, err := engine.Lookup(c.Provider)
377 if err != nil {435 if err != nil {
378 return ""436 return ""
modified internal/agentinit/wizard_test.go +102 -0
@@ -3,13 +3,18 @@ package agentinit
33 import (
44 "bytes"
55 "context"
6+ "encoding/json"
7+ "net/http"
8+ "net/http/httptest"
69 "os"
710 "path/filepath"
811 "strings"
912 "testing"
13+ "time"
1014
1115 "mm/internal/config"
1216 "mm/internal/elicit"
17+ "mm/internal/engine"
1318 )
1419
1520 func llmmanStub(t *testing.T) string {
@@ -247,3 +252,100 @@ func TestMainToStdoutAndBadAnswers(t *testing.T) {
247252 t.Errorf("bad usage: exit %d:\n%s", code, stdout.String())
248253 }
249254 }
255+
256+// Ticket 214: the wizard lists the models from a local address whenever one
257+// answers — the default, then the same with 127.0.0.1 — and takes the
258+// container fallback only when none does. A provider without a fallback is
259+// not probed.
260+func TestChooseBasePrefersALocalAddress(t *testing.T) {
261+ const base, ipv4, fallback = "http://localhost:11434/v1", "http://127.0.0.1:11434/v1", "http://host.docker.internal:11434/v1"
262+ answering := func(urls ...string) (func(string) bool, *[]string) {
263+ var asked []string
264+ return func(url string) bool {
265+ asked = append(asked, url)
266+ for _, u := range urls {
267+ if u == url {
268+ return true
269+ }
270+ }
271+ return false
272+ }, &asked
273+ }
274+ cases := []struct {
275+ name string
276+ answer []string
277+ want string
278+ wantAsked int
279+ }{
280+ {"the default answers, the fallback too (Docker Desktop on a Mac)", []string{base, fallback}, base, 1},
281+ {"only 127.0.0.1 answers (a server on IPv4 only)", []string{ipv4, fallback}, ipv4, 2},
282+ {"only the fallback answers (mm in a container)", []string{fallback}, fallback, 3},
283+ {"nothing answers: the default, whose error the question shows", nil, base, 3},
284+ }
285+ for _, c := range cases {
286+ lists, asked := answering(c.answer...)
287+ if got := chooseBase(base, fallback, lists); got != c.want || len(*asked) != c.wantAsked {
288+ t.Errorf("%s: got %s after asking %v, want %s after %d", c.name, got, *asked, c.want, c.wantAsked)
289+ }
290+ }
291+ lists, asked := answering()
292+ if got := chooseBase("https://api.openai.com/v1", "", lists); got != "https://api.openai.com/v1" || len(*asked) != 0 {
293+ t.Errorf("no fallback: got %s, asked %v — want the default, nothing asked", got, *asked)
294+ }
295+}
296+
297+func TestLocalAddresses(t *testing.T) {
298+ for in, want := range map[string]string{
299+ "http://localhost:11434/v1": "http://localhost:11434/v1 http://127.0.0.1:11434/v1",
300+ "http://localhost:12434/engines/v1": "http://localhost:12434/engines/v1 http://127.0.0.1:12434/engines/v1",
301+ "http://127.0.0.1:17434/v1": "http://127.0.0.1:17434/v1",
302+ } {
303+ if got := strings.Join(localAddresses(in), " "); got != want {
304+ t.Errorf("localAddresses(%s) = %s, want %s", in, got, want)
305+ }
306+ }
307+}
308+
309+// The address comes from the model list, not the engine's 2 s probe of
310+// /models: a server whose /v1/models is slow but whose list answers is kept
311+// — before ticket 214, the probe gave up and the fallback was taken.
312+func TestResolveBaseKeepsASlowLocalServer(t *testing.T) {
313+ slow := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
314+ switch r.URL.Path {
315+ case "/api/tags":
316+ _ = json.NewEncoder(w).Encode(map[string]any{"models": []any{map[string]any{"name": "qwen3.5:4b"}}})
317+ case "/v1/models":
318+ time.Sleep(3 * time.Second) // longer than the engine's probe
319+ _ = json.NewEncoder(w).Encode(map[string]any{"data": []any{}})
320+ default:
321+ http.NotFound(w, r)
322+ }
323+ }))
324+ t.Cleanup(slow.Close)
325+ base := slow.URL + "/v1"
326+ // The engine's own resolution — what the wizard used before — gives up
327+ // on this server and takes the fallback.
328+ fallback := "http://host.docker.internal:1/v1"
329+ p, err := engine.Lookup("ollama")
330+ if err != nil {
331+ t.Fatal(err)
332+ }
333+ if b, err := p.Resolve(config.Config{Provider: "ollama", Model: "probe", BaseURL: base, Fallback: &fallback}); err != nil || b.BaseURL != fallback {
334+ t.Fatalf("the engine's probe kept %s (%v): the test server is not slow enough to show ticket 214", b.BaseURL, err)
335+ }
336+ got := chooseBase(base, fallback, func(url string) bool {
337+ _, err := Models(context.Background(), "ollama", url)
338+ return err == nil
339+ })
340+ if got != base {
341+ t.Errorf("got %s, want the local server %s", got, base)
342+ }
343+}
344+
345+// An address imposed by the environment is taken as is, like the engine does.
346+func TestResolveBaseHonoursAgentBaseURL(t *testing.T) {
347+ t.Setenv("AGENT_BASE_URL", "http://example.invalid:9/v1")
348+ if got := resolveBase(context.Background(), Choices{Provider: "ollama"}); got != "http://example.invalid:9/v1" {
349+ t.Errorf("got %s", got)
350+ }
351+}
@@ -3,13 +3,18 @@ package agentinit
3 import (3 import (
4 "bytes"4 "bytes"
5 "context"5 "context"
6+ "encoding/json"
7+ "net/http"
8+ "net/http/httptest"
6 "os"9 "os"
7 "path/filepath"10 "path/filepath"
8 "strings"11 "strings"
9 "testing"12 "testing"
13+ "time"
10 14
11 "mm/internal/config"15 "mm/internal/config"
12 "mm/internal/elicit"16 "mm/internal/elicit"
17+ "mm/internal/engine"
13 )18 )
14 19
15 func llmmanStub(t *testing.T) string {20 func llmmanStub(t *testing.T) string {
@@ -247,3 +252,100 @@ func TestMainToStdoutAndBadAnswers(t *testing.T) {
247 t.Errorf("bad usage: exit %d:\n%s", code, stdout.String())252 t.Errorf("bad usage: exit %d:\n%s", code, stdout.String())
248 }253 }
249 }254 }
255+
256+// Ticket 214: the wizard lists the models from a local address whenever one
257+// answers — the default, then the same with 127.0.0.1 — and takes the
258+// container fallback only when none does. A provider without a fallback is
259+// not probed.
260+func TestChooseBasePrefersALocalAddress(t *testing.T) {
261+ const base, ipv4, fallback = "http://localhost:11434/v1", "http://127.0.0.1:11434/v1", "http://host.docker.internal:11434/v1"
262+ answering := func(urls ...string) (func(string) bool, *[]string) {
263+ var asked []string
264+ return func(url string) bool {
265+ asked = append(asked, url)
266+ for _, u := range urls {
267+ if u == url {
268+ return true
269+ }
270+ }
271+ return false
272+ }, &asked
273+ }
274+ cases := []struct {
275+ name string
276+ answer []string
277+ want string
278+ wantAsked int
279+ }{
280+ {"the default answers, the fallback too (Docker Desktop on a Mac)", []string{base, fallback}, base, 1},
281+ {"only 127.0.0.1 answers (a server on IPv4 only)", []string{ipv4, fallback}, ipv4, 2},
282+ {"only the fallback answers (mm in a container)", []string{fallback}, fallback, 3},
283+ {"nothing answers: the default, whose error the question shows", nil, base, 3},
284+ }
285+ for _, c := range cases {
286+ lists, asked := answering(c.answer...)
287+ if got := chooseBase(base, fallback, lists); got != c.want || len(*asked) != c.wantAsked {
288+ t.Errorf("%s: got %s after asking %v, want %s after %d", c.name, got, *asked, c.want, c.wantAsked)
289+ }
290+ }
291+ lists, asked := answering()
292+ if got := chooseBase("https://api.openai.com/v1", "", lists); got != "https://api.openai.com/v1" || len(*asked) != 0 {
293+ t.Errorf("no fallback: got %s, asked %v — want the default, nothing asked", got, *asked)
294+ }
295+}
296+
297+func TestLocalAddresses(t *testing.T) {
298+ for in, want := range map[string]string{
299+ "http://localhost:11434/v1": "http://localhost:11434/v1 http://127.0.0.1:11434/v1",
300+ "http://localhost:12434/engines/v1": "http://localhost:12434/engines/v1 http://127.0.0.1:12434/engines/v1",
301+ "http://127.0.0.1:17434/v1": "http://127.0.0.1:17434/v1",
302+ } {
303+ if got := strings.Join(localAddresses(in), " "); got != want {
304+ t.Errorf("localAddresses(%s) = %s, want %s", in, got, want)
305+ }
306+ }
307+}
308+
309+// The address comes from the model list, not the engine's 2 s probe of
310+// /models: a server whose /v1/models is slow but whose list answers is kept
311+// — before ticket 214, the probe gave up and the fallback was taken.
312+func TestResolveBaseKeepsASlowLocalServer(t *testing.T) {
313+ slow := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
314+ switch r.URL.Path {
315+ case "/api/tags":
316+ _ = json.NewEncoder(w).Encode(map[string]any{"models": []any{map[string]any{"name": "qwen3.5:4b"}}})
317+ case "/v1/models":
318+ time.Sleep(3 * time.Second) // longer than the engine's probe
319+ _ = json.NewEncoder(w).Encode(map[string]any{"data": []any{}})
320+ default:
321+ http.NotFound(w, r)
322+ }
323+ }))
324+ t.Cleanup(slow.Close)
325+ base := slow.URL + "/v1"
326+ // The engine's own resolution — what the wizard used before — gives up
327+ // on this server and takes the fallback.
328+ fallback := "http://host.docker.internal:1/v1"
329+ p, err := engine.Lookup("ollama")
330+ if err != nil {
331+ t.Fatal(err)
332+ }
333+ if b, err := p.Resolve(config.Config{Provider: "ollama", Model: "probe", BaseURL: base, Fallback: &fallback}); err != nil || b.BaseURL != fallback {
334+ t.Fatalf("the engine's probe kept %s (%v): the test server is not slow enough to show ticket 214", b.BaseURL, err)
335+ }
336+ got := chooseBase(base, fallback, func(url string) bool {
337+ _, err := Models(context.Background(), "ollama", url)
338+ return err == nil
339+ })
340+ if got != base {
341+ t.Errorf("got %s, want the local server %s", got, base)
342+ }
343+}
344+
345+// An address imposed by the environment is taken as is, like the engine does.
346+func TestResolveBaseHonoursAgentBaseURL(t *testing.T) {
347+ t.Setenv("AGENT_BASE_URL", "http://example.invalid:9/v1")
348+ if got := resolveBase(context.Background(), Choices{Provider: "ollama"}); got != "http://example.invalid:9/v1" {
349+ t.Errorf("got %s", got)
350+ }
351+}
modified internal/engine/openai_compat.go +14 -0
@@ -87,6 +87,20 @@ func DefaultURLs(provider string) (baseURL, fallback string, ok bool) {
8787 return p.baseURL, p.fallback, true
8888 }
8989
90+// EnvBaseURL is the address the environment imposes on a provider —
91+// AGENT_BASE_URL, else the provider's legacy variable (DMR_BASE_URL) — or
92+// "" when none is set. Resolve takes it as is, without probing; so does
93+// `mm init` (ticket 214).
94+func EnvBaseURL(provider string) string {
95+ if env := os.Getenv("AGENT_BASE_URL"); env != "" {
96+ return env
97+ }
98+ if p, found := registry[provider].(*openaiCompat); found && p.legacyBaseURLEnv != "" {
99+ return os.Getenv(p.legacyBaseURLEnv)
100+ }
101+ return ""
102+}
103+
90104 func init() {
91105 register(&openaiCompat{
92106 name: "dmr",
@@ -87,6 +87,20 @@ func DefaultURLs(provider string) (baseURL, fallback string, ok bool) {
87 return p.baseURL, p.fallback, true87 return p.baseURL, p.fallback, true
88 }88 }
89 89
90+// EnvBaseURL is the address the environment imposes on a provider —
91+// AGENT_BASE_URL, else the provider's legacy variable (DMR_BASE_URL) — or
92+// "" when none is set. Resolve takes it as is, without probing; so does
93+// `mm init` (ticket 214).
94+func EnvBaseURL(provider string) string {
95+ if env := os.Getenv("AGENT_BASE_URL"); env != "" {
96+ return env
97+ }
98+ if p, found := registry[provider].(*openaiCompat); found && p.legacyBaseURLEnv != "" {
99+ return os.Getenv(p.legacyBaseURLEnv)
100+ }
101+ return ""
102+}
103+
90 func init() {104 func init() {
91 register(&openaiCompat{105 register(&openaiCompat{
92 name: "dmr",106 name: "dmr",