diff --git a/docs/concepts/models.md b/docs/concepts/models.md index 4b14320eeee..62d5bf07a70 100644 --- a/docs/concepts/models.md +++ b/docs/concepts/models.md @@ -119,7 +119,8 @@ openclaw config set agents.defaults.models '{"openai/gpt-5.4":{}}' --strict-json If `agents.defaults.models` is set, it becomes the **allowlist** for `/model` and for session overrides. When a user selects a model that isn't in that allowlist, OpenClaw returns: ``` -Model "provider/model" is not allowed. Use /model to list available models. +Model "provider/model" is not allowed. Use /models to list providers, or /models to list models. +Add it with: openclaw config set agents.defaults.models '{"provider/model":{}}' --strict-json --merge ``` @@ -131,6 +132,8 @@ This happens **before** a normal reply is generated, so the message can feel lik +When the rejected command included a runtime override such as `/model openai/gpt-5.5 --runtime codex`, fix the allowlist first, then retry the same `/model ... --runtime ...` command. For native Codex execution, the selected model is still `openai/gpt-5.5`; the `codex` runtime selects the harness and uses Codex auth separately. + For local/GGUF models, store the full provider-prefixed ref in the allowlist, for example `ollama/gemma4:26b`, `lmstudio/Gemma4-26b-a4-it-gguf`, or the exact provider/model shown by `openclaw models list --provider `. diff --git a/docs/help/faq-models.md b/docs/help/faq-models.md index c2de0dc49ff..e308029a98f 100644 --- a/docs/help/faq-models.md +++ b/docs/help/faq-models.md @@ -191,11 +191,14 @@ troubleshooting, see the main [FAQ](/help/faq). session overrides. Choosing a model that isn't in that list returns: ``` - Model "provider/model" is not allowed. Use /model to list available models. + Model "provider/model" is not allowed. Use /models to list providers, or /models to list models. + Add it with: openclaw config set agents.defaults.models '{"provider/model":{}}' --strict-json --merge ``` That error is returned **instead of** a normal reply. Fix: add the model to `agents.defaults.models`, remove the allowlist, or pick a model from `/model list`. + If the command also included `--runtime codex`, add the model first and then retry + the same `/model provider/model --runtime codex` command. diff --git a/src/auto-reply/reply/directive-handling.model-selection.ts b/src/auto-reply/reply/directive-handling.model-selection.ts index 33cfbe9d7f6..72cee70f949 100644 --- a/src/auto-reply/reply/directive-handling.model-selection.ts +++ b/src/auto-reply/reply/directive-handling.model-selection.ts @@ -80,6 +80,7 @@ export function resolveModelSelectionFromDirective(params: { defaultModel: params.defaultModel, aliasIndex: params.aliasIndex, allowedModelKeys: params.allowedModelKeys, + rawRuntime: params.directives.rawModelRuntime, }) : null; const useStoredNumericProfile = @@ -131,6 +132,7 @@ export function resolveModelSelectionFromDirective(params: { defaultModel: params.defaultModel, aliasIndex: params.aliasIndex, allowedModelKeys: params.allowedModelKeys, + rawRuntime: params.directives.rawModelRuntime, }); if (resolved.error) { diff --git a/src/auto-reply/reply/directive-handling.model.test.ts b/src/auto-reply/reply/directive-handling.model.test.ts index ff41ea6e17f..758430c6eb0 100644 --- a/src/auto-reply/reply/directive-handling.model.test.ts +++ b/src/auto-reply/reply/directive-handling.model.test.ts @@ -611,6 +611,22 @@ describe("/model chat UX", () => { expect(resolved.errorText).toContain("Browse: /models or /models "); }); + it("includes additive allowlist repair when a runtime switch targets a blocked model", () => { + const resolved = resolveModelSelectionForCommand({ + command: "/model openai/gpt-5.5 --runtime codex", + allowedModelKeys: new Set(["anthropic/claude-opus-4-6"]), + allowedModelCatalog: [], + }); + + expect(resolved.modelSelection).toBeUndefined(); + expect(resolved.errorText).toContain('Model "openai/gpt-5.5" is not allowed.'); + expect(resolved.errorText).toContain( + `openclaw config set agents.defaults.models '{"openai/gpt-5.5":{}}' --strict-json --merge`, + ); + expect(resolved.errorText).toContain("Then retry: /model openai/gpt-5.5 --runtime codex"); + expect(resolved.errorText).toContain("openclaw plugins enable codex"); + }); + it("treats explicit default /model selection as resettable default", () => { const resolved = resolveModelSelectionForCommand({ command: "/model anthropic/claude-opus-4-6", diff --git a/src/auto-reply/reply/directive-handling.model.ts b/src/auto-reply/reply/directive-handling.model.ts index 04f30858f09..3203c985bf1 100644 --- a/src/auto-reply/reply/directive-handling.model.ts +++ b/src/auto-reply/reply/directive-handling.model.ts @@ -265,6 +265,7 @@ export async function maybeHandleModelDirectiveInfo(params: { "", "Tap below to browse models, or use:", "/model to switch", + "/model --runtime to switch harnesses", "/model status for details", ] .filter(Boolean) diff --git a/src/auto-reply/reply/model-selection-directive.ts b/src/auto-reply/reply/model-selection-directive.ts index bce6fa5d8d1..7200c1577b6 100644 --- a/src/auto-reply/reply/model-selection-directive.ts +++ b/src/auto-reply/reply/model-selection-directive.ts @@ -20,6 +20,29 @@ export type ModelDirectiveSelection = { alias?: string; }; +function formatAddModelCommand(modelRef: string): string { + return `openclaw config set agents.defaults.models '${JSON.stringify({ [modelRef]: {} })}' --strict-json --merge`; +} + +function formatNotAllowedError(params: { + modelRef: string; + rawRuntime?: string | undefined; +}): string { + const rawRuntime = params.rawRuntime?.trim(); + const retryCommand = rawRuntime + ? `/model ${params.modelRef} --runtime ${rawRuntime}` + : `/model ${params.modelRef}`; + const lines = [ + `Model "${params.modelRef}" is not allowed. Use /models to list providers, or /models to list models.`, + `Add it with: ${formatAddModelCommand(params.modelRef)}`, + `Then retry: ${retryCommand}`, + ]; + if (rawRuntime && normalizeProviderId(rawRuntime) === "codex") { + lines.push("If the Codex runtime is missing, run: openclaw plugins enable codex"); + } + return lines.join("\n"); +} + const FUZZY_VARIANT_TOKENS = [ "lightning", "preview", @@ -238,6 +261,7 @@ export function resolveModelDirectiveSelection(params: { defaultModel: string; aliasIndex: ModelAliasIndex; allowedModelKeys: Set; + rawRuntime?: string | undefined; }): { selection?: ModelDirectiveSelection; error?: string } { const { raw, defaultProvider, defaultModel, aliasIndex, allowedModelKeys } = params; @@ -401,6 +425,9 @@ export function resolveModelDirectiveSelection(params: { } return { - error: `Model "${resolved.ref.provider}/${resolved.ref.model}" is not allowed. Use /models to list providers, or /models to list models.`, + error: formatNotAllowedError({ + modelRef: `${resolved.ref.provider}/${resolved.ref.model}`, + rawRuntime: params.rawRuntime, + }), }; }