openai compliance #49
Merged@k33g wants to merge
feature/openai-compliant-again into mainMerged as
748290483a61.25 files changed
modified
.tickets/issues/0165-how-to-unload-models-when-using-decision-models.yaml +2 -2 | @@ -6,10 +6,10 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-26T05:00:19.639Z |
| 9 | -updatedAt: 2026-10-26T14:41:47.750Z | |
| 9 | +updatedAt: 2026-10-26T19:50:00.000Z | |
| 10 | 10 | labels: |
| 11 | 11 | - decision models |
| 12 | -milestone: v1.0.3 | |
| 12 | +milestone: v1.0.4 | |
| 13 | 13 | epic: Decision models |
| 14 | 14 | body: "" |
| 15 | 15 | tasks: [] |
| @@ -6,10 +6,10 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-26T05:00:19.639Z | 8 | createdAt: 2026-10-26T05:00:19.639Z |
| 9 | -updatedAt: 2026-10-26T14:41:47.750Z | 9 | +updatedAt: 2026-10-26T19:50:00.000Z |
| 10 | labels: | 10 | labels: |
| 11 | - decision models | 11 | - decision models |
| 12 | -milestone: v1.0.3 | 12 | +milestone: v1.0.4 |
| 13 | epic: Decision models | 13 | epic: Decision models |
| 14 | body: "" | 14 | body: "" |
| 15 | tasks: [] | 15 | tasks: [] |
modified
.tickets/issues/0166-think-on-how-to-simplify-usage-of-decision-model.yaml +2 -2 | @@ -6,10 +6,10 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-26T05:01:03.034Z |
| 9 | -updatedAt: 2026-10-26T14:42:01.348Z | |
| 9 | +updatedAt: 2026-10-26T19:50:00.000Z | |
| 10 | 10 | labels: |
| 11 | 11 | - decision models |
| 12 | -milestone: v1.0.3 | |
| 12 | +milestone: v1.0.4 | |
| 13 | 13 | epic: Decision models |
| 14 | 14 | body: "" |
| 15 | 15 | tasks: [] |
| @@ -6,10 +6,10 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-26T05:01:03.034Z | 8 | createdAt: 2026-10-26T05:01:03.034Z |
| 9 | -updatedAt: 2026-10-26T14:42:01.348Z | 9 | +updatedAt: 2026-10-26T19:50:00.000Z |
| 10 | labels: | 10 | labels: |
| 11 | - decision models | 11 | - decision models |
| 12 | -milestone: v1.0.3 | 12 | +milestone: v1.0.4 |
| 13 | epic: Decision models | 13 | epic: Decision models |
| 14 | body: "" | 14 | body: "" |
| 15 | tasks: [] | 15 | tasks: [] |
modified
.tickets/issues/0176-openai-sampling-settings-bug.yaml +2 -2 | @@ -6,11 +6,11 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-26T13:50:22.583Z |
| 9 | -updatedAt: 2026-10-26T14:40:39.691Z | |
| 9 | +updatedAt: 2026-10-26T19:45:00.000Z | |
| 10 | 10 | labels: |
| 11 | 11 | - kind::bug |
| 12 | 12 | - To be tested |
| 13 | -milestone: v1.0.2 | |
| 13 | +milestone: v1.0.3 | |
| 14 | 14 | body: | |
| 15 | 15 | I generated the `agent.yaml` file with this command: `mm init` |
| 16 | 16 | the result is: |
| @@ -6,11 +6,11 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-26T13:50:22.583Z | 8 | createdAt: 2026-10-26T13:50:22.583Z |
| 9 | -updatedAt: 2026-10-26T14:40:39.691Z | 9 | +updatedAt: 2026-10-26T19:45:00.000Z |
| 10 | labels: | 10 | labels: |
| 11 | - kind::bug | 11 | - kind::bug |
| 12 | - To be tested | 12 | - To be tested |
| 13 | -milestone: v1.0.2 | 13 | +milestone: v1.0.3 |
| 14 | body: | | 14 | body: | |
| 15 | I generated the `agent.yaml` file with this command: `mm init` | 15 | I generated the `agent.yaml` file with this command: `mm init` |
| 16 | the result is: | 16 | the result is: |
added
.tickets/issues/0177-mm-init-openai-compatible-services.yaml +103 -0 | new file mode 100644 | ||
| @@ -0,0 +1,103 @@ | ||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | |
| 2 | +id: 177 | |
| 3 | +title: "mm init: OpenAI-compatible services — ask the endpoint and the API key's variable, list or type the model" | |
| 4 | +state: closed | |
| 5 | +author: | |
| 6 | + name: k33g | |
| 7 | + email: ph.charriere@gmail.com | |
| 8 | +createdAt: 2026-10-26T17:00:00.000Z | |
| 9 | +updatedAt: 2026-10-26T19:45:00.000Z | |
| 10 | +labels: | |
| 11 | + - kind::feature | |
| 12 | +milestone: v1.0.3 | |
| 13 | +related: | |
| 14 | + - 176 | |
| 15 | + - 121 | |
| 16 | +body: | | |
| 17 | + ## Need (2026-10-26) | |
| 18 | + In `mm init`, `provider: openai` is the way to write an agent for ANY OpenAI-compatible service — OpenAI, Cerebras, Hugging Face, Groq… — not only api.openai.com. For that provider, `mm init` should: | |
| 19 | + - ask the **endpoint** (default `https://api.openai.com/v1`) and write it as `baseUrl`; | |
| 20 | + - ask the **name of the environment variable** that holds the API key (default `OPENAI_API_KEY`; e.g. `CEREBRAS_API_KEY`, `HF_TOKEN`) and write it as `apiKeyEnv`; | |
| 21 | + - if that variable is set, **list the models** of that endpoint with the key; otherwise (or when the list fails) **let the user type the model ID**, saying why there is no list. | |
| 22 | + | |
| 23 | + ## Today (checked in internal/agentinit on 2026-10-26) | |
| 24 | + - The endpoint can only be changed with `--base-url`; no question. | |
| 25 | + - The key is always read from `OPENAI_API_KEY` (`facts.go`, `Models`); `apiKeyEnv` is never asked nor written. | |
| 26 | + - The model list is fetched with a **5-second timeout**, the one used to probe local servers. | |
| 27 | + | |
| 28 | + ## Reported by a user | |
| 29 | + - "The first time I choose OpenAI in mm init, no model is offered. The second time, I get the whole list." Likely cause (not reproduced: no network to OpenAI in the sandbox): the 5 s timeout on a first, cold connection to api.openai.com (DNS + TLS); and the **window does not show why** the list is empty — only the line mode prints `ModelsError()`. | |
| 30 | + - `gpt-4o-mini` works; `gpt-6-luna` fails: `400 … Function tools with reasoning_effort are not supported for gpt-6-luna in /v1/chat/completions. To use function tools, use /v1/responses or set reasoning_effort to 'none'.` mm speaks `/v1/chat/completions` only and always sends tools, so the fix is configuration: `sampling: reasoning_effort: none` (sent as written since ticket 176). Not verified (no key). | |
| 31 | + | |
| 32 | + ## Plan | |
| 33 | + 1. Two questions, for `provider: openai` only, between the server and the model: the endpoint, then the key's variable name (`--base-url` and a new `--api-key-env` answer them). A new endpoint or variable forgets the model. | |
| 34 | + 2. The model list uses the key read from that variable; no key → no request, the model question becomes a text question that says the variable is not set; a failed list → the same, with the error in the question (window and line mode alike). | |
| 35 | + 3. A longer timeout for the remote list (the `openai` provider) than for the local probes. | |
| 36 | + 4. The written file: `baseUrl` (unless it is the default), `apiKeyEnv` (unless `OPENAI_API_KEY`), and a commented `# reasoning_effort: none` line in `sampling` with why. | |
| 37 | + 5. Tests against a fake server (key sent, no key → text question, the error in the prompt, the file's keys); docs EN + FR (`mm init` how-to and CLI reference). | |
| 38 | +tasks: | |
| 39 | + - id: 1 | |
| 40 | + title: "Endpoint and key-variable questions for provider openai (+ --api-key-env)" | |
| 41 | + state: closed | |
| 42 | + priority: none | |
| 43 | + author: | |
| 44 | + name: k33g | |
| 45 | + email: ph.charriere@gmail.com | |
| 46 | + createdAt: 2026-10-26T17:00:00.000Z | |
| 47 | + - id: 2 | |
| 48 | + title: "Model list with that key; no key or a failed list: type the model ID, the reason shown" | |
| 49 | + state: closed | |
| 50 | + priority: none | |
| 51 | + author: | |
| 52 | + name: k33g | |
| 53 | + email: ph.charriere@gmail.com | |
| 54 | + createdAt: 2026-10-26T17:00:00.000Z | |
| 55 | + - id: 3 | |
| 56 | + title: "Longer timeout for the remote model list" | |
| 57 | + state: closed | |
| 58 | + priority: none | |
| 59 | + author: | |
| 60 | + name: k33g | |
| 61 | + email: ph.charriere@gmail.com | |
| 62 | + createdAt: 2026-10-26T17:00:00.000Z | |
| 63 | + - id: 4 | |
| 64 | + title: "Written file: baseUrl, apiKeyEnv, a commented reasoning_effort: none" | |
| 65 | + state: closed | |
| 66 | + priority: none | |
| 67 | + author: | |
| 68 | + name: k33g | |
| 69 | + email: ph.charriere@gmail.com | |
| 70 | + createdAt: 2026-10-26T17:00:00.000Z | |
| 71 | + - id: 5 | |
| 72 | + title: "Tests and docs EN + FR" | |
| 73 | + state: closed | |
| 74 | + priority: none | |
| 75 | + author: | |
| 76 | + name: k33g | |
| 77 | + email: ph.charriere@gmail.com | |
| 78 | + createdAt: 2026-10-26T17:00:00.000Z | |
| 79 | +comments: | |
| 80 | + - id: 1 | |
| 81 | + author: | |
| 82 | + name: k33g | |
| 83 | + email: ph.charriere@gmail.com | |
| 84 | + createdAt: 2026-10-26T18:00:00.000Z | |
| 85 | + body: | | |
| 86 | + Done (tasks 1–5). | |
| 87 | + | |
| 88 | + - **Questions** (`internal/agentinit/flow.go`): for `provider: openai` only, `endpoint` (Text, default `https://api.openai.com/v1`, must start with http(s)://, trailing / removed) and `keyenv` (Text, default `OPENAI_API_KEY`, a shell variable name), between the server and the model. Flags: `--base-url` (existing) and the new `--api-key-env`. A new endpoint or variable forgets the model; another server forgets both (unless given by a flag). The OpenAI API's own address and `OPENAI_API_KEY` are stored as "default" (no `baseUrl` / `apiKeyEnv` written). | |
| 89 | + - **Model list** (`facts.go`): `ListModels(ctx, provider, base, key)`; for openai the key is read from the named variable, and the service is not called at all when it is empty. Timeout 20 s for openai, 5 s for the local probes (was 5 s for all). | |
| 90 | + - **Why there is no list** is now in the model question itself (`Model ID? (CEREBRAS_API_KEY is not set: no list — type it)` or `Model ID? (no list from <base>: <error>)`), so the window shows it too; the line mode's separate ℹ️ line and the now unused `Flow.ModelsError` were removed. | |
| 91 | + - **Written file** (`render.go`): `baseUrl` and `apiKeyEnv` when not the defaults, the key comment names the variable, and a commented `# reasoning_effort: none` with its reason at the end of `sampling`. | |
| 92 | + - **Tests**: `internal/agentinit/openai_flow_test.go`, 7 tests against a fake service (questions only for openai; list with the named key; no key → text question, service not called; 401 → reason in the question; answers checked; what a change forgets; the file's keys). `go test ./...`, `go vet ./...`, gofmt clean. | |
| 93 | + - **End to end** with the binary and a local fake service: line mode without the key → "FAKE_SVC_KEY is not set: no list — type it", file written and loaded; `--yes` with the key → the service's models listed, `baseUrl`, `apiKeyEnv` and the hint in the file. | |
| 94 | + - **Docs** EN + FR: reference/cli (`--base-url`, `--api-key-env`, `--model` rows), how-to/create-an-agent (variant "An OpenAI-compatible service"), how-to/run-with-an-openai-compatible-server (the mm init line, the reasoning_effort variant). | |
| 95 | + | |
| 96 | + NOT verified: against the real OpenAI, Cerebras or Hugging Face APIs (no keys, no network in the sandbox); that 20 s fixes the reported "no model on the first run" (cause not reproduced); `reasoning_effort: none` with gpt-6-luna (reported by a user, not tried); the window driven live (its tests pass). | |
| 97 | + - id: 2 | |
| 98 | + author: | |
| 99 | + name: k33g | |
| 100 | + email: ph.charriere@gmail.com | |
| 101 | + createdAt: 2026-10-26T19:00:00.000Z | |
| 102 | + body: | | |
| 103 | + Tested by the user with OpenAI and Mistral AI: works. Follow-up fixed: the window's text answers (endpoint, key variable, file) now have a caret — ←→, Home/End (Ctrl-A/Ctrl-E), Backspace before / Delete after the caret, typing inserts at it (`field` in internal/tui/initwin.go, 2 tests; docs EN + FR). Closed. | |
| new file mode 100644 | |||
| @@ -0,0 +1,103 @@ | |||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | ||
| 2 | +id: 177 | ||
| 3 | +title: "mm init: OpenAI-compatible services — ask the endpoint and the API key's variable, list or type the model" | ||
| 4 | +state: closed | ||
| 5 | +author: | ||
| 6 | + name: k33g | ||
| 7 | + email: ph.charriere@gmail.com | ||
| 8 | +createdAt: 2026-10-26T17:00:00.000Z | ||
| 9 | +updatedAt: 2026-10-26T19:45:00.000Z | ||
| 10 | +labels: | ||
| 11 | + - kind::feature | ||
| 12 | +milestone: v1.0.3 | ||
| 13 | +related: | ||
| 14 | + - 176 | ||
| 15 | + - 121 | ||
| 16 | +body: | | ||
| 17 | + ## Need (2026-10-26) | ||
| 18 | + In `mm init`, `provider: openai` is the way to write an agent for ANY OpenAI-compatible service — OpenAI, Cerebras, Hugging Face, Groq… — not only api.openai.com. For that provider, `mm init` should: | ||
| 19 | + - ask the **endpoint** (default `https://api.openai.com/v1`) and write it as `baseUrl`; | ||
| 20 | + - ask the **name of the environment variable** that holds the API key (default `OPENAI_API_KEY`; e.g. `CEREBRAS_API_KEY`, `HF_TOKEN`) and write it as `apiKeyEnv`; | ||
| 21 | + - if that variable is set, **list the models** of that endpoint with the key; otherwise (or when the list fails) **let the user type the model ID**, saying why there is no list. | ||
| 22 | + | ||
| 23 | + ## Today (checked in internal/agentinit on 2026-10-26) | ||
| 24 | + - The endpoint can only be changed with `--base-url`; no question. | ||
| 25 | + - The key is always read from `OPENAI_API_KEY` (`facts.go`, `Models`); `apiKeyEnv` is never asked nor written. | ||
| 26 | + - The model list is fetched with a **5-second timeout**, the one used to probe local servers. | ||
| 27 | + | ||
| 28 | + ## Reported by a user | ||
| 29 | + - "The first time I choose OpenAI in mm init, no model is offered. The second time, I get the whole list." Likely cause (not reproduced: no network to OpenAI in the sandbox): the 5 s timeout on a first, cold connection to api.openai.com (DNS + TLS); and the **window does not show why** the list is empty — only the line mode prints `ModelsError()`. | ||
| 30 | + - `gpt-4o-mini` works; `gpt-6-luna` fails: `400 … Function tools with reasoning_effort are not supported for gpt-6-luna in /v1/chat/completions. To use function tools, use /v1/responses or set reasoning_effort to 'none'.` mm speaks `/v1/chat/completions` only and always sends tools, so the fix is configuration: `sampling: reasoning_effort: none` (sent as written since ticket 176). Not verified (no key). | ||
| 31 | + | ||
| 32 | + ## Plan | ||
| 33 | + 1. Two questions, for `provider: openai` only, between the server and the model: the endpoint, then the key's variable name (`--base-url` and a new `--api-key-env` answer them). A new endpoint or variable forgets the model. | ||
| 34 | + 2. The model list uses the key read from that variable; no key → no request, the model question becomes a text question that says the variable is not set; a failed list → the same, with the error in the question (window and line mode alike). | ||
| 35 | + 3. A longer timeout for the remote list (the `openai` provider) than for the local probes. | ||
| 36 | + 4. The written file: `baseUrl` (unless it is the default), `apiKeyEnv` (unless `OPENAI_API_KEY`), and a commented `# reasoning_effort: none` line in `sampling` with why. | ||
| 37 | + 5. Tests against a fake server (key sent, no key → text question, the error in the prompt, the file's keys); docs EN + FR (`mm init` how-to and CLI reference). | ||
| 38 | +tasks: | ||
| 39 | + - id: 1 | ||
| 40 | + title: "Endpoint and key-variable questions for provider openai (+ --api-key-env)" | ||
| 41 | + state: closed | ||
| 42 | + priority: none | ||
| 43 | + author: | ||
| 44 | + name: k33g | ||
| 45 | + email: ph.charriere@gmail.com | ||
| 46 | + createdAt: 2026-10-26T17:00:00.000Z | ||
| 47 | + - id: 2 | ||
| 48 | + title: "Model list with that key; no key or a failed list: type the model ID, the reason shown" | ||
| 49 | + state: closed | ||
| 50 | + priority: none | ||
| 51 | + author: | ||
| 52 | + name: k33g | ||
| 53 | + email: ph.charriere@gmail.com | ||
| 54 | + createdAt: 2026-10-26T17:00:00.000Z | ||
| 55 | + - id: 3 | ||
| 56 | + title: "Longer timeout for the remote model list" | ||
| 57 | + state: closed | ||
| 58 | + priority: none | ||
| 59 | + author: | ||
| 60 | + name: k33g | ||
| 61 | + email: ph.charriere@gmail.com | ||
| 62 | + createdAt: 2026-10-26T17:00:00.000Z | ||
| 63 | + - id: 4 | ||
| 64 | + title: "Written file: baseUrl, apiKeyEnv, a commented reasoning_effort: none" | ||
| 65 | + state: closed | ||
| 66 | + priority: none | ||
| 67 | + author: | ||
| 68 | + name: k33g | ||
| 69 | + email: ph.charriere@gmail.com | ||
| 70 | + createdAt: 2026-10-26T17:00:00.000Z | ||
| 71 | + - id: 5 | ||
| 72 | + title: "Tests and docs EN + FR" | ||
| 73 | + state: closed | ||
| 74 | + priority: none | ||
| 75 | + author: | ||
| 76 | + name: k33g | ||
| 77 | + email: ph.charriere@gmail.com | ||
| 78 | + createdAt: 2026-10-26T17:00:00.000Z | ||
| 79 | +comments: | ||
| 80 | + - id: 1 | ||
| 81 | + author: | ||
| 82 | + name: k33g | ||
| 83 | + email: ph.charriere@gmail.com | ||
| 84 | + createdAt: 2026-10-26T18:00:00.000Z | ||
| 85 | + body: | | ||
| 86 | + Done (tasks 1–5). | ||
| 87 | + | ||
| 88 | + - **Questions** (`internal/agentinit/flow.go`): for `provider: openai` only, `endpoint` (Text, default `https://api.openai.com/v1`, must start with http(s)://, trailing / removed) and `keyenv` (Text, default `OPENAI_API_KEY`, a shell variable name), between the server and the model. Flags: `--base-url` (existing) and the new `--api-key-env`. A new endpoint or variable forgets the model; another server forgets both (unless given by a flag). The OpenAI API's own address and `OPENAI_API_KEY` are stored as "default" (no `baseUrl` / `apiKeyEnv` written). | ||
| 89 | + - **Model list** (`facts.go`): `ListModels(ctx, provider, base, key)`; for openai the key is read from the named variable, and the service is not called at all when it is empty. Timeout 20 s for openai, 5 s for the local probes (was 5 s for all). | ||
| 90 | + - **Why there is no list** is now in the model question itself (`Model ID? (CEREBRAS_API_KEY is not set: no list — type it)` or `Model ID? (no list from <base>: <error>)`), so the window shows it too; the line mode's separate ℹ️ line and the now unused `Flow.ModelsError` were removed. | ||
| 91 | + - **Written file** (`render.go`): `baseUrl` and `apiKeyEnv` when not the defaults, the key comment names the variable, and a commented `# reasoning_effort: none` with its reason at the end of `sampling`. | ||
| 92 | + - **Tests**: `internal/agentinit/openai_flow_test.go`, 7 tests against a fake service (questions only for openai; list with the named key; no key → text question, service not called; 401 → reason in the question; answers checked; what a change forgets; the file's keys). `go test ./...`, `go vet ./...`, gofmt clean. | ||
| 93 | + - **End to end** with the binary and a local fake service: line mode without the key → "FAKE_SVC_KEY is not set: no list — type it", file written and loaded; `--yes` with the key → the service's models listed, `baseUrl`, `apiKeyEnv` and the hint in the file. | ||
| 94 | + - **Docs** EN + FR: reference/cli (`--base-url`, `--api-key-env`, `--model` rows), how-to/create-an-agent (variant "An OpenAI-compatible service"), how-to/run-with-an-openai-compatible-server (the mm init line, the reasoning_effort variant). | ||
| 95 | + | ||
| 96 | + NOT verified: against the real OpenAI, Cerebras or Hugging Face APIs (no keys, no network in the sandbox); that 20 s fixes the reported "no model on the first run" (cause not reproduced); `reasoning_effort: none` with gpt-6-luna (reported by a user, not tried); the window driven live (its tests pass). | ||
| 97 | + - id: 2 | ||
| 98 | + author: | ||
| 99 | + name: k33g | ||
| 100 | + email: ph.charriere@gmail.com | ||
| 101 | + createdAt: 2026-10-26T19:00:00.000Z | ||
| 102 | + body: | | ||
| 103 | + Tested by the user with OpenAI and Mistral AI: works. Follow-up fixed: the window's text answers (endpoint, key variable, file) now have a caret — ←→, Home/End (Ctrl-A/Ctrl-E), Backspace before / Delete after the caret, typing inserts at it (`field` in internal/tui/initwin.go, 2 tests; docs EN + FR). Closed. | ||
added
.tickets/issues/0178-support-openai-responses-api.yaml +102 -0 | new file mode 100644 | ||
| @@ -0,0 +1,102 @@ | ||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | |
| 2 | +id: 178 | |
| 3 | +title: "Support OpenAI's Responses API, to keep reasoning and tools together (gpt-6-luna)" | |
| 4 | +state: open | |
| 5 | +author: | |
| 6 | + name: k33g | |
| 7 | + email: ph.charriere@gmail.com | |
| 8 | +createdAt: 2026-10-26T19:30:00.000Z | |
| 9 | +updatedAt: 2026-10-26T19:30:00.000Z | |
| 10 | +labels: | |
| 11 | + - kind::feature | |
| 12 | +epic: Providers (LLM Engine) | |
| 13 | +related: | |
| 14 | + - 176 | |
| 15 | + - 177 | |
| 16 | +body: | | |
| 17 | + ## The problem (reported 2026-10-26) | |
| 18 | + With `provider: openai`, `gpt-4o-mini` works but `gpt-6-luna` answers: | |
| 19 | + | |
| 20 | + ``` | |
| 21 | + 400 Bad Request — "Function tools with reasoning_effort are not supported for gpt-6-luna in | |
| 22 | + /v1/chat/completions. To use function tools, use /v1/responses or set reasoning_effort to 'none'." | |
| 23 | + ``` | |
| 24 | + | |
| 25 | + - OpenAI has two APIs: **Chat Completions** (`/v1/chat/completions`, the one every "OpenAI-compatible" server speaks) and **Responses** (`/v1/responses`, the newer one OpenAI recommends for its reasoning models). | |
| 26 | + - mm speaks **only Chat Completions**, through Genkit's `compat_oai` plugin. | |
| 27 | + - An mm agent **always sends tools** (bash, the file tools…). For this model, OpenAI refuses tools + active reasoning on Chat Completions; the model's default reasoning applies when the file sets none. | |
| 28 | + | |
| 29 | + ## What already works (configuration only) | |
| 30 | + `sampling: reasoning_effort: none` — sent as written since ticket 176, written commented by `mm init` since ticket 177. The model then works with tools **but does not reason**. Not tried with gpt-6-luna (no key in the sandbox). | |
| 31 | + | |
| 32 | + ## The goal | |
| 33 | + Keep the reasoning AND the tools on such models: let mm speak the **Responses API**. | |
| 34 | + | |
| 35 | + ## What exists (checked 2026-10-26) | |
| 36 | + - Genkit Go v1.13.1, `plugins/compat_oai`: Chat Completions only (no Responses code; one comment links the Responses docs). | |
| 37 | + - `github.com/openai/openai-go` v1.8.2, already in `go.mod`, has a `responses` package (`client.Responses.New` / `NewStreaming`). | |
| 38 | + - So: a Genkit model of our own (`genkit.DefineModel` with mm's generate function) that converts Genkit's request and response to and from the Responses API with openai-go — the same seam `engine.Provider` already gives (one implementation per wire protocol). | |
| 39 | + | |
| 40 | + ## What it must keep working (mm relies on all of it) | |
| 41 | + - Streaming text chunks (watchdog, line mode, window, ACP) — Responses streams typed events (`response.output_text.delta`, `response.function_call_arguments.delta`, `response.completed`…), not Chat Completions chunks. | |
| 42 | + - Tool calls and their results across the turns (`function_call` / `function_call_output` items instead of `tool_calls` / `role: tool` messages); the full conversation returned to mm (`engine.Generate`'s WrapModel capture). | |
| 43 | + - **The reasoning between turns**: with `store: false`, the reasoning items must be sent back (`include: ["reasoning.encrypted_content"]`) or the model loses its thread after each tool call; or `previous_response_id` with `store: true` (the conversation then lives on OpenAI's side — to decide). | |
| 44 | + - Reasoning shown as thoughts (`showThinking`), usage counts (`usage_update`, context %), the struggle signals, `structuredOutput` (`text.format: json_schema`), the context compression's summary request, `max_output_tokens` (Responses' name for the limit). | |
| 45 | + | |
| 46 | + ## Options to decide | |
| 47 | + - **A — a new provider** `openai-responses` (explicit, nothing changes for the existing `openai` files). | |
| 48 | + - **B — a key on the openai provider**, e.g. `api: responses` (one provider, two wire protocols). | |
| 49 | + - **C — automatic**: Responses for OpenAI's reasoning models only — rejected for now: a list of model names would age fast, and other OpenAI-compatible services do not all serve `/v1/responses` (which do — vLLM, Ollama, llama-server… — is NOT verified). | |
| 50 | + | |
| 51 | + ## Not done here | |
| 52 | + Nothing implemented. A real check needs an OpenAI key and a reasoning model. | |
| 53 | +tasks: | |
| 54 | + - id: 1 | |
| 55 | + title: "Decide: a new provider (A) or an api key on openai (B); store false + encrypted reasoning, or previous_response_id" | |
| 56 | + state: open | |
| 57 | + priority: none | |
| 58 | + author: | |
| 59 | + name: k33g | |
| 60 | + email: ph.charriere@gmail.com | |
| 61 | + createdAt: 2026-10-26T19:30:00.000Z | |
| 62 | + - id: 2 | |
| 63 | + title: "Check which OpenAI-compatible servers serve /v1/responses (vLLM, Ollama, llama-server…)" | |
| 64 | + state: open | |
| 65 | + priority: none | |
| 66 | + author: | |
| 67 | + name: k33g | |
| 68 | + email: ph.charriere@gmail.com | |
| 69 | + createdAt: 2026-10-26T19:30:00.000Z | |
| 70 | + - id: 3 | |
| 71 | + title: "A Genkit model on openai-go's responses package: request/response conversion, streaming events, tool calls" | |
| 72 | + state: open | |
| 73 | + priority: none | |
| 74 | + author: | |
| 75 | + name: k33g | |
| 76 | + email: ph.charriere@gmail.com | |
| 77 | + createdAt: 2026-10-26T19:30:00.000Z | |
| 78 | + - id: 4 | |
| 79 | + title: "Reasoning items carried between turns; thoughts, usage, structuredOutput, compaction, max_output_tokens" | |
| 80 | + state: open | |
| 81 | + priority: none | |
| 82 | + author: | |
| 83 | + name: k33g | |
| 84 | + email: ph.charriere@gmail.com | |
| 85 | + createdAt: 2026-10-26T19:30:00.000Z | |
| 86 | + - id: 5 | |
| 87 | + title: "Tests against a fake /v1/responses server; a live check with a reasoning model" | |
| 88 | + state: open | |
| 89 | + priority: none | |
| 90 | + author: | |
| 91 | + name: k33g | |
| 92 | + email: ph.charriere@gmail.com | |
| 93 | + createdAt: 2026-10-26T19:30:00.000Z | |
| 94 | + - id: 6 | |
| 95 | + title: "mm init and docs EN + FR" | |
| 96 | + state: open | |
| 97 | + priority: none | |
| 98 | + author: | |
| 99 | + name: k33g | |
| 100 | + email: ph.charriere@gmail.com | |
| 101 | + createdAt: 2026-10-26T19:30:00.000Z | |
| 102 | +comments: [] | |
| new file mode 100644 | |||
| @@ -0,0 +1,102 @@ | |||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | ||
| 2 | +id: 178 | ||
| 3 | +title: "Support OpenAI's Responses API, to keep reasoning and tools together (gpt-6-luna)" | ||
| 4 | +state: open | ||
| 5 | +author: | ||
| 6 | + name: k33g | ||
| 7 | + email: ph.charriere@gmail.com | ||
| 8 | +createdAt: 2026-10-26T19:30:00.000Z | ||
| 9 | +updatedAt: 2026-10-26T19:30:00.000Z | ||
| 10 | +labels: | ||
| 11 | + - kind::feature | ||
| 12 | +epic: Providers (LLM Engine) | ||
| 13 | +related: | ||
| 14 | + - 176 | ||
| 15 | + - 177 | ||
| 16 | +body: | | ||
| 17 | + ## The problem (reported 2026-10-26) | ||
| 18 | + With `provider: openai`, `gpt-4o-mini` works but `gpt-6-luna` answers: | ||
| 19 | + | ||
| 20 | + ``` | ||
| 21 | + 400 Bad Request — "Function tools with reasoning_effort are not supported for gpt-6-luna in | ||
| 22 | + /v1/chat/completions. To use function tools, use /v1/responses or set reasoning_effort to 'none'." | ||
| 23 | + ``` | ||
| 24 | + | ||
| 25 | + - OpenAI has two APIs: **Chat Completions** (`/v1/chat/completions`, the one every "OpenAI-compatible" server speaks) and **Responses** (`/v1/responses`, the newer one OpenAI recommends for its reasoning models). | ||
| 26 | + - mm speaks **only Chat Completions**, through Genkit's `compat_oai` plugin. | ||
| 27 | + - An mm agent **always sends tools** (bash, the file tools…). For this model, OpenAI refuses tools + active reasoning on Chat Completions; the model's default reasoning applies when the file sets none. | ||
| 28 | + | ||
| 29 | + ## What already works (configuration only) | ||
| 30 | + `sampling: reasoning_effort: none` — sent as written since ticket 176, written commented by `mm init` since ticket 177. The model then works with tools **but does not reason**. Not tried with gpt-6-luna (no key in the sandbox). | ||
| 31 | + | ||
| 32 | + ## The goal | ||
| 33 | + Keep the reasoning AND the tools on such models: let mm speak the **Responses API**. | ||
| 34 | + | ||
| 35 | + ## What exists (checked 2026-10-26) | ||
| 36 | + - Genkit Go v1.13.1, `plugins/compat_oai`: Chat Completions only (no Responses code; one comment links the Responses docs). | ||
| 37 | + - `github.com/openai/openai-go` v1.8.2, already in `go.mod`, has a `responses` package (`client.Responses.New` / `NewStreaming`). | ||
| 38 | + - So: a Genkit model of our own (`genkit.DefineModel` with mm's generate function) that converts Genkit's request and response to and from the Responses API with openai-go — the same seam `engine.Provider` already gives (one implementation per wire protocol). | ||
| 39 | + | ||
| 40 | + ## What it must keep working (mm relies on all of it) | ||
| 41 | + - Streaming text chunks (watchdog, line mode, window, ACP) — Responses streams typed events (`response.output_text.delta`, `response.function_call_arguments.delta`, `response.completed`…), not Chat Completions chunks. | ||
| 42 | + - Tool calls and their results across the turns (`function_call` / `function_call_output` items instead of `tool_calls` / `role: tool` messages); the full conversation returned to mm (`engine.Generate`'s WrapModel capture). | ||
| 43 | + - **The reasoning between turns**: with `store: false`, the reasoning items must be sent back (`include: ["reasoning.encrypted_content"]`) or the model loses its thread after each tool call; or `previous_response_id` with `store: true` (the conversation then lives on OpenAI's side — to decide). | ||
| 44 | + - Reasoning shown as thoughts (`showThinking`), usage counts (`usage_update`, context %), the struggle signals, `structuredOutput` (`text.format: json_schema`), the context compression's summary request, `max_output_tokens` (Responses' name for the limit). | ||
| 45 | + | ||
| 46 | + ## Options to decide | ||
| 47 | + - **A — a new provider** `openai-responses` (explicit, nothing changes for the existing `openai` files). | ||
| 48 | + - **B — a key on the openai provider**, e.g. `api: responses` (one provider, two wire protocols). | ||
| 49 | + - **C — automatic**: Responses for OpenAI's reasoning models only — rejected for now: a list of model names would age fast, and other OpenAI-compatible services do not all serve `/v1/responses` (which do — vLLM, Ollama, llama-server… — is NOT verified). | ||
| 50 | + | ||
| 51 | + ## Not done here | ||
| 52 | + Nothing implemented. A real check needs an OpenAI key and a reasoning model. | ||
| 53 | +tasks: | ||
| 54 | + - id: 1 | ||
| 55 | + title: "Decide: a new provider (A) or an api key on openai (B); store false + encrypted reasoning, or previous_response_id" | ||
| 56 | + state: open | ||
| 57 | + priority: none | ||
| 58 | + author: | ||
| 59 | + name: k33g | ||
| 60 | + email: ph.charriere@gmail.com | ||
| 61 | + createdAt: 2026-10-26T19:30:00.000Z | ||
| 62 | + - id: 2 | ||
| 63 | + title: "Check which OpenAI-compatible servers serve /v1/responses (vLLM, Ollama, llama-server…)" | ||
| 64 | + state: open | ||
| 65 | + priority: none | ||
| 66 | + author: | ||
| 67 | + name: k33g | ||
| 68 | + email: ph.charriere@gmail.com | ||
| 69 | + createdAt: 2026-10-26T19:30:00.000Z | ||
| 70 | + - id: 3 | ||
| 71 | + title: "A Genkit model on openai-go's responses package: request/response conversion, streaming events, tool calls" | ||
| 72 | + state: open | ||
| 73 | + priority: none | ||
| 74 | + author: | ||
| 75 | + name: k33g | ||
| 76 | + email: ph.charriere@gmail.com | ||
| 77 | + createdAt: 2026-10-26T19:30:00.000Z | ||
| 78 | + - id: 4 | ||
| 79 | + title: "Reasoning items carried between turns; thoughts, usage, structuredOutput, compaction, max_output_tokens" | ||
| 80 | + state: open | ||
| 81 | + priority: none | ||
| 82 | + author: | ||
| 83 | + name: k33g | ||
| 84 | + email: ph.charriere@gmail.com | ||
| 85 | + createdAt: 2026-10-26T19:30:00.000Z | ||
| 86 | + - id: 5 | ||
| 87 | + title: "Tests against a fake /v1/responses server; a live check with a reasoning model" | ||
| 88 | + state: open | ||
| 89 | + priority: none | ||
| 90 | + author: | ||
| 91 | + name: k33g | ||
| 92 | + email: ph.charriere@gmail.com | ||
| 93 | + createdAt: 2026-10-26T19:30:00.000Z | ||
| 94 | + - id: 6 | ||
| 95 | + title: "mm init and docs EN + FR" | ||
| 96 | + state: open | ||
| 97 | + priority: none | ||
| 98 | + author: | ||
| 99 | + name: k33g | ||
| 100 | + email: ph.charriere@gmail.com | ||
| 101 | + createdAt: 2026-10-26T19:30:00.000Z | ||
| 102 | +comments: [] | ||
modified
.tickets/milestones.yaml +3 -0 | @@ -47,3 +47,6 @@ milestones: | ||
| 47 | 47 | - title: v1.0.3 |
| 48 | 48 | state: open |
| 49 | 49 | dueOn: 2026-10-31 |
| 50 | + - title: v1.0.4 | |
| 51 | + state: open | |
| 52 | + dueOn: 2026-10-31 | |
| @@ -47,3 +47,6 @@ milestones: | |||
| 47 | - title: v1.0.3 | 47 | - title: v1.0.3 |
| 48 | state: open | 48 | state: open |
| 49 | dueOn: 2026-10-31 | 49 | dueOn: 2026-10-31 |
| 50 | + - title: v1.0.4 | ||
| 51 | + state: open | ||
| 52 | + dueOn: 2026-10-31 | ||
added
demos/open-ai-compliant/README.md +19 -0 | new file mode 100644 | ||
| @@ -0,0 +1,19 @@ | ||
| 1 | +# OpenAI compliant provider | |
| 2 | + | |
| 3 | +## OpenAI | |
| 4 | + | |
| 5 | +```bash | |
| 6 | +export OPENAI_API_KEY=sk-proj.....GO-kA | |
| 7 | +mm init | |
| 8 | +# name of the API key: OPENAI_API_KEY | |
| 9 | +# default endpoint: https://api.openai.com/v1 | |
| 10 | +``` | |
| 11 | + | |
| 12 | +## MistralAI | |
| 13 | + | |
| 14 | +```bash | |
| 15 | +export MISTRAL_API_KEY=mstrl_Ks......_3RFL8e | |
| 16 | +mm init | |
| 17 | +# name of the API key: MISTRAL_API_KEY | |
| 18 | +# default endpoint: https://api.mistral.ai/v1 | |
| 19 | +``` | |
| \ No newline at end of file | ||
| new file mode 100644 | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | +# OpenAI compliant provider | ||
| 2 | + | ||
| 3 | +## OpenAI | ||
| 4 | + | ||
| 5 | +```bash | ||
| 6 | +export OPENAI_API_KEY=sk-proj.....GO-kA | ||
| 7 | +mm init | ||
| 8 | +# name of the API key: OPENAI_API_KEY | ||
| 9 | +# default endpoint: https://api.openai.com/v1 | ||
| 10 | +``` | ||
| 11 | + | ||
| 12 | +## MistralAI | ||
| 13 | + | ||
| 14 | +```bash | ||
| 15 | +export MISTRAL_API_KEY=mstrl_Ks......_3RFL8e | ||
| 16 | +mm init | ||
| 17 | +# name of the API key: MISTRAL_API_KEY | ||
| 18 | +# default endpoint: https://api.mistral.ai/v1 | ||
| 19 | +``` | ||
| \ No newline at end of file | \ No newline at end of file | ||
added
demos/open-ai-compliant/agent.mistral.yaml +99 -0 | new file mode 100644 | ||
| @@ -0,0 +1,99 @@ | ||
| 1 | +# developer — a coding agent: reads and changes the project, runs commands. | |
| 2 | +# | |
| 3 | +# Written by `mm init`. Every value says why it has that value; change | |
| 4 | +# them as you watch the agent work (guides/agent-settings-for-small-models.md). | |
| 5 | +# | |
| 6 | +# model family: other | |
| 7 | +# usage: developer | |
| 8 | +# machine: small — a laptop, models up to about 8B | |
| 9 | +# model facts: none — openai said nothing about this model beyond its name | |
| 10 | +# | |
| 11 | +# Where the numbers come from (the tag before their comment): | |
| 12 | +# [guide] the settings guide's row for this machine | |
| 13 | +# [rule] a rule of thumb, not specific to this model | |
| 14 | +# | |
| 15 | +# mm -config <this file> or mm -tui <this file> | |
| 16 | + | |
| 17 | +provider: openai | |
| 18 | +# The model. Chosen for it (family other): sampling (temperature, top_p, | |
| 19 | +# max_tokens). Change the model and redo them together — running mm init | |
| 20 | +# again is the quick way. | |
| 21 | +model: "ministral-8b-2512" | |
| 22 | +baseUrl: "https://api.mistral.ai/v1" | |
| 23 | +# The API key is read from MISTRAL_API_KEY: export it — it never goes in this file. | |
| 24 | +apiKeyEnv: MISTRAL_API_KEY | |
| 25 | + | |
| 26 | +# 0 = unknown: the generic openai provider cannot ask the server for its | |
| 27 | +# window, so compression counts messages (context.maxMessages) instead — set | |
| 28 | +# the window your server serves | |
| 29 | +contextWindow: 0 | |
| 30 | +# your answer: every command and file change waits for your answer | |
| 31 | +yolo: false | |
| 32 | +# usage developer | |
| 33 | +bashTool: true | |
| 34 | +editTools: true | |
| 35 | +planTool: true | |
| 36 | +askUserTool: true | |
| 37 | +# no skills: add a skills/ folder and set skillsDir to use some (see | |
| 38 | +# docs/en/how-to/add-a-skill.md) | |
| 39 | +skillsDir: no-skills | |
| 40 | +# usage developer | |
| 41 | +maxTurns: 40 | |
| 42 | +# [guide] settings guide, row "Small model (≤ 8B), short tasks" | |
| 43 | +maxOutput: 8000 | |
| 44 | +firstTokenTimeout: 3m | |
| 45 | +# [guide] aligned on bashTimeout, as the settings guide says when bash is on | |
| 46 | +# (section 3) | |
| 47 | +watchdogTimeout: 30s | |
| 48 | +# [guide] settings guide, row "Small model (≤ 8B), short tasks" | |
| 49 | +bashTimeout: 30s | |
| 50 | + | |
| 51 | +sampling: | |
| 52 | + # [rule] precise temperature for a developer agent without reasoning (family | |
| 53 | + # other) | |
| 54 | + temperature: 0.2 | |
| 55 | + # [rule] family other: skills/create-agent rule when nothing is published: | |
| 56 | + # 0.7 / 0.9, 0.2 for a code agent without reasoning | |
| 57 | + top_p: 0.9 | |
| 58 | + # [rule] room for an answer or a whole file in one tool call | |
| 59 | + max_tokens: 4096 | |
| 60 | + # one tool call per reply: small models run a check before the change it | |
| 61 | + # checks has landed otherwise | |
| 62 | + parallel_tool_calls: false | |
| 63 | + # Uncomment if the API answers "Function tools with reasoning_effort are not | |
| 64 | + # supported … set reasoning_effort to 'none'" (seen with gpt-6-luna): mm uses | |
| 65 | + # /v1/chat/completions and always sends tools. It turns the reasoning off. | |
| 66 | + # reasoning_effort: none | |
| 67 | + | |
| 68 | +context: | |
| 69 | + # compress the history before the window is full (settings guide, every row) | |
| 70 | + enabled: true | |
| 71 | + # mm's default: 75 % of the window the server serves, a quarter left for the | |
| 72 | + # next question and its tool outputs | |
| 73 | + threshold: 75 | |
| 74 | + | |
| 75 | +agentsMd: | |
| 76 | + # An AGENTS.md in the project directory is added to the system prompt. | |
| 77 | + enabled: true | |
| 78 | + | |
| 79 | +acp: | |
| 80 | + name: developer | |
| 81 | + title: "Developer (developer)" | |
| 82 | + | |
| 83 | +system: | | |
| 84 | + You are a coding agent working in a terminal, in the user's project. | |
| 85 | + You have a bash tool, file tools (read_file, write_file, edit_file) and a | |
| 86 | + plan tool. | |
| 87 | + Read before you change: look at the files and run the commands that tell | |
| 88 | + you how the project works. Make the smallest change that does the job, | |
| 89 | + then run the tests or the build to check it. | |
| 90 | + Never state the output of a command you did not run in this answer. | |
| 91 | + When a task has several steps, make a plan and update it as you go. | |
| 92 | + | |
| 93 | +# The family of this model is not in mm's table: the sampling is a generic | |
| 94 | +# starting point — check the model card. | |
| 95 | +# The server said nothing about this model beyond its name: the settings | |
| 96 | +# come from the family table. | |
| 97 | + | |
| 98 | +# Next: skills (docs/en/how-to/add-a-skill.md), MCP servers (docs/en/tutorials/mcp-getting-started.md), | |
| 99 | +# hooks (docs/en/how-to/guard-the-agent-with-hooks.md), sessions (docs/en/how-to/keep-sessions.md). | |
| new file mode 100644 | |||
| @@ -0,0 +1,99 @@ | |||
| 1 | +# developer — a coding agent: reads and changes the project, runs commands. | ||
| 2 | +# | ||
| 3 | +# Written by `mm init`. Every value says why it has that value; change | ||
| 4 | +# them as you watch the agent work (guides/agent-settings-for-small-models.md). | ||
| 5 | +# | ||
| 6 | +# model family: other | ||
| 7 | +# usage: developer | ||
| 8 | +# machine: small — a laptop, models up to about 8B | ||
| 9 | +# model facts: none — openai said nothing about this model beyond its name | ||
| 10 | +# | ||
| 11 | +# Where the numbers come from (the tag before their comment): | ||
| 12 | +# [guide] the settings guide's row for this machine | ||
| 13 | +# [rule] a rule of thumb, not specific to this model | ||
| 14 | +# | ||
| 15 | +# mm -config <this file> or mm -tui <this file> | ||
| 16 | + | ||
| 17 | +provider: openai | ||
| 18 | +# The model. Chosen for it (family other): sampling (temperature, top_p, | ||
| 19 | +# max_tokens). Change the model and redo them together — running mm init | ||
| 20 | +# again is the quick way. | ||
| 21 | +model: "ministral-8b-2512" | ||
| 22 | +baseUrl: "https://api.mistral.ai/v1" | ||
| 23 | +# The API key is read from MISTRAL_API_KEY: export it — it never goes in this file. | ||
| 24 | +apiKeyEnv: MISTRAL_API_KEY | ||
| 25 | + | ||
| 26 | +# 0 = unknown: the generic openai provider cannot ask the server for its | ||
| 27 | +# window, so compression counts messages (context.maxMessages) instead — set | ||
| 28 | +# the window your server serves | ||
| 29 | +contextWindow: 0 | ||
| 30 | +# your answer: every command and file change waits for your answer | ||
| 31 | +yolo: false | ||
| 32 | +# usage developer | ||
| 33 | +bashTool: true | ||
| 34 | +editTools: true | ||
| 35 | +planTool: true | ||
| 36 | +askUserTool: true | ||
| 37 | +# no skills: add a skills/ folder and set skillsDir to use some (see | ||
| 38 | +# docs/en/how-to/add-a-skill.md) | ||
| 39 | +skillsDir: no-skills | ||
| 40 | +# usage developer | ||
| 41 | +maxTurns: 40 | ||
| 42 | +# [guide] settings guide, row "Small model (≤ 8B), short tasks" | ||
| 43 | +maxOutput: 8000 | ||
| 44 | +firstTokenTimeout: 3m | ||
| 45 | +# [guide] aligned on bashTimeout, as the settings guide says when bash is on | ||
| 46 | +# (section 3) | ||
| 47 | +watchdogTimeout: 30s | ||
| 48 | +# [guide] settings guide, row "Small model (≤ 8B), short tasks" | ||
| 49 | +bashTimeout: 30s | ||
| 50 | + | ||
| 51 | +sampling: | ||
| 52 | + # [rule] precise temperature for a developer agent without reasoning (family | ||
| 53 | + # other) | ||
| 54 | + temperature: 0.2 | ||
| 55 | + # [rule] family other: skills/create-agent rule when nothing is published: | ||
| 56 | + # 0.7 / 0.9, 0.2 for a code agent without reasoning | ||
| 57 | + top_p: 0.9 | ||
| 58 | + # [rule] room for an answer or a whole file in one tool call | ||
| 59 | + max_tokens: 4096 | ||
| 60 | + # one tool call per reply: small models run a check before the change it | ||
| 61 | + # checks has landed otherwise | ||
| 62 | + parallel_tool_calls: false | ||
| 63 | + # Uncomment if the API answers "Function tools with reasoning_effort are not | ||
| 64 | + # supported … set reasoning_effort to 'none'" (seen with gpt-6-luna): mm uses | ||
| 65 | + # /v1/chat/completions and always sends tools. It turns the reasoning off. | ||
| 66 | + # reasoning_effort: none | ||
| 67 | + | ||
| 68 | +context: | ||
| 69 | + # compress the history before the window is full (settings guide, every row) | ||
| 70 | + enabled: true | ||
| 71 | + # mm's default: 75 % of the window the server serves, a quarter left for the | ||
| 72 | + # next question and its tool outputs | ||
| 73 | + threshold: 75 | ||
| 74 | + | ||
| 75 | +agentsMd: | ||
| 76 | + # An AGENTS.md in the project directory is added to the system prompt. | ||
| 77 | + enabled: true | ||
| 78 | + | ||
| 79 | +acp: | ||
| 80 | + name: developer | ||
| 81 | + title: "Developer (developer)" | ||
| 82 | + | ||
| 83 | +system: | | ||
| 84 | + You are a coding agent working in a terminal, in the user's project. | ||
| 85 | + You have a bash tool, file tools (read_file, write_file, edit_file) and a | ||
| 86 | + plan tool. | ||
| 87 | + Read before you change: look at the files and run the commands that tell | ||
| 88 | + you how the project works. Make the smallest change that does the job, | ||
| 89 | + then run the tests or the build to check it. | ||
| 90 | + Never state the output of a command you did not run in this answer. | ||
| 91 | + When a task has several steps, make a plan and update it as you go. | ||
| 92 | + | ||
| 93 | +# The family of this model is not in mm's table: the sampling is a generic | ||
| 94 | +# starting point — check the model card. | ||
| 95 | +# The server said nothing about this model beyond its name: the settings | ||
| 96 | +# come from the family table. | ||
| 97 | + | ||
| 98 | +# Next: skills (docs/en/how-to/add-a-skill.md), MCP servers (docs/en/tutorials/mcp-getting-started.md), | ||
| 99 | +# hooks (docs/en/how-to/guard-the-agent-with-hooks.md), sessions (docs/en/how-to/keep-sessions.md). | ||
renamed
demos/open-ai-compliant/agent.yaml +8 -4 | similarity index 90% | ||
| rename from demos/open-ai/agent.yaml | ||
| rename to demos/open-ai-compliant/agent.yaml | ||
| @@ -19,13 +19,13 @@ provider: openai | ||
| 19 | 19 | # The model. Chosen for it (family openai): sampling (temperature, |
| 20 | 20 | # max_completion_tokens). Change the model and redo them together — running |
| 21 | 21 | # mm init again is the quick way. |
| 22 | -model: "gpt-5.4" | |
| 22 | +model: "gpt-4.1" | |
| 23 | 23 | # No baseUrl: openai's defaults apply. For the record: |
| 24 | 24 | # baseUrl: "https://api.openai.com/v1" |
| 25 | 25 | # (openai has no fallback address) |
| 26 | -# The key, when the server needs one, is read from OPENAI_API_KEY (or the | |
| 27 | -# variable apiKeyEnv names): export it — it never goes in this file. | |
| 28 | -# The default is the OpenAI API: set baseUrl for any other OpenAI-compatible server. | |
| 26 | +# The API key is read from OPENAI_API_KEY (or the variable apiKeyEnv names): | |
| 27 | +# export it — it never goes in this file. | |
| 28 | +# The default is the OpenAI API: set baseUrl for any other OpenAI-compatible service. | |
| 29 | 29 | |
| 30 | 30 | # 0 = unknown: the generic openai provider cannot ask the server for its |
| 31 | 31 | # window, so compression counts messages (context.maxMessages) instead — set |
| @@ -62,6 +62,10 @@ sampling: | ||
| 62 | 62 | # one tool call per reply: small models run a check before the change it |
| 63 | 63 | # checks has landed otherwise |
| 64 | 64 | parallel_tool_calls: false |
| 65 | + # Uncomment if the API answers "Function tools with reasoning_effort are not | |
| 66 | + # supported … set reasoning_effort to 'none'" (seen with gpt-6-luna): mm uses | |
| 67 | + # /v1/chat/completions and always sends tools. It turns the reasoning off. | |
| 68 | + # reasoning_effort: none | |
| 65 | 69 | |
| 66 | 70 | context: |
| 67 | 71 | # compress the history before the window is full (settings guide, every row) |
| similarity index 90% | |||
| rename from demos/open-ai/agent.yaml | |||
| rename to demos/open-ai-compliant/agent.yaml | |||
| @@ -19,13 +19,13 @@ provider: openai | |||
| 19 | # The model. Chosen for it (family openai): sampling (temperature, | 19 | # The model. Chosen for it (family openai): sampling (temperature, |
| 20 | # max_completion_tokens). Change the model and redo them together — running | 20 | # max_completion_tokens). Change the model and redo them together — running |
| 21 | # mm init again is the quick way. | 21 | # mm init again is the quick way. |
| 22 | -model: "gpt-5.4" | 22 | +model: "gpt-4.1" |
| 23 | # No baseUrl: openai's defaults apply. For the record: | 23 | # No baseUrl: openai's defaults apply. For the record: |
| 24 | # baseUrl: "https://api.openai.com/v1" | 24 | # baseUrl: "https://api.openai.com/v1" |
| 25 | # (openai has no fallback address) | 25 | # (openai has no fallback address) |
| 26 | -# The key, when the server needs one, is read from OPENAI_API_KEY (or the | 26 | +# The API key is read from OPENAI_API_KEY (or the variable apiKeyEnv names): |
| 27 | -# variable apiKeyEnv names): export it — it never goes in this file. | 27 | +# export it — it never goes in this file. |
| 28 | -# The default is the OpenAI API: set baseUrl for any other OpenAI-compatible server. | 28 | +# The default is the OpenAI API: set baseUrl for any other OpenAI-compatible service. |
| 29 | 29 | ||
| 30 | # 0 = unknown: the generic openai provider cannot ask the server for its | 30 | # 0 = unknown: the generic openai provider cannot ask the server for its |
| 31 | # window, so compression counts messages (context.maxMessages) instead — set | 31 | # window, so compression counts messages (context.maxMessages) instead — set |
| @@ -62,6 +62,10 @@ sampling: | |||
| 62 | # one tool call per reply: small models run a check before the change it | 62 | # one tool call per reply: small models run a check before the change it |
| 63 | # checks has landed otherwise | 63 | # checks has landed otherwise |
| 64 | parallel_tool_calls: false | 64 | parallel_tool_calls: false |
| 65 | + # Uncomment if the API answers "Function tools with reasoning_effort are not | ||
| 66 | + # supported … set reasoning_effort to 'none'" (seen with gpt-6-luna): mm uses | ||
| 67 | + # /v1/chat/completions and always sends tools. It turns the reasoning off. | ||
| 68 | + # reasoning_effort: none | ||
| 65 | 69 | ||
| 66 | context: | 70 | context: |
| 67 | # compress the history before the window is full (settings guide, every row) | 71 | # compress the history before the window is full (settings guide, every row) |
modified
docs/en/how-to/create-an-agent.md +2 -1 | @@ -51,7 +51,7 @@ This guide shows how to write a new agent.yaml for a model and a job, with mm's | ||
| 51 | 51 | ↑↓ choose · Enter next · Esc back · PgUp/PgDn preview · Ctrl-C quit |
| 52 | 52 | ``` |
| 53 | 53 | |
| 54 | - `↑` `↓` choose, `Enter` answers and goes to the next question, `Esc` goes back one question, `PgUp` `PgDn` scroll the preview, `Ctrl-C` quits without writing. A text question (the name, the file) is typed; `Ctrl-U` clears it. On a screen narrower than 100 columns, `Tab` shows the preview in place of the question. When every question is answered, `Enter` writes the file. | |
| 54 | + `↑` `↓` choose, `Enter` answers and goes to the next question, `Esc` goes back one question, `PgUp` `PgDn` scroll the preview, `Ctrl-C` quits without writing. A text question (the name, the file; the endpoint and the key's variable for `openai`) is typed; `←` `→` move the cursor in it, `Home` `End` go to its ends, `Delete` removes the character after the cursor, `Ctrl-U` clears it. On a screen narrower than 100 columns, `Tab` shows the preview in place of the question. When every question is answered, `Enter` writes the file. | |
| 55 | 55 | |
| 56 | 56 | The questions: the model server, the model (from the server's list), its family (detected from the name), what the agent is for (developer, reviewer, translator, writer, assistant, researcher, docs, json, coordinator), which machine runs the model (small, medium, large), whether to ask before each command or file change, the agent's name and the file. |
| 57 | 57 | |
| @@ -79,6 +79,7 @@ This guide shows how to write a new agent.yaml for a model and a job, with mm's | ||
| 79 | 79 | ``` |
| 80 | 80 | |
| 81 | 81 | - **See the file without writing it.** `-o -` prints it on stdout. |
| 82 | +- **An OpenAI-compatible service (OpenAI, Cerebras, Hugging Face…).** Choose `openai`: two more questions follow, the endpoint (`https://api.openai.com/v1` by default) and the name of the variable that holds the API key (`OPENAI_API_KEY` by default, `CEREBRAS_API_KEY`, `HF_TOKEN`…). Export the key before running `mm init`: with it, the model question lists the service's models; without it, you type the model's ID (the question says the variable is not set). Flags: `--provider openai --base-url https://api.cerebras.ai/v1 --api-key-env CEREBRAS_API_KEY`. The file gets `baseUrl` and `apiKeyEnv`, and a commented `reasoning_effort: none` in `sampling`, to uncomment if the API answers "Function tools with reasoning_effort are not supported". | |
| 82 | 83 | - **A server elsewhere.** `--base-url http://192.168.1.20:17434/v1`; it is written into the file as `baseUrl`. Without it, the file has no `baseUrl` and records the provider's default address and fallback as comments, ready to uncomment. |
| 83 | 84 | - **A server that does not answer.** mm says so and asks for the model's name: the settings then come from the family table only. |
| 84 | 85 | - **An existing file.** mm never overwrites it, and says so before asking anything: choose another name with `-o`. |
| @@ -51,7 +51,7 @@ This guide shows how to write a new agent.yaml for a model and a job, with mm's | |||
| 51 | ↑↓ choose · Enter next · Esc back · PgUp/PgDn preview · Ctrl-C quit | 51 | ↑↓ choose · Enter next · Esc back · PgUp/PgDn preview · Ctrl-C quit |
| 52 | ``` | 52 | ``` |
| 53 | 53 | ||
| 54 | - `↑` `↓` choose, `Enter` answers and goes to the next question, `Esc` goes back one question, `PgUp` `PgDn` scroll the preview, `Ctrl-C` quits without writing. A text question (the name, the file) is typed; `Ctrl-U` clears it. On a screen narrower than 100 columns, `Tab` shows the preview in place of the question. When every question is answered, `Enter` writes the file. | 54 | + `↑` `↓` choose, `Enter` answers and goes to the next question, `Esc` goes back one question, `PgUp` `PgDn` scroll the preview, `Ctrl-C` quits without writing. A text question (the name, the file; the endpoint and the key's variable for `openai`) is typed; `←` `→` move the cursor in it, `Home` `End` go to its ends, `Delete` removes the character after the cursor, `Ctrl-U` clears it. On a screen narrower than 100 columns, `Tab` shows the preview in place of the question. When every question is answered, `Enter` writes the file. |
| 55 | 55 | ||
| 56 | The questions: the model server, the model (from the server's list), its family (detected from the name), what the agent is for (developer, reviewer, translator, writer, assistant, researcher, docs, json, coordinator), which machine runs the model (small, medium, large), whether to ask before each command or file change, the agent's name and the file. | 56 | The questions: the model server, the model (from the server's list), its family (detected from the name), what the agent is for (developer, reviewer, translator, writer, assistant, researcher, docs, json, coordinator), which machine runs the model (small, medium, large), whether to ask before each command or file change, the agent's name and the file. |
| 57 | 57 | ||
| @@ -79,6 +79,7 @@ This guide shows how to write a new agent.yaml for a model and a job, with mm's | |||
| 79 | ``` | 79 | ``` |
| 80 | 80 | ||
| 81 | - **See the file without writing it.** `-o -` prints it on stdout. | 81 | - **See the file without writing it.** `-o -` prints it on stdout. |
| 82 | +- **An OpenAI-compatible service (OpenAI, Cerebras, Hugging Face…).** Choose `openai`: two more questions follow, the endpoint (`https://api.openai.com/v1` by default) and the name of the variable that holds the API key (`OPENAI_API_KEY` by default, `CEREBRAS_API_KEY`, `HF_TOKEN`…). Export the key before running `mm init`: with it, the model question lists the service's models; without it, you type the model's ID (the question says the variable is not set). Flags: `--provider openai --base-url https://api.cerebras.ai/v1 --api-key-env CEREBRAS_API_KEY`. The file gets `baseUrl` and `apiKeyEnv`, and a commented `reasoning_effort: none` in `sampling`, to uncomment if the API answers "Function tools with reasoning_effort are not supported". | ||
| 82 | - **A server elsewhere.** `--base-url http://192.168.1.20:17434/v1`; it is written into the file as `baseUrl`. Without it, the file has no `baseUrl` and records the provider's default address and fallback as comments, ready to uncomment. | 83 | - **A server elsewhere.** `--base-url http://192.168.1.20:17434/v1`; it is written into the file as `baseUrl`. Without it, the file has no `baseUrl` and records the provider's default address and fallback as comments, ready to uncomment. |
| 83 | - **A server that does not answer.** mm says so and asks for the model's name: the settings then come from the family table only. | 84 | - **A server that does not answer.** mm says so and asks for the model's name: the settings then come from the family table only. |
| 84 | - **An existing file.** mm never overwrites it, and says so before asking anything: choose another name with `-o`. | 85 | - **An existing file.** mm never overwrites it, and says so before asking anything: choose another name with `-o`. |
modified
docs/en/how-to/run-with-an-openai-compatible-server.md +2 -1 | @@ -29,11 +29,12 @@ This guide shows how to run mini-me on a model server that has no dedicated prov | ||
| 29 | 29 | 🤖 The command printed: `hello-from-openai-provider` |
| 30 | 30 | ``` |
| 31 | 31 | |
| 32 | -`mm init --provider openai --base-url <address>` writes such a file for you (see [Create an agent with `mm init`](create-an-agent.md)). | |
| 32 | +`mm init` writes such a file for you: choose `openai`, then give the endpoint and the name of the variable that holds the key (or `--provider openai --base-url <address> --api-key-env <VARIABLE>`) (see [Create an agent with `mm init`](create-an-agent.md)). | |
| 33 | 33 | |
| 34 | 34 | ## Variants |
| 35 | 35 | |
| 36 | 36 | - **The OpenAI API.** No `baseUrl`, `OPENAI_API_KEY` exported, a model such as `gpt-5-mini`. Its reasoning models accept only the default temperature and take `max_completion_tokens` instead of `max_tokens`: write `temperature: 1.0` and `max_completion_tokens` in `sampling`. With `provider: openai` mm sends that block as written, without its own defaults (`top_p`, `max_tokens`), so remove a key the API still refuses and nothing else gets in the way. `mm init` writes such a block. (The request's content was checked against a local server; no call to the OpenAI API was made in this repository.) |
| 37 | +- **"Function tools with reasoning_effort are not supported".** Some OpenAI models (seen: `gpt-6-luna`) refuse tools on `/v1/chat/completions` while they reason; mm uses that route and always sends tools. Add `reasoning_effort: none` to `sampling` (it turns the reasoning off). Not verified in this repository: reported by a user. | |
| 37 | 38 | - **The key is refused.** The start-up warning says `openai: <baseUrl> refused the key (HTTP 401) — export OPENAI_API_KEY`. |
| 38 | 39 | - **Nothing answers.** The warning says `openai: nothing answers at <baseUrl> — check baseUrl: …`: the address must be the OpenAI-compatible one, usually ending in `/v1`. |
| 39 | 40 | - **A server mm has a provider for.** Prefer it (`dmr`, `llamacpp`, `llmman`, `ollama`): those know how the server reads a long prompt, turns reasoning off for a summary, unloads a model, and tell the window size. The generic provider does none of that. |
| @@ -29,11 +29,12 @@ This guide shows how to run mini-me on a model server that has no dedicated prov | |||
| 29 | 🤖 The command printed: `hello-from-openai-provider` | 29 | 🤖 The command printed: `hello-from-openai-provider` |
| 30 | ``` | 30 | ``` |
| 31 | 31 | ||
| 32 | -`mm init --provider openai --base-url <address>` writes such a file for you (see [Create an agent with `mm init`](create-an-agent.md)). | 32 | +`mm init` writes such a file for you: choose `openai`, then give the endpoint and the name of the variable that holds the key (or `--provider openai --base-url <address> --api-key-env <VARIABLE>`) (see [Create an agent with `mm init`](create-an-agent.md)). |
| 33 | 33 | ||
| 34 | ## Variants | 34 | ## Variants |
| 35 | 35 | ||
| 36 | - **The OpenAI API.** No `baseUrl`, `OPENAI_API_KEY` exported, a model such as `gpt-5-mini`. Its reasoning models accept only the default temperature and take `max_completion_tokens` instead of `max_tokens`: write `temperature: 1.0` and `max_completion_tokens` in `sampling`. With `provider: openai` mm sends that block as written, without its own defaults (`top_p`, `max_tokens`), so remove a key the API still refuses and nothing else gets in the way. `mm init` writes such a block. (The request's content was checked against a local server; no call to the OpenAI API was made in this repository.) | 36 | - **The OpenAI API.** No `baseUrl`, `OPENAI_API_KEY` exported, a model such as `gpt-5-mini`. Its reasoning models accept only the default temperature and take `max_completion_tokens` instead of `max_tokens`: write `temperature: 1.0` and `max_completion_tokens` in `sampling`. With `provider: openai` mm sends that block as written, without its own defaults (`top_p`, `max_tokens`), so remove a key the API still refuses and nothing else gets in the way. `mm init` writes such a block. (The request's content was checked against a local server; no call to the OpenAI API was made in this repository.) |
| 37 | +- **"Function tools with reasoning_effort are not supported".** Some OpenAI models (seen: `gpt-6-luna`) refuse tools on `/v1/chat/completions` while they reason; mm uses that route and always sends tools. Add `reasoning_effort: none` to `sampling` (it turns the reasoning off). Not verified in this repository: reported by a user. | ||
| 37 | - **The key is refused.** The start-up warning says `openai: <baseUrl> refused the key (HTTP 401) — export OPENAI_API_KEY`. | 38 | - **The key is refused.** The start-up warning says `openai: <baseUrl> refused the key (HTTP 401) — export OPENAI_API_KEY`. |
| 38 | - **Nothing answers.** The warning says `openai: nothing answers at <baseUrl> — check baseUrl: …`: the address must be the OpenAI-compatible one, usually ending in `/v1`. | 39 | - **Nothing answers.** The warning says `openai: nothing answers at <baseUrl> — check baseUrl: …`: the address must be the OpenAI-compatible one, usually ending in `/v1`. |
| 39 | - **A server mm has a provider for.** Prefer it (`dmr`, `llamacpp`, `llmman`, `ollama`): those know how the server reads a long prompt, turns reasoning off for a summary, unloads a model, and tell the window size. The generic provider does none of that. | 40 | - **A server mm has a provider for.** Prefer it (`dmr`, `llamacpp`, `llmman`, `ollama`): those know how the server reads a long prompt, turns reasoning off for a summary, unloads a model, and tell the window size. The generic provider does none of that. |
modified
docs/en/reference/cli.md +4 -3 | @@ -192,8 +192,9 @@ A turn cancelled by the user reports nothing. `demos/struggle-signals/` triggers | ||
| 192 | 192 | | Flag | Question when absent | Default | |
| 193 | 193 | |------|----------------------|---------| |
| 194 | 194 | | `--provider` | Model server: `llmman`, `ollama`, `dmr`, `llamacpp`, `openai` | the first local one whose server answers, else `llmman` (`openai` is never probed: its default address is the OpenAI API) | |
| 195 | -| `--base-url` | — (not asked) | the provider's address, or its fallback when it does not answer | | |
| 196 | -| `--model` | Model: a list of the server's models (`/api/tags` for llmman and Ollama, `/models` for the others), or a name to type when the server does not answer | the first of the list | | |
| 195 | +| `--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 | | |
| 196 | +| `--api-key-env` | `openai` only: Environment variable that holds the API key | `OPENAI_API_KEY`. Written as `apiKeyEnv` unless it is the default | | |
| 197 | +| `--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 | | |
| 197 | 198 | | `--family` | Model family | detected from the model's name, then from the server's family name | |
| 198 | 199 | | `--usage` | What the agent is for: `developer`, `reviewer`, `translator`, `writer`, `assistant`, `researcher`, `docs`, `json`, `coordinator` | `developer` | |
| 199 | 200 | | `--machine` | Which machine runs the model: `small`, `medium`, `large` | `small` | |
| @@ -206,7 +207,7 @@ A turn cancelled by the user reports nothing. `demos/struggle-signals/` triggers | ||
| 206 | 207 | |
| 207 | 208 | | Aspect | Behaviour | |
| 208 | 209 | |--------|-----------| |
| 209 | -| Window | When stdin and stdout are terminals (and neither `--yes` nor `--line`): a full-screen window — steps (✓ answered, ▸ current, · to come, each answer beside its step), the question with the highlighted choice's description (models are listed by their short name, as in the window's header — `Qwen3.5-4B:Q4_K_M` — and the highlighted model's full identifier is shown under the list), and the file as it would be written with that choice (preview beside the question from 100 columns, in its place with `Tab` below). Keys: `↑` `↓` `Home` `End` choose, `Enter` answers, `y` / `n` answer the yes-or-no question, `Esc` takes the previous answer back (answers given by flags are skipped), `PgUp` `PgDn` scroll the preview, `Ctrl-U` clears a text answer, `Ctrl-C` quits without writing (`nothing written`, exit 1). A refused answer shows on the bottom rule. Once every question is answered, `Enter` writes the file. | | |
| 210 | +| Window | When stdin and stdout are terminals (and neither `--yes` nor `--line`): a full-screen window — steps (✓ answered, ▸ current, · to come, each answer beside its step), the question with the highlighted choice's description (models are listed by their short name, as in the window's header — `Qwen3.5-4B:Q4_K_M` — and the highlighted model's full identifier is shown under the list), and the file as it would be written with that choice (preview beside the question from 100 columns, in its place with `Tab` below). Keys: `↑` `↓` `Home` `End` choose, `Enter` answers, `y` / `n` answer the yes-or-no question, `Esc` takes the previous answer back (answers given by flags are skipped), `PgUp` `PgDn` scroll the preview, `←` `→` move the cursor in a text answer (`Home` `End` or `Ctrl-A` `Ctrl-E` to its ends; typing inserts at the cursor, `Backspace` deletes before it, `Delete` after it), `Ctrl-U` clears it, `Ctrl-C` quits without writing (`nothing written`, exit 1). A refused answer shows on the bottom rule. Once every question is answered, `Enter` writes the file. | | |
| 210 | 211 | | Line mode | Otherwise: one question per screen line, numbered choices; a number, an option's text or another text answers; an empty line or `/skip` takes the default. Usages and machines are listed with a one-line description first. | |
| 211 | 212 | | Model facts | llmman and Ollama: `POST /api/show` — the native window, the GGUF's sampling (`general.sampling.*`, Ollama's Modelfile parameters), the chat template's reasoning switch (`enable_thinking`, `reasoning_effort`) and its default. llama-server: `GET /props` — `n_ctx` and the chat template. Docker Model Runner and `openai`: none — the family table decides; `openai` lists its models with `OPENAI_API_KEY`. | |
| 212 | 213 | | Where each value comes from | The server's facts, then the family table, then the usage, then the machine row; the table is `internal/agentinit/presets.yaml`, each entry with its source and, per sampling block, its `origin` (`measured`, `card` or `rule`). | |
| @@ -192,8 +192,9 @@ A turn cancelled by the user reports nothing. `demos/struggle-signals/` triggers | |||
| 192 | | Flag | Question when absent | Default | | 192 | | Flag | Question when absent | Default | |
| 193 | |------|----------------------|---------| | 193 | |------|----------------------|---------| |
| 194 | | `--provider` | Model server: `llmman`, `ollama`, `dmr`, `llamacpp`, `openai` | the first local one whose server answers, else `llmman` (`openai` is never probed: its default address is the OpenAI API) | | 194 | | `--provider` | Model server: `llmman`, `ollama`, `dmr`, `llamacpp`, `openai` | the first local one whose server answers, else `llmman` (`openai` is never probed: its default address is the OpenAI API) | |
| 195 | -| `--base-url` | — (not asked) | the provider's address, or its fallback when it does not answer | | 195 | +| `--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 | |
| 196 | -| `--model` | Model: a list of the server's models (`/api/tags` for llmman and Ollama, `/models` for the others), or a name to type when the server does not answer | the first of the list | | 196 | +| `--api-key-env` | `openai` only: Environment variable that holds the API key | `OPENAI_API_KEY`. Written as `apiKeyEnv` unless it is the default | |
| 197 | +| `--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 | | ||
| 197 | | `--family` | Model family | detected from the model's name, then from the server's family name | | 198 | | `--family` | Model family | detected from the model's name, then from the server's family name | |
| 198 | | `--usage` | What the agent is for: `developer`, `reviewer`, `translator`, `writer`, `assistant`, `researcher`, `docs`, `json`, `coordinator` | `developer` | | 199 | | `--usage` | What the agent is for: `developer`, `reviewer`, `translator`, `writer`, `assistant`, `researcher`, `docs`, `json`, `coordinator` | `developer` | |
| 199 | | `--machine` | Which machine runs the model: `small`, `medium`, `large` | `small` | | 200 | | `--machine` | Which machine runs the model: `small`, `medium`, `large` | `small` | |
| @@ -206,7 +207,7 @@ A turn cancelled by the user reports nothing. `demos/struggle-signals/` triggers | |||
| 206 | 207 | ||
| 207 | | Aspect | Behaviour | | 208 | | Aspect | Behaviour | |
| 208 | |--------|-----------| | 209 | |--------|-----------| |
| 209 | -| Window | When stdin and stdout are terminals (and neither `--yes` nor `--line`): a full-screen window — steps (✓ answered, ▸ current, · to come, each answer beside its step), the question with the highlighted choice's description (models are listed by their short name, as in the window's header — `Qwen3.5-4B:Q4_K_M` — and the highlighted model's full identifier is shown under the list), and the file as it would be written with that choice (preview beside the question from 100 columns, in its place with `Tab` below). Keys: `↑` `↓` `Home` `End` choose, `Enter` answers, `y` / `n` answer the yes-or-no question, `Esc` takes the previous answer back (answers given by flags are skipped), `PgUp` `PgDn` scroll the preview, `Ctrl-U` clears a text answer, `Ctrl-C` quits without writing (`nothing written`, exit 1). A refused answer shows on the bottom rule. Once every question is answered, `Enter` writes the file. | | 210 | +| Window | When stdin and stdout are terminals (and neither `--yes` nor `--line`): a full-screen window — steps (✓ answered, ▸ current, · to come, each answer beside its step), the question with the highlighted choice's description (models are listed by their short name, as in the window's header — `Qwen3.5-4B:Q4_K_M` — and the highlighted model's full identifier is shown under the list), and the file as it would be written with that choice (preview beside the question from 100 columns, in its place with `Tab` below). Keys: `↑` `↓` `Home` `End` choose, `Enter` answers, `y` / `n` answer the yes-or-no question, `Esc` takes the previous answer back (answers given by flags are skipped), `PgUp` `PgDn` scroll the preview, `←` `→` move the cursor in a text answer (`Home` `End` or `Ctrl-A` `Ctrl-E` to its ends; typing inserts at the cursor, `Backspace` deletes before it, `Delete` after it), `Ctrl-U` clears it, `Ctrl-C` quits without writing (`nothing written`, exit 1). A refused answer shows on the bottom rule. Once every question is answered, `Enter` writes the file. | |
| 210 | | Line mode | Otherwise: one question per screen line, numbered choices; a number, an option's text or another text answers; an empty line or `/skip` takes the default. Usages and machines are listed with a one-line description first. | | 211 | | Line mode | Otherwise: one question per screen line, numbered choices; a number, an option's text or another text answers; an empty line or `/skip` takes the default. Usages and machines are listed with a one-line description first. | |
| 211 | | Model facts | llmman and Ollama: `POST /api/show` — the native window, the GGUF's sampling (`general.sampling.*`, Ollama's Modelfile parameters), the chat template's reasoning switch (`enable_thinking`, `reasoning_effort`) and its default. llama-server: `GET /props` — `n_ctx` and the chat template. Docker Model Runner and `openai`: none — the family table decides; `openai` lists its models with `OPENAI_API_KEY`. | | 212 | | Model facts | llmman and Ollama: `POST /api/show` — the native window, the GGUF's sampling (`general.sampling.*`, Ollama's Modelfile parameters), the chat template's reasoning switch (`enable_thinking`, `reasoning_effort`) and its default. llama-server: `GET /props` — `n_ctx` and the chat template. Docker Model Runner and `openai`: none — the family table decides; `openai` lists its models with `OPENAI_API_KEY`. | |
| 212 | | Where each value comes from | The server's facts, then the family table, then the usage, then the machine row; the table is `internal/agentinit/presets.yaml`, each entry with its source and, per sampling block, its `origin` (`measured`, `card` or `rule`). | | 213 | | Where each value comes from | The server's facts, then the family table, then the usage, then the machine row; the table is `internal/agentinit/presets.yaml`, each entry with its source and, per sampling block, its `origin` (`measured`, `card` or `rule`). | |
modified
docs/fr/how-to/create-an-agent.md +2 -1 | @@ -51,7 +51,7 @@ Ce guide montre comment écrire un nouvel agent.yaml pour un modèle et une tâc | ||
| 51 | 51 | ↑↓ choose · Enter next · Esc back · PgUp/PgDn preview · Ctrl-C quit |
| 52 | 52 | ``` |
| 53 | 53 | |
| 54 | - `↑` `↓` choisissent, `Entrée` répond et passe à la question suivante, `Échap` revient d'une question en arrière, `PgUp` `PgDn` font défiler l'aperçu, `Ctrl-C` quitte sans rien écrire. Une question de texte (le nom, le fichier) se tape ; `Ctrl-U` l'efface. Sur un écran de moins de 100 colonnes, `Tab` affiche l'aperçu à la place de la question. Quand toutes les questions ont une réponse, `Entrée` écrit le fichier. | |
| 54 | + `↑` `↓` choisissent, `Entrée` répond et passe à la question suivante, `Échap` revient d'une question en arrière, `PgUp` `PgDn` font défiler l'aperçu, `Ctrl-C` quitte sans rien écrire. Une question de texte (le nom, le fichier ; l'endpoint et la variable de la clé pour `openai`) se tape ; `←` `→` déplacent le curseur, `Début` `Fin` vont à ses bouts, `Suppr` efface le caractère après le curseur, `Ctrl-U` efface tout. Sur un écran de moins de 100 colonnes, `Tab` affiche l'aperçu à la place de la question. Quand toutes les questions ont une réponse, `Entrée` écrit le fichier. | |
| 55 | 55 | |
| 56 | 56 | Les questions : le serveur de modèles, le modèle (dans la liste du serveur), sa famille (détectée d'après le nom), à quoi sert l'agent (developer, reviewer, translator, writer, assistant, researcher, docs, json, coordinator), quelle machine fait tourner le modèle (small, medium, large), s'il faut demander avant chaque commande ou modification de fichier, le nom de l'agent et le fichier. |
| 57 | 57 | |
| @@ -79,6 +79,7 @@ Ce guide montre comment écrire un nouvel agent.yaml pour un modèle et une tâc | ||
| 79 | 79 | ``` |
| 80 | 80 | |
| 81 | 81 | - **Voir le fichier sans l'écrire.** `-o -` l'affiche sur la sortie standard. |
| 82 | +- **Un service compatible OpenAI (OpenAI, Cerebras, Hugging Face…).** Choisissez `openai` : deux questions de plus suivent, l'endpoint (`https://api.openai.com/v1` par défaut) et le nom de la variable qui contient la clé d'API (`OPENAI_API_KEY` par défaut, `CEREBRAS_API_KEY`, `HF_TOKEN`…). Exportez la clé avant de lancer `mm init` : avec elle, la question du modèle liste les modèles du service ; sans elle, vous tapez l'ID du modèle (la question dit que la variable n'est pas renseignée). Options : `--provider openai --base-url https://api.cerebras.ai/v1 --api-key-env CEREBRAS_API_KEY`. Le fichier reçoit `baseUrl` et `apiKeyEnv`, et un `reasoning_effort: none` en commentaire dans `sampling`, à décommenter si l'API répond « Function tools with reasoning_effort are not supported ». | |
| 82 | 83 | - **Un serveur ailleurs.** `--base-url http://192.168.1.20:17434/v1` ; il est écrit dans le fichier comme `baseUrl`. Sans lui, le fichier n'a pas de `baseUrl` et garde en commentaire l'adresse par défaut du fournisseur et son fallback, prêts à être décommentés. |
| 83 | 84 | - **Un serveur qui ne répond pas.** mm le dit et demande le nom du modèle : les réglages viennent alors de la seule table des familles. |
| 84 | 85 | - **Un fichier existant.** mm ne l'écrase jamais, et le dit avant de poser la moindre question : choisissez un autre nom avec `-o`. |
| @@ -51,7 +51,7 @@ Ce guide montre comment écrire un nouvel agent.yaml pour un modèle et une tâc | |||
| 51 | ↑↓ choose · Enter next · Esc back · PgUp/PgDn preview · Ctrl-C quit | 51 | ↑↓ choose · Enter next · Esc back · PgUp/PgDn preview · Ctrl-C quit |
| 52 | ``` | 52 | ``` |
| 53 | 53 | ||
| 54 | - `↑` `↓` choisissent, `Entrée` répond et passe à la question suivante, `Échap` revient d'une question en arrière, `PgUp` `PgDn` font défiler l'aperçu, `Ctrl-C` quitte sans rien écrire. Une question de texte (le nom, le fichier) se tape ; `Ctrl-U` l'efface. Sur un écran de moins de 100 colonnes, `Tab` affiche l'aperçu à la place de la question. Quand toutes les questions ont une réponse, `Entrée` écrit le fichier. | 54 | + `↑` `↓` choisissent, `Entrée` répond et passe à la question suivante, `Échap` revient d'une question en arrière, `PgUp` `PgDn` font défiler l'aperçu, `Ctrl-C` quitte sans rien écrire. Une question de texte (le nom, le fichier ; l'endpoint et la variable de la clé pour `openai`) se tape ; `←` `→` déplacent le curseur, `Début` `Fin` vont à ses bouts, `Suppr` efface le caractère après le curseur, `Ctrl-U` efface tout. Sur un écran de moins de 100 colonnes, `Tab` affiche l'aperçu à la place de la question. Quand toutes les questions ont une réponse, `Entrée` écrit le fichier. |
| 55 | 55 | ||
| 56 | Les questions : le serveur de modèles, le modèle (dans la liste du serveur), sa famille (détectée d'après le nom), à quoi sert l'agent (developer, reviewer, translator, writer, assistant, researcher, docs, json, coordinator), quelle machine fait tourner le modèle (small, medium, large), s'il faut demander avant chaque commande ou modification de fichier, le nom de l'agent et le fichier. | 56 | Les questions : le serveur de modèles, le modèle (dans la liste du serveur), sa famille (détectée d'après le nom), à quoi sert l'agent (developer, reviewer, translator, writer, assistant, researcher, docs, json, coordinator), quelle machine fait tourner le modèle (small, medium, large), s'il faut demander avant chaque commande ou modification de fichier, le nom de l'agent et le fichier. |
| 57 | 57 | ||
| @@ -79,6 +79,7 @@ Ce guide montre comment écrire un nouvel agent.yaml pour un modèle et une tâc | |||
| 79 | ``` | 79 | ``` |
| 80 | 80 | ||
| 81 | - **Voir le fichier sans l'écrire.** `-o -` l'affiche sur la sortie standard. | 81 | - **Voir le fichier sans l'écrire.** `-o -` l'affiche sur la sortie standard. |
| 82 | +- **Un service compatible OpenAI (OpenAI, Cerebras, Hugging Face…).** Choisissez `openai` : deux questions de plus suivent, l'endpoint (`https://api.openai.com/v1` par défaut) et le nom de la variable qui contient la clé d'API (`OPENAI_API_KEY` par défaut, `CEREBRAS_API_KEY`, `HF_TOKEN`…). Exportez la clé avant de lancer `mm init` : avec elle, la question du modèle liste les modèles du service ; sans elle, vous tapez l'ID du modèle (la question dit que la variable n'est pas renseignée). Options : `--provider openai --base-url https://api.cerebras.ai/v1 --api-key-env CEREBRAS_API_KEY`. Le fichier reçoit `baseUrl` et `apiKeyEnv`, et un `reasoning_effort: none` en commentaire dans `sampling`, à décommenter si l'API répond « Function tools with reasoning_effort are not supported ». | ||
| 82 | - **Un serveur ailleurs.** `--base-url http://192.168.1.20:17434/v1` ; il est écrit dans le fichier comme `baseUrl`. Sans lui, le fichier n'a pas de `baseUrl` et garde en commentaire l'adresse par défaut du fournisseur et son fallback, prêts à être décommentés. | 83 | - **Un serveur ailleurs.** `--base-url http://192.168.1.20:17434/v1` ; il est écrit dans le fichier comme `baseUrl`. Sans lui, le fichier n'a pas de `baseUrl` et garde en commentaire l'adresse par défaut du fournisseur et son fallback, prêts à être décommentés. |
| 83 | - **Un serveur qui ne répond pas.** mm le dit et demande le nom du modèle : les réglages viennent alors de la seule table des familles. | 84 | - **Un serveur qui ne répond pas.** mm le dit et demande le nom du modèle : les réglages viennent alors de la seule table des familles. |
| 84 | - **Un fichier existant.** mm ne l'écrase jamais, et le dit avant de poser la moindre question : choisissez un autre nom avec `-o`. | 85 | - **Un fichier existant.** mm ne l'écrase jamais, et le dit avant de poser la moindre question : choisissez un autre nom avec `-o`. |
modified
docs/fr/how-to/run-with-an-openai-compatible-server.md +2 -1 | @@ -29,11 +29,12 @@ Ce guide montre comment faire tourner mini-me sur un serveur de modèles qui n'a | ||
| 29 | 29 | 🤖 The command printed: `hello-from-openai-provider` |
| 30 | 30 | ``` |
| 31 | 31 | |
| 32 | -`mm init --provider openai --base-url <adresse>` écrit ce fichier pour vous (voir [Créer un agent avec `mm init`](create-an-agent.md)). | |
| 32 | +`mm init` écrit ce fichier pour vous : choisissez `openai`, puis donnez l'endpoint et le nom de la variable qui contient la clé (ou `--provider openai --base-url <adresse> --api-key-env <VARIABLE>`) (voir [Créer un agent avec `mm init`](create-an-agent.md)). | |
| 33 | 33 | |
| 34 | 34 | ## Variantes |
| 35 | 35 | |
| 36 | 36 | - **L'API d'OpenAI.** Pas de `baseUrl`, `OPENAI_API_KEY` exportée, un modèle comme `gpt-5-mini`. Ses modèles à raisonnement n'acceptent que la température par défaut et prennent `max_completion_tokens` au lieu de `max_tokens` : écrivez `temperature: 1.0` et `max_completion_tokens` dans `sampling`. Avec `provider: openai`, mm envoie ce bloc tel quel, sans ses propres valeurs par défaut (`top_p`, `max_tokens`) : retirez seulement une clé que l'API refuserait encore. `mm init` écrit un tel bloc. (Le contenu de la requête a été vérifié contre un serveur local ; aucun appel à l'API OpenAI n'a été fait dans ce dépôt.) |
| 37 | +- **« Function tools with reasoning_effort are not supported ».** Certains modèles OpenAI (vu : `gpt-6-luna`) refusent les outils sur `/v1/chat/completions` pendant qu'ils raisonnent ; mm utilise cette route et envoie toujours des outils. Ajoutez `reasoning_effort: none` à `sampling` (cela coupe le raisonnement). Non vérifié dans ce dépôt : signalé par un utilisateur. | |
| 37 | 38 | - **La clé est refusée.** L'avertissement de démarrage indique `openai: <baseUrl> refused the key (HTTP 401) — export OPENAI_API_KEY`. |
| 38 | 39 | - **Rien ne répond.** L'avertissement indique `openai: nothing answers at <baseUrl> — check baseUrl: …` : l'adresse doit être celle compatible OpenAI, qui finit généralement par `/v1`. |
| 39 | 40 | - **Un serveur pour lequel mm a un fournisseur.** Préférez-le (`dmr`, `llamacpp`, `llmman`, `ollama`) : ceux-là savent comment le serveur lit un long prompt, coupent le raisonnement pour un résumé, déchargent un modèle et donnent la taille de la fenêtre. Le fournisseur générique ne fait rien de tout cela. |
| @@ -29,11 +29,12 @@ Ce guide montre comment faire tourner mini-me sur un serveur de modèles qui n'a | |||
| 29 | 🤖 The command printed: `hello-from-openai-provider` | 29 | 🤖 The command printed: `hello-from-openai-provider` |
| 30 | ``` | 30 | ``` |
| 31 | 31 | ||
| 32 | -`mm init --provider openai --base-url <adresse>` écrit ce fichier pour vous (voir [Créer un agent avec `mm init`](create-an-agent.md)). | 32 | +`mm init` écrit ce fichier pour vous : choisissez `openai`, puis donnez l'endpoint et le nom de la variable qui contient la clé (ou `--provider openai --base-url <adresse> --api-key-env <VARIABLE>`) (voir [Créer un agent avec `mm init`](create-an-agent.md)). |
| 33 | 33 | ||
| 34 | ## Variantes | 34 | ## Variantes |
| 35 | 35 | ||
| 36 | - **L'API d'OpenAI.** Pas de `baseUrl`, `OPENAI_API_KEY` exportée, un modèle comme `gpt-5-mini`. Ses modèles à raisonnement n'acceptent que la température par défaut et prennent `max_completion_tokens` au lieu de `max_tokens` : écrivez `temperature: 1.0` et `max_completion_tokens` dans `sampling`. Avec `provider: openai`, mm envoie ce bloc tel quel, sans ses propres valeurs par défaut (`top_p`, `max_tokens`) : retirez seulement une clé que l'API refuserait encore. `mm init` écrit un tel bloc. (Le contenu de la requête a été vérifié contre un serveur local ; aucun appel à l'API OpenAI n'a été fait dans ce dépôt.) | 36 | - **L'API d'OpenAI.** Pas de `baseUrl`, `OPENAI_API_KEY` exportée, un modèle comme `gpt-5-mini`. Ses modèles à raisonnement n'acceptent que la température par défaut et prennent `max_completion_tokens` au lieu de `max_tokens` : écrivez `temperature: 1.0` et `max_completion_tokens` dans `sampling`. Avec `provider: openai`, mm envoie ce bloc tel quel, sans ses propres valeurs par défaut (`top_p`, `max_tokens`) : retirez seulement une clé que l'API refuserait encore. `mm init` écrit un tel bloc. (Le contenu de la requête a été vérifié contre un serveur local ; aucun appel à l'API OpenAI n'a été fait dans ce dépôt.) |
| 37 | +- **« Function tools with reasoning_effort are not supported ».** Certains modèles OpenAI (vu : `gpt-6-luna`) refusent les outils sur `/v1/chat/completions` pendant qu'ils raisonnent ; mm utilise cette route et envoie toujours des outils. Ajoutez `reasoning_effort: none` à `sampling` (cela coupe le raisonnement). Non vérifié dans ce dépôt : signalé par un utilisateur. | ||
| 37 | - **La clé est refusée.** L'avertissement de démarrage indique `openai: <baseUrl> refused the key (HTTP 401) — export OPENAI_API_KEY`. | 38 | - **La clé est refusée.** L'avertissement de démarrage indique `openai: <baseUrl> refused the key (HTTP 401) — export OPENAI_API_KEY`. |
| 38 | - **Rien ne répond.** L'avertissement indique `openai: nothing answers at <baseUrl> — check baseUrl: …` : l'adresse doit être celle compatible OpenAI, qui finit généralement par `/v1`. | 39 | - **Rien ne répond.** L'avertissement indique `openai: nothing answers at <baseUrl> — check baseUrl: …` : l'adresse doit être celle compatible OpenAI, qui finit généralement par `/v1`. |
| 39 | - **Un serveur pour lequel mm a un fournisseur.** Préférez-le (`dmr`, `llamacpp`, `llmman`, `ollama`) : ceux-là savent comment le serveur lit un long prompt, coupent le raisonnement pour un résumé, déchargent un modèle et donnent la taille de la fenêtre. Le fournisseur générique ne fait rien de tout cela. | 40 | - **Un serveur pour lequel mm a un fournisseur.** Préférez-le (`dmr`, `llamacpp`, `llmman`, `ollama`) : ceux-là savent comment le serveur lit un long prompt, coupent le raisonnement pour un résumé, déchargent un modèle et donnent la taille de la fenêtre. Le fournisseur générique ne fait rien de tout cela. |
modified
docs/fr/reference/cli.md +4 -3 | @@ -192,8 +192,9 @@ Un tour annulé par l'utilisateur ne signale rien. `demos/struggle-signals/` dé | ||
| 192 | 192 | | Option | Question quand elle est absente | Défaut | |
| 193 | 193 | |--------|---------------------------------|--------| |
| 194 | 194 | | `--provider` | Serveur de modèles : `llmman`, `ollama`, `dmr`, `llamacpp`, `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) | |
| 195 | -| `--base-url` | — (pas posée) | l'adresse du fournisseur, ou son adresse de secours quand elle ne répond pas | | |
| 196 | -| `--model` | Modèle : la liste des modèles du serveur (`/api/tags` pour llmman et Ollama, `/models` pour les autres), ou un nom à taper quand le serveur ne répond pas | le premier de la liste | | |
| 195 | +| `--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 | | |
| 196 | +| `--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 | | |
| 197 | +| `--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 | | |
| 197 | 198 | | `--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 | |
| 198 | 199 | | `--usage` | À quoi sert l'agent : `developer`, `reviewer`, `translator`, `writer`, `assistant`, `researcher`, `docs`, `json`, `coordinator` | `developer` | |
| 199 | 200 | | `--machine` | Quelle machine fait tourner le modèle : `small`, `medium`, `large` | `small` | |
| @@ -206,7 +207,7 @@ Un tour annulé par l'utilisateur ne signale rien. `demos/struggle-signals/` dé | ||
| 206 | 207 | |
| 207 | 208 | | Aspect | Comportement | |
| 208 | 209 | |--------|--------------| |
| 209 | -| Fenêtre | Quand l'entrée et la sortie standard sont des terminaux (et ni `--yes` ni `--line`) : une fenêtre plein écran — les étapes (✓ répondue, ▸ en cours, · à venir, chaque réponse à côté de son étape), la question avec la description du choix surligné (les modèles sont listés par leur nom court, comme dans l'en-tête de la fenêtre — `Qwen3.5-4B:Q4_K_M` — et l'identifiant complet du modèle surligné s'affiche sous la liste), et le fichier tel qu'il serait écrit avec ce choix (aperçu à côté de la question à partir de 100 colonnes, à sa place avec `Tab` en dessous). Touches : `↑` `↓` `Début` `Fin` choisissent, `Entrée` répond, `y` / `n` répondent à la question oui ou non, `Échap` reprend la réponse précédente (les réponses données par option sont sautées), `PgUp` `PgDn` font défiler l'aperçu, `Ctrl-U` efface une réponse de texte, `Ctrl-C` quitte sans rien écrire (`nothing written`, code 1). Une réponse refusée s'affiche sur le trait du bas. Quand toutes les questions ont une réponse, `Entrée` écrit le fichier. | | |
| 210 | +| Fenêtre | Quand l'entrée et la sortie standard sont des terminaux (et ni `--yes` ni `--line`) : une fenêtre plein écran — les étapes (✓ répondue, ▸ en cours, · à venir, chaque réponse à côté de son étape), la question avec la description du choix surligné (les modèles sont listés par leur nom court, comme dans l'en-tête de la fenêtre — `Qwen3.5-4B:Q4_K_M` — et l'identifiant complet du modèle surligné s'affiche sous la liste), et le fichier tel qu'il serait écrit avec ce choix (aperçu à côté de la question à partir de 100 colonnes, à sa place avec `Tab` en dessous). Touches : `↑` `↓` `Début` `Fin` choisissent, `Entrée` répond, `y` / `n` répondent à la question oui ou non, `Échap` reprend la réponse précédente (les réponses données par option sont sautées), `PgUp` `PgDn` font défiler l'aperçu, `←` `→` déplacent le curseur dans une réponse de texte (`Début` `Fin` ou `Ctrl-A` `Ctrl-E` pour aller à ses bouts ; la saisie s'insère au curseur, `Retour arrière` efface avant lui, `Suppr` après), `Ctrl-U` efface une réponse de texte, `Ctrl-C` quitte sans rien écrire (`nothing written`, code 1). Une réponse refusée s'affiche sur le trait du bas. Quand toutes les questions ont une réponse, `Entrée` écrit le fichier. | | |
| 210 | 211 | | Mode ligne | Sinon : une question par ligne, choix numérotés ; un numéro, le texte d'une option ou un autre texte y répond ; une ligne vide ou `/skip` prend le défaut. Les usages et les machines sont d'abord listés avec une description d'une ligne. | |
| 211 | 212 | | Faits sur le modèle | llmman et Ollama : `POST /api/show` — la fenêtre native, l'échantillonnage du GGUF (`general.sampling.*`, les paramètres du Modelfile d'Ollama), l'interrupteur de raisonnement du chat template (`enable_thinking`, `reasoning_effort`) et son défaut. llama-server : `GET /props` — `n_ctx` et le chat template. Docker Model Runner et `openai` : rien — la table des familles décide ; `openai` liste ses modèles avec `OPENAI_API_KEY`. | |
| 212 | 213 | | Origine de chaque valeur | Les faits du serveur, puis la table des familles, puis l'usage, puis la ligne de la machine ; la table est `internal/agentinit/presets.yaml`, chaque entrée avec sa source et, par bloc d'échantillonnage, son `origin` (`measured`, `card` ou `rule`). | |
| @@ -192,8 +192,9 @@ Un tour annulé par l'utilisateur ne signale rien. `demos/struggle-signals/` dé | |||
| 192 | | Option | Question quand elle est absente | Défaut | | 192 | | Option | Question quand elle est absente | Défaut | |
| 193 | |--------|---------------------------------|--------| | 193 | |--------|---------------------------------|--------| |
| 194 | | `--provider` | Serveur de modèles : `llmman`, `ollama`, `dmr`, `llamacpp`, `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) | | 194 | | `--provider` | Serveur de modèles : `llmman`, `ollama`, `dmr`, `llamacpp`, `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) | |
| 195 | -| `--base-url` | — (pas posée) | l'adresse du fournisseur, ou son adresse de secours quand elle ne répond pas | | 195 | +| `--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 | |
| 196 | -| `--model` | Modèle : la liste des modèles du serveur (`/api/tags` pour llmman et Ollama, `/models` pour les autres), ou un nom à taper quand le serveur ne répond pas | le premier de la liste | | 196 | +| `--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 | |
| 197 | +| `--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 | | ||
| 197 | | `--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 | | 198 | | `--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 | |
| 198 | | `--usage` | À quoi sert l'agent : `developer`, `reviewer`, `translator`, `writer`, `assistant`, `researcher`, `docs`, `json`, `coordinator` | `developer` | | 199 | | `--usage` | À quoi sert l'agent : `developer`, `reviewer`, `translator`, `writer`, `assistant`, `researcher`, `docs`, `json`, `coordinator` | `developer` | |
| 199 | | `--machine` | Quelle machine fait tourner le modèle : `small`, `medium`, `large` | `small` | | 200 | | `--machine` | Quelle machine fait tourner le modèle : `small`, `medium`, `large` | `small` | |
| @@ -206,7 +207,7 @@ Un tour annulé par l'utilisateur ne signale rien. `demos/struggle-signals/` dé | |||
| 206 | 207 | ||
| 207 | | Aspect | Comportement | | 208 | | Aspect | Comportement | |
| 208 | |--------|--------------| | 209 | |--------|--------------| |
| 209 | -| Fenêtre | Quand l'entrée et la sortie standard sont des terminaux (et ni `--yes` ni `--line`) : une fenêtre plein écran — les étapes (✓ répondue, ▸ en cours, · à venir, chaque réponse à côté de son étape), la question avec la description du choix surligné (les modèles sont listés par leur nom court, comme dans l'en-tête de la fenêtre — `Qwen3.5-4B:Q4_K_M` — et l'identifiant complet du modèle surligné s'affiche sous la liste), et le fichier tel qu'il serait écrit avec ce choix (aperçu à côté de la question à partir de 100 colonnes, à sa place avec `Tab` en dessous). Touches : `↑` `↓` `Début` `Fin` choisissent, `Entrée` répond, `y` / `n` répondent à la question oui ou non, `Échap` reprend la réponse précédente (les réponses données par option sont sautées), `PgUp` `PgDn` font défiler l'aperçu, `Ctrl-U` efface une réponse de texte, `Ctrl-C` quitte sans rien écrire (`nothing written`, code 1). Une réponse refusée s'affiche sur le trait du bas. Quand toutes les questions ont une réponse, `Entrée` écrit le fichier. | | 210 | +| Fenêtre | Quand l'entrée et la sortie standard sont des terminaux (et ni `--yes` ni `--line`) : une fenêtre plein écran — les étapes (✓ répondue, ▸ en cours, · à venir, chaque réponse à côté de son étape), la question avec la description du choix surligné (les modèles sont listés par leur nom court, comme dans l'en-tête de la fenêtre — `Qwen3.5-4B:Q4_K_M` — et l'identifiant complet du modèle surligné s'affiche sous la liste), et le fichier tel qu'il serait écrit avec ce choix (aperçu à côté de la question à partir de 100 colonnes, à sa place avec `Tab` en dessous). Touches : `↑` `↓` `Début` `Fin` choisissent, `Entrée` répond, `y` / `n` répondent à la question oui ou non, `Échap` reprend la réponse précédente (les réponses données par option sont sautées), `PgUp` `PgDn` font défiler l'aperçu, `←` `→` déplacent le curseur dans une réponse de texte (`Début` `Fin` ou `Ctrl-A` `Ctrl-E` pour aller à ses bouts ; la saisie s'insère au curseur, `Retour arrière` efface avant lui, `Suppr` après), `Ctrl-U` efface une réponse de texte, `Ctrl-C` quitte sans rien écrire (`nothing written`, code 1). Une réponse refusée s'affiche sur le trait du bas. Quand toutes les questions ont une réponse, `Entrée` écrit le fichier. | |
| 210 | | Mode ligne | Sinon : une question par ligne, choix numérotés ; un numéro, le texte d'une option ou un autre texte y répond ; une ligne vide ou `/skip` prend le défaut. Les usages et les machines sont d'abord listés avec une description d'une ligne. | | 211 | | Mode ligne | Sinon : une question par ligne, choix numérotés ; un numéro, le texte d'une option ou un autre texte y répond ; une ligne vide ou `/skip` prend le défaut. Les usages et les machines sont d'abord listés avec une description d'une ligne. | |
| 211 | | Faits sur le modèle | llmman et Ollama : `POST /api/show` — la fenêtre native, l'échantillonnage du GGUF (`general.sampling.*`, les paramètres du Modelfile d'Ollama), l'interrupteur de raisonnement du chat template (`enable_thinking`, `reasoning_effort`) et son défaut. llama-server : `GET /props` — `n_ctx` et le chat template. Docker Model Runner et `openai` : rien — la table des familles décide ; `openai` liste ses modèles avec `OPENAI_API_KEY`. | | 212 | | Faits sur le modèle | llmman et Ollama : `POST /api/show` — la fenêtre native, l'échantillonnage du GGUF (`general.sampling.*`, les paramètres du Modelfile d'Ollama), l'interrupteur de raisonnement du chat template (`enable_thinking`, `reasoning_effort`) et son défaut. llama-server : `GET /props` — `n_ctx` et le chat template. Docker Model Runner et `openai` : rien — la table des familles décide ; `openai` liste ses modèles avec `OPENAI_API_KEY`. | |
| 212 | | Origine de chaque valeur | Les faits du serveur, puis la table des familles, puis l'usage, puis la ligne de la machine ; la table est `internal/agentinit/presets.yaml`, chaque entrée avec sa source et, par bloc d'échantillonnage, son `origin` (`measured`, `card` ou `rule`). | | 213 | | Origine de chaque valeur | Les faits du serveur, puis la table des familles, puis l'usage, puis la ligne de la machine ; la table est `internal/agentinit/presets.yaml`, chaque entrée avec sa source et, par bloc d'échantillonnage, son `origin` (`measured`, `card` ou `rule`). | |
modified
guides/raccourcis-clavier-tui.md +2 -1 | @@ -109,7 +109,8 @@ Quand l'agent vous pose une question (son outil `ask_user`), un petit formulaire | ||
| 109 | 109 | | `Échap` | Revient d'une question en arrière, en reprenant sa réponse. | |
| 110 | 110 | | `PgUp` `PgDn` | Font défiler l'aperçu. | |
| 111 | 111 | | `Tab` | Sur un écran de moins de 100 colonnes : affiche l'aperçu à la place de la question, et inversement. | |
| 112 | -| `Ctrl-U` | Efface une réponse de texte (le nom, le fichier). | | |
| 112 | +| `←` `→` | Dans une réponse de texte (l'endpoint, la variable de la clé, le nom, le fichier) : déplacent le curseur ; la saisie s'insère là. `Début` `Fin` (ou `Ctrl-A` `Ctrl-E`) vont au début et à la fin ; `Retour arrière` efface avant le curseur, `Suppr` après. | | |
| 113 | +| `Ctrl-U` | Efface une réponse de texte. | | |
| 113 | 114 | | `Ctrl-C` | Quitte sans rien écrire. | |
| 114 | 115 | |
| 115 | 116 | ## Bon à savoir |
| @@ -109,7 +109,8 @@ Quand l'agent vous pose une question (son outil `ask_user`), un petit formulaire | |||
| 109 | | `Échap` | Revient d'une question en arrière, en reprenant sa réponse. | | 109 | | `Échap` | Revient d'une question en arrière, en reprenant sa réponse. | |
| 110 | | `PgUp` `PgDn` | Font défiler l'aperçu. | | 110 | | `PgUp` `PgDn` | Font défiler l'aperçu. | |
| 111 | | `Tab` | Sur un écran de moins de 100 colonnes : affiche l'aperçu à la place de la question, et inversement. | | 111 | | `Tab` | Sur un écran de moins de 100 colonnes : affiche l'aperçu à la place de la question, et inversement. | |
| 112 | -| `Ctrl-U` | Efface une réponse de texte (le nom, le fichier). | | 112 | +| `←` `→` | Dans une réponse de texte (l'endpoint, la variable de la clé, le nom, le fichier) : déplacent le curseur ; la saisie s'insère là. `Début` `Fin` (ou `Ctrl-A` `Ctrl-E`) vont au début et à la fin ; `Retour arrière` efface avant le curseur, `Suppr` après. | |
| 113 | +| `Ctrl-U` | Efface une réponse de texte. | | ||
| 113 | | `Ctrl-C` | Quitte sans rien écrire. | | 114 | | `Ctrl-C` | Quitte sans rien écrire. | |
| 114 | 115 | ||
| 115 | ## Bon à savoir | 116 | ## Bon à savoir |
modified
guides/tui-keyboard-shortcuts.md +2 -1 | @@ -109,7 +109,8 @@ When the agent asks you something (its `ask_user` tool), a small form replaces t | ||
| 109 | 109 | | `Esc` | Go back one question, taking its answer back. | |
| 110 | 110 | | `PgUp` `PgDn` | Scroll the preview. | |
| 111 | 111 | | `Tab` | On a screen narrower than 100 columns: show the preview in place of the question, and back. | |
| 112 | -| `Ctrl-U` | Clear a text answer (the name, the file). | | |
| 112 | +| `←` `→` | In a text answer (the endpoint, the key's variable, the name, the file): move the cursor; typing inserts there. `Home` `End` (or `Ctrl-A` `Ctrl-E`) go to the start and the end; `Backspace` deletes before the cursor, `Delete` after it. | | |
| 113 | +| `Ctrl-U` | Clear a text answer. | | |
| 113 | 114 | | `Ctrl-C` | Quit without writing anything. | |
| 114 | 115 | |
| 115 | 116 | ## Good to know |
| @@ -109,7 +109,8 @@ When the agent asks you something (its `ask_user` tool), a small form replaces t | |||
| 109 | | `Esc` | Go back one question, taking its answer back. | | 109 | | `Esc` | Go back one question, taking its answer back. | |
| 110 | | `PgUp` `PgDn` | Scroll the preview. | | 110 | | `PgUp` `PgDn` | Scroll the preview. | |
| 111 | | `Tab` | On a screen narrower than 100 columns: show the preview in place of the question, and back. | | 111 | | `Tab` | On a screen narrower than 100 columns: show the preview in place of the question, and back. | |
| 112 | -| `Ctrl-U` | Clear a text answer (the name, the file). | | 112 | +| `←` `→` | In a text answer (the endpoint, the key's variable, the name, the file): move the cursor; typing inserts there. `Home` `End` (or `Ctrl-A` `Ctrl-E`) go to the start and the end; `Backspace` deletes before the cursor, `Delete` after it. | |
| 113 | +| `Ctrl-U` | Clear a text answer. | | ||
| 113 | | `Ctrl-C` | Quit without writing anything. | | 114 | | `Ctrl-C` | Quit without writing anything. | |
| 114 | 115 | ||
| 115 | ## Good to know | 116 | ## Good to know |
modified
internal/agentinit/decide.go +10 -7 | @@ -7,14 +7,17 @@ import ( | ||
| 7 | 7 | |
| 8 | 8 | // Choices are the wizard's answers. |
| 9 | 9 | type Choices struct { |
| 10 | - Provider string // dmr, llmman, ollama, llamacpp | |
| 10 | + Provider string // llmman, ollama, dmr, llamacpp, openai | |
| 11 | 11 | BaseURL string // "" = the provider's default |
| 12 | - Model string | |
| 13 | - Usage string | |
| 14 | - Machine string | |
| 15 | - Family string // "" = detected from the model's name | |
| 16 | - Name string // acp.name | |
| 17 | - Yolo *bool // nil = the usage's default | |
| 12 | + // APIKeyEnv names the variable holding the API key of an | |
| 13 | + // OpenAI-compatible service (provider openai); "" = OPENAI_API_KEY. | |
| 14 | + APIKeyEnv string | |
| 15 | + Model string | |
| 16 | + Usage string | |
| 17 | + Machine string | |
| 18 | + Family string // "" = detected from the model's name | |
| 19 | + Name string // acp.name | |
| 20 | + Yolo *bool // nil = the usage's default | |
| 18 | 21 | } |
| 19 | 22 | |
| 20 | 23 | // Setting is one value of the generated file with the reason it has that |
| @@ -7,14 +7,17 @@ import ( | |||
| 7 | 7 | ||
| 8 | // Choices are the wizard's answers. | 8 | // Choices are the wizard's answers. |
| 9 | type Choices struct { | 9 | type Choices struct { |
| 10 | - Provider string // dmr, llmman, ollama, llamacpp | 10 | + Provider string // llmman, ollama, dmr, llamacpp, openai |
| 11 | BaseURL string // "" = the provider's default | 11 | BaseURL string // "" = the provider's default |
| 12 | - Model string | 12 | + // APIKeyEnv names the variable holding the API key of an |
| 13 | - Usage string | 13 | + // OpenAI-compatible service (provider openai); "" = OPENAI_API_KEY. |
| 14 | - Machine string | 14 | + APIKeyEnv string |
| 15 | - Family string // "" = detected from the model's name | 15 | + Model string |
| 16 | - Name string // acp.name | 16 | + Usage string |
| 17 | - Yolo *bool // nil = the usage's default | 17 | + Machine string |
| 18 | + Family string // "" = detected from the model's name | ||
| 19 | + Name string // acp.name | ||
| 20 | + Yolo *bool // nil = the usage's default | ||
| 18 | } | 21 | } |
| 19 | 22 | ||
| 20 | // Setting is one value of the generated file with the reason it has that | 23 | // Setting is one value of the generated file with the reason it has that |
modified
internal/agentinit/facts.go +30 -4 | @@ -89,12 +89,38 @@ func call(ctx context.Context, method, url string, body any, timeout time.Durati | ||
| 89 | 89 | } |
| 90 | 90 | |
| 91 | 91 | // Models lists the models a server serves: llmman's and Ollama's /api/tags, |
| 92 | -// the OpenAI /models of the others. Sorted. | |
| 92 | +// the OpenAI /models of the others. Sorted. For `openai` the key is read | |
| 93 | +// from OPENAI_API_KEY; ListModels takes it from elsewhere. | |
| 93 | 94 | // |
| 94 | 95 | // names, err := agentinit.Models(ctx, "llmman", "http://127.0.0.1:17434/v1") |
| 95 | 96 | func Models(ctx context.Context, provider, base string) ([]string, error) { |
| 97 | + key := "" | |
| 96 | 98 | if provider == "openai" { |
| 97 | - ctx = withKey(ctx, os.Getenv("OPENAI_API_KEY")) | |
| 99 | + key = os.Getenv("OPENAI_API_KEY") | |
| 100 | + } | |
| 101 | + return ListModels(ctx, provider, base, key) | |
| 102 | +} | |
| 103 | + | |
| 104 | +// Timeouts of a model list: a local server answers at once or is not | |
| 105 | +// there — the first question probes four of them, so it must stay short; a | |
| 106 | +// remote service (provider openai) may take seconds on a first, cold | |
| 107 | +// connection (DNS, TLS). Ticket 177: with 5 s, the OpenAI list came back | |
| 108 | +// empty on a first `mm init` and full on the second. | |
| 109 | +const ( | |
| 110 | + localListTimeout = 5 * time.Second | |
| 111 | + remoteListTimeout = 20 * time.Second | |
| 112 | +) | |
| 113 | + | |
| 114 | +// ListModels is Models with the API key given (sent as a Bearer token when | |
| 115 | +// not empty) — the key of an OpenAI-compatible service, read from the | |
| 116 | +// variable the user named. | |
| 117 | +// | |
| 118 | +// names, err := agentinit.ListModels(ctx, "openai", "https://api.cerebras.ai/v1", os.Getenv("CEREBRAS_API_KEY")) | |
| 119 | +func ListModels(ctx context.Context, provider, base, key string) ([]string, error) { | |
| 120 | + ctx = withKey(ctx, key) | |
| 121 | + timeout := localListTimeout | |
| 122 | + if provider == "openai" { | |
| 123 | + timeout = remoteListTimeout | |
| 98 | 124 | } |
| 99 | 125 | var names []string |
| 100 | 126 | switch provider { |
| @@ -104,7 +130,7 @@ func Models(ctx context.Context, provider, base string) ([]string, error) { | ||
| 104 | 130 | Name string `json:"name"` |
| 105 | 131 | } `json:"models"` |
| 106 | 132 | } |
| 107 | - if err := call(ctx, http.MethodGet, root(base)+"/api/tags", nil, 5*time.Second, &tags); err != nil { | |
| 133 | + if err := call(ctx, http.MethodGet, root(base)+"/api/tags", nil, timeout, &tags); err != nil { | |
| 108 | 134 | return nil, err |
| 109 | 135 | } |
| 110 | 136 | for _, m := range tags.Models { |
| @@ -116,7 +142,7 @@ func Models(ctx context.Context, provider, base string) ([]string, error) { | ||
| 116 | 142 | ID string `json:"id"` |
| 117 | 143 | } `json:"data"` |
| 118 | 144 | } |
| 119 | - if err := call(ctx, http.MethodGet, strings.TrimRight(base, "/")+"/models", nil, 5*time.Second, &list); err != nil { | |
| 145 | + if err := call(ctx, http.MethodGet, strings.TrimRight(base, "/")+"/models", nil, timeout, &list); err != nil { | |
| 120 | 146 | return nil, err |
| 121 | 147 | } |
| 122 | 148 | for _, m := range list.Data { |
| @@ -89,12 +89,38 @@ func call(ctx context.Context, method, url string, body any, timeout time.Durati | |||
| 89 | } | 89 | } |
| 90 | 90 | ||
| 91 | // Models lists the models a server serves: llmman's and Ollama's /api/tags, | 91 | // Models lists the models a server serves: llmman's and Ollama's /api/tags, |
| 92 | -// the OpenAI /models of the others. Sorted. | 92 | +// the OpenAI /models of the others. Sorted. For `openai` the key is read |
| 93 | +// from OPENAI_API_KEY; ListModels takes it from elsewhere. | ||
| 93 | // | 94 | // |
| 94 | // names, err := agentinit.Models(ctx, "llmman", "http://127.0.0.1:17434/v1") | 95 | // names, err := agentinit.Models(ctx, "llmman", "http://127.0.0.1:17434/v1") |
| 95 | func Models(ctx context.Context, provider, base string) ([]string, error) { | 96 | func Models(ctx context.Context, provider, base string) ([]string, error) { |
| 97 | + key := "" | ||
| 96 | if provider == "openai" { | 98 | if provider == "openai" { |
| 97 | - ctx = withKey(ctx, os.Getenv("OPENAI_API_KEY")) | 99 | + key = os.Getenv("OPENAI_API_KEY") |
| 100 | + } | ||
| 101 | + return ListModels(ctx, provider, base, key) | ||
| 102 | +} | ||
| 103 | + | ||
| 104 | +// Timeouts of a model list: a local server answers at once or is not | ||
| 105 | +// there — the first question probes four of them, so it must stay short; a | ||
| 106 | +// remote service (provider openai) may take seconds on a first, cold | ||
| 107 | +// connection (DNS, TLS). Ticket 177: with 5 s, the OpenAI list came back | ||
| 108 | +// empty on a first `mm init` and full on the second. | ||
| 109 | +const ( | ||
| 110 | + localListTimeout = 5 * time.Second | ||
| 111 | + remoteListTimeout = 20 * time.Second | ||
| 112 | +) | ||
| 113 | + | ||
| 114 | +// ListModels is Models with the API key given (sent as a Bearer token when | ||
| 115 | +// not empty) — the key of an OpenAI-compatible service, read from the | ||
| 116 | +// variable the user named. | ||
| 117 | +// | ||
| 118 | +// names, err := agentinit.ListModels(ctx, "openai", "https://api.cerebras.ai/v1", os.Getenv("CEREBRAS_API_KEY")) | ||
| 119 | +func ListModels(ctx context.Context, provider, base, key string) ([]string, error) { | ||
| 120 | + ctx = withKey(ctx, key) | ||
| 121 | + timeout := localListTimeout | ||
| 122 | + if provider == "openai" { | ||
| 123 | + timeout = remoteListTimeout | ||
| 98 | } | 124 | } |
| 99 | var names []string | 125 | var names []string |
| 100 | switch provider { | 126 | switch provider { |
| @@ -104,7 +130,7 @@ func Models(ctx context.Context, provider, base string) ([]string, error) { | |||
| 104 | Name string `json:"name"` | 130 | Name string `json:"name"` |
| 105 | } `json:"models"` | 131 | } `json:"models"` |
| 106 | } | 132 | } |
| 107 | - if err := call(ctx, http.MethodGet, root(base)+"/api/tags", nil, 5*time.Second, &tags); err != nil { | 133 | + if err := call(ctx, http.MethodGet, root(base)+"/api/tags", nil, timeout, &tags); err != nil { |
| 108 | return nil, err | 134 | return nil, err |
| 109 | } | 135 | } |
| 110 | for _, m := range tags.Models { | 136 | for _, m := range tags.Models { |
| @@ -116,7 +142,7 @@ func Models(ctx context.Context, provider, base string) ([]string, error) { | |||
| 116 | ID string `json:"id"` | 142 | ID string `json:"id"` |
| 117 | } `json:"data"` | 143 | } `json:"data"` |
| 118 | } | 144 | } |
| 119 | - if err := call(ctx, http.MethodGet, strings.TrimRight(base, "/")+"/models", nil, 5*time.Second, &list); err != nil { | 145 | + if err := call(ctx, http.MethodGet, strings.TrimRight(base, "/")+"/models", nil, timeout, &list); err != nil { |
| 120 | return nil, err | 146 | return nil, err |
| 121 | } | 147 | } |
| 122 | for _, m := range list.Data { | 148 | for _, m := range list.Data { |
modified
internal/agentinit/flow.go +114 -15 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/agentinit/openai_flow_test.go +178 -0 | new file mode 100644 | ||
| @@ -0,0 +1,178 @@ | ||
| 1 | +package agentinit | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "context" | |
| 5 | + "encoding/json" | |
| 6 | + "net/http" | |
| 7 | + "net/http/httptest" | |
| 8 | + "strings" | |
| 9 | + "testing" | |
| 10 | + | |
| 11 | + "mm/internal/elicit" | |
| 12 | +) | |
| 13 | + | |
| 14 | +// openaiService is a fake OpenAI-compatible service: GET /v1/models answers | |
| 15 | +// two models to the key sk-good, 401 to anything else. calls counts the | |
| 16 | +// requests, auth keeps the last Authorization header. | |
| 17 | +type openaiService struct { | |
| 18 | + url string | |
| 19 | + calls int | |
| 20 | + auth string | |
| 21 | +} | |
| 22 | + | |
| 23 | +func newOpenAIService(t *testing.T) *openaiService { | |
| 24 | + t.Helper() | |
| 25 | + s := &openaiService{} | |
| 26 | + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 27 | + s.calls++ | |
| 28 | + s.auth = r.Header.Get("Authorization") | |
| 29 | + if s.auth != "Bearer sk-good" { | |
| 30 | + w.WriteHeader(http.StatusUnauthorized) | |
| 31 | + return | |
| 32 | + } | |
| 33 | + _ = json.NewEncoder(w).Encode(map[string]any{"data": []any{map[string]any{"id": "llama-4-scout"}, map[string]any{"id": "gpt-oss-120b"}}}) | |
| 34 | + })) | |
| 35 | + t.Cleanup(srv.Close) | |
| 36 | + s.url = srv.URL + "/v1" | |
| 37 | + return s | |
| 38 | +} | |
| 39 | + | |
| 40 | +func openaiFlow(t *testing.T, given Choices) *Flow { | |
| 41 | + t.Helper() | |
| 42 | + keepConfig(t) | |
| 43 | + p, err := LoadPresets() | |
| 44 | + if err != nil { | |
| 45 | + t.Fatal(err) | |
| 46 | + } | |
| 47 | + given.Provider = "openai" | |
| 48 | + return NewFlow(context.Background(), p, given, "") | |
| 49 | +} | |
| 50 | + | |
| 51 | +// Ticket 177: for openai — any OpenAI-compatible service — the endpoint and | |
| 52 | +// the key's variable are asked between the server and the model; for the | |
| 53 | +// other providers they are not. | |
| 54 | +func TestOpenAIAsksTheEndpointAndTheKeyVariable(t *testing.T) { | |
| 55 | + f := openaiFlow(t, Choices{}) | |
| 56 | + if got := keys(f.Pending()); !strings.HasPrefix(got, "endpoint,keyenv,model,") { | |
| 57 | + t.Errorf("pending = %s, want endpoint, keyenv, then the model", got) | |
| 58 | + } | |
| 59 | + if st := f.Step("endpoint"); st.Kind != elicit.Text || st.Default != "https://api.openai.com/v1" { | |
| 60 | + t.Errorf("endpoint step: %+v", st) | |
| 61 | + } | |
| 62 | + if st := f.Step("keyenv"); st.Kind != elicit.Text || st.Default != "OPENAI_API_KEY" { | |
| 63 | + t.Errorf("keyenv step: %+v", st) | |
| 64 | + } | |
| 65 | + | |
| 66 | + other := NewFlow(context.Background(), f.presets, Choices{Provider: "llmman", BaseURL: llmmanStub(t)}, "") | |
| 67 | + if got := keys(other.Pending()); strings.Contains(got, "endpoint") || strings.Contains(got, "keyenv") { | |
| 68 | + t.Errorf("llmman pending = %s, want no endpoint or key question", got) | |
| 69 | + } | |
| 70 | +} | |
| 71 | + | |
| 72 | +func TestOpenAIListsTheModelsWithTheNamedKey(t *testing.T) { | |
| 73 | + svc := newOpenAIService(t) | |
| 74 | + t.Setenv("CEREBRAS_API_KEY", "sk-good") | |
| 75 | + f := openaiFlow(t, Choices{}) | |
| 76 | + for k, v := range map[string]string{"endpoint": svc.url, "keyenv": "CEREBRAS_API_KEY"} { | |
| 77 | + if err := f.Set(k, v); err != nil { | |
| 78 | + t.Fatal(err) | |
| 79 | + } | |
| 80 | + } | |
| 81 | + st := f.Step("model") | |
| 82 | + if st.Kind != elicit.Choice || len(st.Options) != 2 || st.Default != "gpt-oss-120b" { | |
| 83 | + t.Errorf("model step: %+v, want the two models of the service", st) | |
| 84 | + } | |
| 85 | + if svc.auth != "Bearer sk-good" { | |
| 86 | + t.Errorf("Authorization = %q, want the key read from CEREBRAS_API_KEY", svc.auth) | |
| 87 | + } | |
| 88 | +} | |
| 89 | + | |
| 90 | +func TestOpenAIWithoutTheKeyAsksToTypeTheModel(t *testing.T) { | |
| 91 | + svc := newOpenAIService(t) | |
| 92 | + t.Setenv("HF_TOKEN", "") | |
| 93 | + f := openaiFlow(t, Choices{BaseURL: svc.url, APIKeyEnv: "HF_TOKEN"}) | |
| 94 | + st := f.Step("model") | |
| 95 | + if st.Kind != elicit.Text || !strings.Contains(st.Prompt, "HF_TOKEN is not set") { | |
| 96 | + t.Errorf("model step: %+v, want a text question saying HF_TOKEN is not set", st) | |
| 97 | + } | |
| 98 | + if svc.calls != 0 { | |
| 99 | + t.Errorf("the service was called %d time(s) without a key", svc.calls) | |
| 100 | + } | |
| 101 | + if err := f.Set("model", "meta-llama/Llama-3.3-70B-Instruct"); err != nil { | |
| 102 | + t.Fatal(err) | |
| 103 | + } | |
| 104 | +} | |
| 105 | + | |
| 106 | +func TestOpenAIRefusedListSaysWhyInTheQuestion(t *testing.T) { | |
| 107 | + svc := newOpenAIService(t) | |
| 108 | + t.Setenv("OPENAI_API_KEY", "sk-wrong") | |
| 109 | + f := openaiFlow(t, Choices{BaseURL: svc.url}) | |
| 110 | + st := f.Step("model") | |
| 111 | + if st.Kind != elicit.Text || !strings.Contains(st.Prompt, "no list from") || !strings.Contains(st.Prompt, "401") { | |
| 112 | + t.Errorf("model step prompt = %q, want the refusal in it", st.Prompt) | |
| 113 | + } | |
| 114 | +} | |
| 115 | + | |
| 116 | +func TestOpenAIEndpointAndKeyAnswersAreChecked(t *testing.T) { | |
| 117 | + f := openaiFlow(t, Choices{}) | |
| 118 | + if err := f.Set("endpoint", "api.cerebras.ai/v1"); err == nil { | |
| 119 | + t.Error("an endpoint without http(s):// was accepted") | |
| 120 | + } | |
| 121 | + if err := f.Set("keyenv", "MY-KEY"); err == nil { | |
| 122 | + t.Error("a variable name with a dash was accepted") | |
| 123 | + } | |
| 124 | + if err := f.Set("endpoint", "https://api.cerebras.ai/v1/"); err != nil || f.c.BaseURL != "https://api.cerebras.ai/v1" { | |
| 125 | + t.Errorf("endpoint: err %v, BaseURL %q, want the address without its trailing slash", err, f.c.BaseURL) | |
| 126 | + } | |
| 127 | + if err := f.Set("endpoint", "https://api.openai.com/v1"); err != nil || f.c.BaseURL != "" { | |
| 128 | + t.Errorf("the OpenAI API's own address: err %v, BaseURL %q, want none (the default)", err, f.c.BaseURL) | |
| 129 | + } | |
| 130 | + if err := f.Set("keyenv", "OPENAI_API_KEY"); err != nil || f.c.APIKeyEnv != "" { | |
| 131 | + t.Errorf("OPENAI_API_KEY: err %v, APIKeyEnv %q, want none (the default)", err, f.c.APIKeyEnv) | |
| 132 | + } | |
| 133 | +} | |
| 134 | + | |
| 135 | +// A new endpoint or key forgets the model; another server forgets both. | |
| 136 | +func TestOpenAIChangesForgetWhatDependsOnThem(t *testing.T) { | |
| 137 | + keepConfig(t) | |
| 138 | + p, _ := LoadPresets() | |
| 139 | + f := NewFlow(context.Background(), p, Choices{}, "") | |
| 140 | + for k, v := range map[string]string{"provider": "openai"} { | |
| 141 | + if err := f.Set(k, v); err != nil { | |
| 142 | + t.Fatal(err) | |
| 143 | + } | |
| 144 | + } | |
| 145 | + for _, kv := range [][2]string{{"endpoint", "https://api.cerebras.ai/v1"}, {"keyenv", "CEREBRAS_API_KEY"}, {"model", "llama-4-scout"}} { | |
| 146 | + if err := f.Set(kv[0], kv[1]); err != nil { | |
| 147 | + t.Fatal(err) | |
| 148 | + } | |
| 149 | + } | |
| 150 | + if err := f.Set("keyenv", "OTHER_KEY"); err != nil || f.Step("model").Value != "" { | |
| 151 | + t.Errorf("a new key variable kept the model %q", f.Step("model").Value) | |
| 152 | + } | |
| 153 | + if err := f.Set("provider", "llmman"); err != nil { | |
| 154 | + t.Fatal(err) | |
| 155 | + } | |
| 156 | + if f.c.BaseURL != "" || f.c.APIKeyEnv != "" { | |
| 157 | + t.Errorf("another server kept BaseURL %q, APIKeyEnv %q", f.c.BaseURL, f.c.APIKeyEnv) | |
| 158 | + } | |
| 159 | +} | |
| 160 | + | |
| 161 | +// The written file: baseUrl and apiKeyEnv when they are not the defaults, | |
| 162 | +// and the reasoning_effort line, commented, in sampling. | |
| 163 | +func TestOpenAIFileHasTheEndpointTheKeyAndTheReasoningHint(t *testing.T) { | |
| 164 | + keepConfig(t) | |
| 165 | + p, _ := LoadPresets() | |
| 166 | + c := Choices{Provider: "openai", BaseURL: "https://api.cerebras.ai/v1", APIKeyEnv: "CEREBRAS_API_KEY", Model: "llama-4-scout", Usage: "developer", Machine: "small"} | |
| 167 | + text := Render(Decide(p, c, Facts{})) | |
| 168 | + for _, want := range []string{`baseUrl: "https://api.cerebras.ai/v1"`, "apiKeyEnv: CEREBRAS_API_KEY", " # reasoning_effort: none"} { | |
| 169 | + if !strings.Contains(text, want) { | |
| 170 | + t.Errorf("the file has no %q:\n%s", want, text) | |
| 171 | + } | |
| 172 | + } | |
| 173 | + c.BaseURL, c.APIKeyEnv = "", "" | |
| 174 | + text = Render(Decide(p, c, Facts{})) | |
| 175 | + if strings.Contains(text, "\napiKeyEnv:") || strings.Contains(text, "\nbaseUrl:") { | |
| 176 | + t.Errorf("defaults written out:\n%s", text) | |
| 177 | + } | |
| 178 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,178 @@ | |||
| 1 | +package agentinit | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "context" | ||
| 5 | + "encoding/json" | ||
| 6 | + "net/http" | ||
| 7 | + "net/http/httptest" | ||
| 8 | + "strings" | ||
| 9 | + "testing" | ||
| 10 | + | ||
| 11 | + "mm/internal/elicit" | ||
| 12 | +) | ||
| 13 | + | ||
| 14 | +// openaiService is a fake OpenAI-compatible service: GET /v1/models answers | ||
| 15 | +// two models to the key sk-good, 401 to anything else. calls counts the | ||
| 16 | +// requests, auth keeps the last Authorization header. | ||
| 17 | +type openaiService struct { | ||
| 18 | + url string | ||
| 19 | + calls int | ||
| 20 | + auth string | ||
| 21 | +} | ||
| 22 | + | ||
| 23 | +func newOpenAIService(t *testing.T) *openaiService { | ||
| 24 | + t.Helper() | ||
| 25 | + s := &openaiService{} | ||
| 26 | + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| 27 | + s.calls++ | ||
| 28 | + s.auth = r.Header.Get("Authorization") | ||
| 29 | + if s.auth != "Bearer sk-good" { | ||
| 30 | + w.WriteHeader(http.StatusUnauthorized) | ||
| 31 | + return | ||
| 32 | + } | ||
| 33 | + _ = json.NewEncoder(w).Encode(map[string]any{"data": []any{map[string]any{"id": "llama-4-scout"}, map[string]any{"id": "gpt-oss-120b"}}}) | ||
| 34 | + })) | ||
| 35 | + t.Cleanup(srv.Close) | ||
| 36 | + s.url = srv.URL + "/v1" | ||
| 37 | + return s | ||
| 38 | +} | ||
| 39 | + | ||
| 40 | +func openaiFlow(t *testing.T, given Choices) *Flow { | ||
| 41 | + t.Helper() | ||
| 42 | + keepConfig(t) | ||
| 43 | + p, err := LoadPresets() | ||
| 44 | + if err != nil { | ||
| 45 | + t.Fatal(err) | ||
| 46 | + } | ||
| 47 | + given.Provider = "openai" | ||
| 48 | + return NewFlow(context.Background(), p, given, "") | ||
| 49 | +} | ||
| 50 | + | ||
| 51 | +// Ticket 177: for openai — any OpenAI-compatible service — the endpoint and | ||
| 52 | +// the key's variable are asked between the server and the model; for the | ||
| 53 | +// other providers they are not. | ||
| 54 | +func TestOpenAIAsksTheEndpointAndTheKeyVariable(t *testing.T) { | ||
| 55 | + f := openaiFlow(t, Choices{}) | ||
| 56 | + if got := keys(f.Pending()); !strings.HasPrefix(got, "endpoint,keyenv,model,") { | ||
| 57 | + t.Errorf("pending = %s, want endpoint, keyenv, then the model", got) | ||
| 58 | + } | ||
| 59 | + if st := f.Step("endpoint"); st.Kind != elicit.Text || st.Default != "https://api.openai.com/v1" { | ||
| 60 | + t.Errorf("endpoint step: %+v", st) | ||
| 61 | + } | ||
| 62 | + if st := f.Step("keyenv"); st.Kind != elicit.Text || st.Default != "OPENAI_API_KEY" { | ||
| 63 | + t.Errorf("keyenv step: %+v", st) | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + other := NewFlow(context.Background(), f.presets, Choices{Provider: "llmman", BaseURL: llmmanStub(t)}, "") | ||
| 67 | + if got := keys(other.Pending()); strings.Contains(got, "endpoint") || strings.Contains(got, "keyenv") { | ||
| 68 | + t.Errorf("llmman pending = %s, want no endpoint or key question", got) | ||
| 69 | + } | ||
| 70 | +} | ||
| 71 | + | ||
| 72 | +func TestOpenAIListsTheModelsWithTheNamedKey(t *testing.T) { | ||
| 73 | + svc := newOpenAIService(t) | ||
| 74 | + t.Setenv("CEREBRAS_API_KEY", "sk-good") | ||
| 75 | + f := openaiFlow(t, Choices{}) | ||
| 76 | + for k, v := range map[string]string{"endpoint": svc.url, "keyenv": "CEREBRAS_API_KEY"} { | ||
| 77 | + if err := f.Set(k, v); err != nil { | ||
| 78 | + t.Fatal(err) | ||
| 79 | + } | ||
| 80 | + } | ||
| 81 | + st := f.Step("model") | ||
| 82 | + if st.Kind != elicit.Choice || len(st.Options) != 2 || st.Default != "gpt-oss-120b" { | ||
| 83 | + t.Errorf("model step: %+v, want the two models of the service", st) | ||
| 84 | + } | ||
| 85 | + if svc.auth != "Bearer sk-good" { | ||
| 86 | + t.Errorf("Authorization = %q, want the key read from CEREBRAS_API_KEY", svc.auth) | ||
| 87 | + } | ||
| 88 | +} | ||
| 89 | + | ||
| 90 | +func TestOpenAIWithoutTheKeyAsksToTypeTheModel(t *testing.T) { | ||
| 91 | + svc := newOpenAIService(t) | ||
| 92 | + t.Setenv("HF_TOKEN", "") | ||
| 93 | + f := openaiFlow(t, Choices{BaseURL: svc.url, APIKeyEnv: "HF_TOKEN"}) | ||
| 94 | + st := f.Step("model") | ||
| 95 | + if st.Kind != elicit.Text || !strings.Contains(st.Prompt, "HF_TOKEN is not set") { | ||
| 96 | + t.Errorf("model step: %+v, want a text question saying HF_TOKEN is not set", st) | ||
| 97 | + } | ||
| 98 | + if svc.calls != 0 { | ||
| 99 | + t.Errorf("the service was called %d time(s) without a key", svc.calls) | ||
| 100 | + } | ||
| 101 | + if err := f.Set("model", "meta-llama/Llama-3.3-70B-Instruct"); err != nil { | ||
| 102 | + t.Fatal(err) | ||
| 103 | + } | ||
| 104 | +} | ||
| 105 | + | ||
| 106 | +func TestOpenAIRefusedListSaysWhyInTheQuestion(t *testing.T) { | ||
| 107 | + svc := newOpenAIService(t) | ||
| 108 | + t.Setenv("OPENAI_API_KEY", "sk-wrong") | ||
| 109 | + f := openaiFlow(t, Choices{BaseURL: svc.url}) | ||
| 110 | + st := f.Step("model") | ||
| 111 | + if st.Kind != elicit.Text || !strings.Contains(st.Prompt, "no list from") || !strings.Contains(st.Prompt, "401") { | ||
| 112 | + t.Errorf("model step prompt = %q, want the refusal in it", st.Prompt) | ||
| 113 | + } | ||
| 114 | +} | ||
| 115 | + | ||
| 116 | +func TestOpenAIEndpointAndKeyAnswersAreChecked(t *testing.T) { | ||
| 117 | + f := openaiFlow(t, Choices{}) | ||
| 118 | + if err := f.Set("endpoint", "api.cerebras.ai/v1"); err == nil { | ||
| 119 | + t.Error("an endpoint without http(s):// was accepted") | ||
| 120 | + } | ||
| 121 | + if err := f.Set("keyenv", "MY-KEY"); err == nil { | ||
| 122 | + t.Error("a variable name with a dash was accepted") | ||
| 123 | + } | ||
| 124 | + if err := f.Set("endpoint", "https://api.cerebras.ai/v1/"); err != nil || f.c.BaseURL != "https://api.cerebras.ai/v1" { | ||
| 125 | + t.Errorf("endpoint: err %v, BaseURL %q, want the address without its trailing slash", err, f.c.BaseURL) | ||
| 126 | + } | ||
| 127 | + if err := f.Set("endpoint", "https://api.openai.com/v1"); err != nil || f.c.BaseURL != "" { | ||
| 128 | + t.Errorf("the OpenAI API's own address: err %v, BaseURL %q, want none (the default)", err, f.c.BaseURL) | ||
| 129 | + } | ||
| 130 | + if err := f.Set("keyenv", "OPENAI_API_KEY"); err != nil || f.c.APIKeyEnv != "" { | ||
| 131 | + t.Errorf("OPENAI_API_KEY: err %v, APIKeyEnv %q, want none (the default)", err, f.c.APIKeyEnv) | ||
| 132 | + } | ||
| 133 | +} | ||
| 134 | + | ||
| 135 | +// A new endpoint or key forgets the model; another server forgets both. | ||
| 136 | +func TestOpenAIChangesForgetWhatDependsOnThem(t *testing.T) { | ||
| 137 | + keepConfig(t) | ||
| 138 | + p, _ := LoadPresets() | ||
| 139 | + f := NewFlow(context.Background(), p, Choices{}, "") | ||
| 140 | + for k, v := range map[string]string{"provider": "openai"} { | ||
| 141 | + if err := f.Set(k, v); err != nil { | ||
| 142 | + t.Fatal(err) | ||
| 143 | + } | ||
| 144 | + } | ||
| 145 | + for _, kv := range [][2]string{{"endpoint", "https://api.cerebras.ai/v1"}, {"keyenv", "CEREBRAS_API_KEY"}, {"model", "llama-4-scout"}} { | ||
| 146 | + if err := f.Set(kv[0], kv[1]); err != nil { | ||
| 147 | + t.Fatal(err) | ||
| 148 | + } | ||
| 149 | + } | ||
| 150 | + if err := f.Set("keyenv", "OTHER_KEY"); err != nil || f.Step("model").Value != "" { | ||
| 151 | + t.Errorf("a new key variable kept the model %q", f.Step("model").Value) | ||
| 152 | + } | ||
| 153 | + if err := f.Set("provider", "llmman"); err != nil { | ||
| 154 | + t.Fatal(err) | ||
| 155 | + } | ||
| 156 | + if f.c.BaseURL != "" || f.c.APIKeyEnv != "" { | ||
| 157 | + t.Errorf("another server kept BaseURL %q, APIKeyEnv %q", f.c.BaseURL, f.c.APIKeyEnv) | ||
| 158 | + } | ||
| 159 | +} | ||
| 160 | + | ||
| 161 | +// The written file: baseUrl and apiKeyEnv when they are not the defaults, | ||
| 162 | +// and the reasoning_effort line, commented, in sampling. | ||
| 163 | +func TestOpenAIFileHasTheEndpointTheKeyAndTheReasoningHint(t *testing.T) { | ||
| 164 | + keepConfig(t) | ||
| 165 | + p, _ := LoadPresets() | ||
| 166 | + c := Choices{Provider: "openai", BaseURL: "https://api.cerebras.ai/v1", APIKeyEnv: "CEREBRAS_API_KEY", Model: "llama-4-scout", Usage: "developer", Machine: "small"} | ||
| 167 | + text := Render(Decide(p, c, Facts{})) | ||
| 168 | + for _, want := range []string{`baseUrl: "https://api.cerebras.ai/v1"`, "apiKeyEnv: CEREBRAS_API_KEY", " # reasoning_effort: none"} { | ||
| 169 | + if !strings.Contains(text, want) { | ||
| 170 | + t.Errorf("the file has no %q:\n%s", want, text) | ||
| 171 | + } | ||
| 172 | + } | ||
| 173 | + c.BaseURL, c.APIKeyEnv = "", "" | ||
| 174 | + text = Render(Decide(p, c, Facts{})) | ||
| 175 | + if strings.Contains(text, "\napiKeyEnv:") || strings.Contains(text, "\nbaseUrl:") { | ||
| 176 | + t.Errorf("defaults written out:\n%s", text) | ||
| 177 | + } | ||
| 178 | +} | ||
modified
internal/agentinit/render.go +30 -4 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/agentinit/wizard.go +4 -6 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/tui/initwin.go +72 -19 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/tui/initwin_test.go +58 -1 Large diff collapsed to keep the page fast — load it when you need it.