#Update to 2026.4.21 broke openclaw

1 messages · Page 1 of 1 (latest)

worn acorn
#

Running OpenClaw 2026.4.21 (updated from 2026.4.14 today via npm). After the update Lex lost her identity — system_prompt.md Sections 1-4 are missing (may have been a pre-existing issue exposed by the update).
Ollama weekly quota is exhausted (100% used, resets Sunday) so Kimi k2.5 and k2.6 are both down. We need OpenRouter as the only working fallback but can't get it working.
Wanted DeepSeek (deepseek/deepseek-chat-v3-0324:free) as primary with other free OpenRouter models as fallbacks, but getting 404 errors. Llama 4 Maverick doesn't support tool use. Llama 4 Scout returns a billing error despite being free tier.
Also: OpenClaw keeps reverting to Kimi/Ollama as primary regardless of what's set in openclaw.json — appears to be hardcoded local provider preference overriding config.
Questions:

Which free OpenRouter models support tool use in 2026.4.21?
Why is OpenClaw ignoring openclaw.json primary model and defaulting to Ollama?
Why is Llama 4 Scout returning a billing error on a free account?
How do we recover with no working model until Sunday?

wild pendantBOT
humble flickerBOT
#

To help others find answers, you can mark your question as solved via Right click solution message -> Apps -> âś… Mark Solution

wild pendantBOT
blazing cragBOT
#

I dug through the docs/source. Short version:

  • Your 404 is very likely the model ref.
    For OpenRouter, OpenClaw expects:
    openrouter/<provider>/<model>
    So this is probably wrong:
    deepseek/deepseek-chat-v3-0324:free
    and this is the correct form:
    openrouter/deepseek/deepseek-chat-v3-0324:free

    The docs are explicit here: if the model id itself contains / (OpenRouter-style), you must include the provider prefix.

  • I do not see a hardcoded Ollama preference.
    The selection path is:

    1. agent-specific model override (agents.list[].model) if present
    2. agents.defaults.model.primary
    3. agents.defaults.model.fallbacks
#

Also, persisted session overrides can beat config, and fallback runs can temporarily persist an auto override. So if Lex keeps landing on Kimi/Ollama, the likely causes are:

  • Lex has its own agents.list[].model
  • this thread/session has a stored /model override
  • OpenRouter is failing, so fallback is dropping onto the old Kimi/Ollama chain

Also worth noting: the built-in default is openai/gpt-5.4, not Ollama.

  • Llama 4 Scout “billing error” looks OpenRouter-side, not OpenClaw-side.
    OpenRouter’s docs say 402 means insufficient credits, and even :free routes still have free-tier limits. Their docs also note:

    • if you’ve bought less than 10 credits, free models are capped per day
    • a negative balance can still cause 402 even on free models
  • For “which free OpenRouter models support tools”, don’t trust a pasted static list.
    2026.4.21 has openclaw models scan specifically for this:

    • openclaw models scan --no-probe = metadata candidates
    • openclaw models scan = live tool/image probing with your key
#

That’s the right source of truth because the OpenRouter free catalog changes constantly.

Best recovery path until Sunday

  1. Fix the primary ref to:
    openrouter/deepseek/deepseek-chat-v3-0324:free

  2. Temporarily remove dead Ollama/Kimi fallbacks from agents.defaults.model.fallbacks so failures don’t keep landing there.

  3. If Lex is a separate agent, check agents.list[].model too, not just agents.defaults.model.primary.

  4. Reset the active session after config changes (/new or /reset) so an old session override stops winning.

  5. Verify with:

    • openclaw models status --plain
    • openclaw status --all
worn acorn
#

openclaw models status --plain correctly shows openrouter/deepseek/deepseek-chat-v3-0324:free as active model. Config is correct.
But every request still fails with: 404 No endpoints found for deepseek/deepseek-chat-v3-0324:free and logs show requested=ollama/kimi-k2.5:cloud — meaning something is still overriding the config to Kimi despite it being exhausted.
Also found: openclaw status --all shows "1 bootstrapping agent" and BOOTSTRAP.md was present in workspace — removed it. Cleared session model overrides from sessions.json. Restarted gateway. Still same result.
Direct OpenRouter API test confirms key is valid and 346 models available. DeepSeek is in that list.
Specific questions:

Why does the log still show requested=ollama/kimi-k2.5:cloud when models status shows DeepSeek as primary?
Where else could a model override be stored beyond agents.defaults.model, agents.list[].model, and sessions.json?
Is there a way to force-reset all model state cleanly?

blazing cragBOT
#

I think I found the missing piece. This does not look like a hardcoded Ollama preference.

Why logs say requested=ollama/kimi-k2.5:cloud while models status --plain shows DeepSeek

openclaw models status --plain shows the configured default primary.

The runtime can still start from an override, and the code path does exactly that:

  • session override, providerOverride / modelOverride
  • channel override via channels.modelByChannel
  • plugin hook override via before_model_resolve or legacy before_agent_start

Then failover kicks in. Per the failover docs, when a run starts from an override, OpenClaw tries:

  1. the override model first
  2. fallbacks
  3. the configured primary at the end

So this sequence is completely consistent with what you’re seeing:

#
  • runtime starts on ollama/kimi-k2.5:cloud
  • that fails
  • it eventually reaches configured DeepSeek
  • DeepSeek throws the 404
  • logs still show requested=ollama/kimi-k2.5:cloud

That “requested=” line comes from logModelFallbackDecision(...).

Most likely culprit

channels.modelByChannel is now my top suspect.

Docs: channels.modelByChannel can pin a specific Discord channel/thread to a model, and it applies when the session does not already have its own override.

That would explain exactly why:

  • global model status says DeepSeek
  • this specific Discord thread keeps trying Kimi
#

It can match more than just the raw thread ID too, including parent/thread conversation keys and wildcard entries.

Where else model state can live

Beyond agents.defaults.model, agents.list[].model, and one sessions.json, check these:

  1. Per-agent session stores

    • default shape is:
    • ~/.openclaw/agents/<agentId>/sessions/sessions.json
    • if you only cleared one store, the active agent may be using another
  2. Channel overrides

    • channels.modelByChannel
    • especially under discord
    • search for this thread id: 1496471075666526258
    • also check * wildcard entries
#
  1. Session override fields

    • providerOverride
    • modelOverride
    • modelOverrideSource
    • authProfileOverride
    • authProfileOverrideSource
    • liveModelSwitchPending
  2. Plugin hooks

    • before_model_resolve
    • legacy before_agent_start
  3. Auth failover state (not a model override, but still affects routing)

    • ~/.openclaw/agents/<agentId>/agent/auth-state.json
    • if Scout threw a billing-shaped error, OpenClaw can temporarily disable the OpenRouter profile there

Cleanest reset path I found

#

I do not see a single “wipe all model state” command in docs/source.

I’d do this:

openclaw sessions --all-agents --json

That tells you the actual session store paths in use.

Then check:

  • channels.modelByChannel
  • every relevant ~/.openclaw/agents/*/sessions/sessions.json
  • ~/.openclaw/agents/*/agent/auth-state.json

And clear any stale:

  • providerOverride
#
  • modelOverride
  • modelOverrideSource
  • authProfileOverride
  • authProfileOverrideSource
  • liveModelSwitchPending

Then restart gateway, and do /new in the affected Discord thread.

Also, sessions cleanup is not the reset you want here, it only prunes stale/orphaned sessions.

Free OpenRouter models with tool use

For 2026.4.21, the reliable answer is don’t trust a static list, use the built-in live scan:

openclaw models scan --probe --json | jq -r '.[] | select(.tool.ok) | .modelRef'
#

That scans OpenRouter’s current free catalog and actually probes tool support.

Llama 4 Scout billing error

I can’t prove the exact Scout cause from local docs alone, but that one looks much more like an OpenRouter account/routing/billing-side issue than an OpenClaw bug.

Important wrinkle: if OpenClaw sees a billing-shaped response, it can mark the OpenRouter auth profile disabled in auth-state.json, which can make recovery look weirder afterward.

Recovery until Sunday

My recommendation:

  1. remove any Discord modelByChannel pin to Kimi
  2. reset the active session store for the actual agent
  3. check auth-state.json for disabled OpenRouter state
  4. use openclaw models scan --probe to pick current free tool-capable OpenRouter models
  5. keep DeepSeek only if the probe passes
worn acorn
#

Update: found and cleared providerOverride: ollama and modelOverride: kimi-k2.5:cloud from sessions.json on agent:main:telegram:direct:8112308424 and agent:main:new. They keep coming back after every gateway restart.
openclaw models status --plain correctly shows openrouter/deepseek/deepseek-chat-v3-0324:free but logs still show requested=ollama/kimi-k2.5:cloud on every request.
Checked: agents.defaults.model, agents.list, sessions.json, auth-state.json, BOOTSTRAP.md removed. No channels.modelByChannel in config.
Where is OpenClaw re-writing these overrides from? Is there another file or runtime mechanism that re-adds providerOverride/modelOverride after restart?

#

Also seeing cron jobs fail with openrouter/openrouter/free: rate limit exceeded — so OpenRouter IS connecting. Main session still forcing Kimi via providerOverride despite clearing sessions.json. Rate limit suggests OpenRouter free tier is capping requests per minute — is there a way to add retry/backoff?

blazing cragBOT
#

That new detail helps a lot.

openrouter/... rate limit exceeded means OpenRouter auth is working. So this is now two separate problems:

  1. OpenRouter free tier is actually being hit and rate-limited
  2. Something is still making the main session start on Kimi

On the “where is it being re-written from?” question

From the code I checked, I do not see a second hidden “model override DB”.

The persistent write path for these fields is still the session store. The auto-write happens in the fallback runner, via applyFallbackCandidateSelectionToEntry(...), and it writes back to the active sessions.json.

So if providerOverride=modelOverride=ollama/kimi-k2.5:cloud keeps reappearing, the most likely explanations are now:

1. The gateway is using a different session store path than the file you edited

This is still very possible.

#

openclaw models status --plain is a config view, not proof that the service is using the same store/config path as your CLI edit.

I’d check:

openclaw gateway status --deep --json
openclaw sessions --all-agents --json

What I want to verify there is:

  • the service config path
  • the resolved session store path(s)
  • whether the bad session key is actually the one being used

2. A background run is re-populating it after startup

If a cron / heartbeat / startup-triggered run starts with Kimi, OpenClaw can write that model choice back into the session store again.

#

So “I removed it, restarted, and it came back” does not necessarily mean startup itself wrote it. It can mean the first run after restart wrote it.

3. A plugin hook is still overriding model selection

Since you already ruled out:

  • agents.defaults.model
  • agents.list[].model
  • sessions.json
  • channels.modelByChannel
  • auth-state.json

…the remaining runtime source I’d take seriously is a plugin hook:

  • before_model_resolve
  • legacy before_agent_start

Those can override provider/model before the run, and they will not show up in models status --plain.

#
  • agent:main:telegram:direct:8112308424
  • agent:main:new

That may still not be the actual currently failing session key.

So before chasing ghosts, I’d verify the exact live key from:

openclaw sessions --all-agents --json

On retry/backoff for OpenRouter free tier

Yes, OpenClaw already has built-in cooldown/backoff behavior for rate limits.

Docs/source show:

  • rate-limit cooldowns: 1 min, 5 min, 25 min, 1 hr
#
  • config knobs under auth.cooldowns

Relevant config keys:

  • auth.cooldowns.rateLimitedProfileRotations
  • auth.cooldowns.overloadedProfileRotations
  • auth.cooldowns.overloadedBackoffMs

Docs: https://docs.openclaw.ai/concepts/model-failover and https://docs.openclaw.ai/gateway/configuration-reference

But for OpenRouter free, the more practical fix is usually:

  • reduce concurrency
  • stagger cron jobs
  • avoid multiple jobs hitting free models at once

There is not, from what I found, a simple “wait 60s and retry this same message forever” knob.

My best current read

#

I think the Kimi rewrite is not a hardcoded Ollama preference.

It’s most likely one of these:

  1. wrong session store path / wrong service config path
  2. wrong active session key
  3. plugin hook re-selecting Kimi
  4. first post-restart run re-persisting the bad override

If you paste:

  • openclaw gateway status --deep --json
  • openclaw sessions --all-agents --json
  • openclaw plugins inspect --all

I can help narrow it to one of those pretty quickly.

worn acorn
#

Further info:
openclaw gateway status --deep --json confirms CLI and daemon both using same config: /root/.openclaw/openclaw.json
openclaw plugins inspect --all shows two plugins with hooks:

ACPX Runtime — 1 typed hook
Memory (Core) — 1 typed hook + 1 custom hook

All other loaded plugins show no hooks. No before_model_resolve or before_agent_start visible.
Sessions output shows active session agent:main:telegram:direct:8112308424 still has providerOverride: ollama and modelOverride: kimi-k2.5:cloud even after clearing and restarting.
Theory: could ACPX Runtime or Memory (Core) hooks be re-writing the model override? If not, what do the hook types on these two plugins do?

blazing cragBOT
#

I checked the source, and no, ACPX Runtime and Memory (Core) are not the thing re-writing your model override.

What those hooks actually do:

  • ACPX Runtime

    • Registers a service plus one typed hook: reply_dispatch
    • That hook is for dispatching outbound replies to ACP sessions/runtime, not for model selection or session override writes.
  • Memory (Core)

    • Typed hook: before_agent_reply
      • In this plugin it is only used for heartbeat-triggered memory dreaming/promotion
      • It explicitly checks for ctx.trigger === "heartbeat" plus a special system-event token before doing anything
    • Custom hook: gateway:startup
      • Used to reconcile the managed dreaming cron on startup
    • Its registerMemoryCapability(...) call is memory prompt/flush/runtime registration, not a model override hook.

So your providerOverride: ollama + modelOverride: kimi-k2.5:cloud persistence is much more likely coming from core session state, not those plugins.

#

Most likely sources are still:

  1. Persisted session entry reuse
  2. Fallback runner auto-selecting a candidate and saving it
  3. A prior model/live-switch directive that got persisted

That also matches what you already observed: no plugin before_model_resolve or before_agent_start hooks are present.

So the short answer is:

  • Could ACPX or Memory Core be doing it?
    No, not from the code in 2026.4.21.
  • What are the hook types?
    ACPX = reply_dispatch
    Memory Core = before_agent_reply + custom gateway:startup

At this point I’d treat the override persistence as a core session/fallback bug, not a plugin-hook bug.

worn acorn
#

openclaw models scan returns: Error: No tool-capable OpenRouter free models found. Ollama quota exhausted until Sunday. No paid alternatives available. Is there a way to run OpenClaw in a reduced/no-tools mode for basic Telegram chat until quota resets?

blazing cragBOT
#

Yes, temporary no-tools mode is the cleanest way to keep basic Telegram chat alive until Sunday.

Short answer to your 4 questions

  1. Which free OpenRouter models support tool use right now?
    The live answer is openclaw models scan. If it currently says “No tool-capable OpenRouter free models found”, I’d treat that as none usable right now for tool-enabled OpenClaw on your account/profile. For live community recommendations, check #models too.
    Docs: https://docs.openclaw.ai/cli/models

  2. Why is OpenClaw “ignoring” openclaw.json and defaulting to Ollama/Kimi?
    From the code path + your session dump, this looks like a persisted session override (providerOverride / modelOverride), not a hardcoded Ollama preference.

  3. Why is Llama 4 Scout returning a billing error on a free account?
    That is most likely OpenRouter/provider-side gating or quota/billing policy, not OpenClaw inventing an error. “Free” in catalog metadata is not always “available to this account right now.”

  4. How do you recover until Sunday?
    Run tool-less mode and use a plain chat model.

#

Temporary survival config: disable all tools

{
  tools: {
    deny: ["*"]
  }
}

Then restart:

openclaw gateway restart
#

Important detail: allow: [] does not disable all tools. Use deny: ["*"].

With no tools available, OpenClaw stops sending tool schemas, so a non-tool-capable model can still do plain chat.


Then pick a plain OpenRouter free model

Since models scan is failing on the tool-capable check, use:

openclaw models scan --no-probe

That gives you the free catalog without requiring tool support.

Then set one manually:

#

Practical expectation

This should give you:

  • basic Telegram replies
  • no tool calls
  • no browsing/files/exec/memory tools

So it’s a decent “keep the lights on” mode, but it will not fix the separate Lex identity / system_prompt.md issue.

If you want, I can help you write the exact temporary openclaw.json block for your current config next.

worn acorn
#

Can you help me write up a GitHub issue for the persisted session override bug? Summary: after clearing providerOverride and modelOverride from sessions.json and restarting the gateway, the fallback runner re-writes them back to the previous failed model (ollama/kimi-k2.5:cloud) on the first run after restart. This happens even when the configured primary is a different provider entirely. Happy to provide full logs, config, and session dumps

#

?

blazing cragBOT
#

Sorry, concrete version:

Temporary “just keep Telegram chatting” mode

Add this to openclaw.json:

{
  tools: {
    deny: ["*"]
  }
}

Then:

#
openclaw gateway restart
openclaw models scan --no-probe
openclaw models set openrouter/<plain-chat-free-model>

Then start a fresh session (/new), because your current session still has the persisted ollama / kimi-k2.5:cloud override.

Important

  • allow: [] is not disable-all
  • use deny: ["*"]
  • models scan failing only means no tool-capable free model
  • with tools disabled, a plain chat OpenRouter free model can still work

If you paste your current agents.defaults.model block, I’ll write the exact safe merge for your config.

worn acorn
#

What's the correct openclaw.json config to add Google Gemini 2.5 Flash as a provider with an API key? I can see the Google plugin is loaded (@openclaw/google). What's the exact providers block format?

blazing cragBOT
#

If @openclaw/google is already loaded, the recommended config is just this, no custom api/baseUrl needed:

{
  agents: {
    defaults: {
      model: {
        primary: "google/gemini-2.5-flash"
      }
    }
  },
  models: {
    providers: {
      google: {
        apiKey: "${GEMINI_API_KEY}"
      }
#
    }
  }
}

A few important bits:

  • Provider id is google
  • Model ref is google/gemini-2.5-flash
  • Both GEMINI_API_KEY and GOOGLE_API_KEY are accepted
  • Since the Google plugin already ships the catalog, you usually should not add baseUrl, api, or models unless you are intentionally overriding the built-in provider behavior

If you really want the more explicit override shape, it would be:

#
{
  models: {
    providers: {
      google: {
        api: "google-generative-ai",
        baseUrl: "https://generativelanguage.googleapis.com",
        apiKey: "${GEMINI_API_KEY}"
      }
    }
  }
}

But again, for normal Gemini API key usage, the first minimal block is the right one.

Then verify with:

worn acorn
#

Gemini 2.5 Flash showing in openclaw models list but Auth = no. Google plugin loaded (@openclaw/google). Added to openclaw.json:

models: {
providers: {
google: {
apiKey: "AIza..."
}
}
}

Added google/gemini-2.5-flash: {} to agents.defaults.models allowlist. Set agents.defaults.model.primary to google/gemini-2.5-flash. Model shows as configured but Auth: no. What's missing?

blazing cragBOT
#

Nothing else should be required. For the bundled Google provider, this alone is enough:

{
  models: {
    providers: {
      google: {
        apiKey: "AIza..."
      }
    }
  }
}

So if openclaw models list still shows Auth: no, the real issue is usually OpenClaw is not seeing that key in the effective config/runtime, not that another config field is missing.

Most likely causes

#
  1. Wrong config file

    • confirm with:
    openclaw gateway status --deep --json
    
  2. Gateway still running old config

    • restart it:
    openclaw gateway restart
    
  3. models list is only showing “missing auth”, not “bad key”

    • run:
    openclaw models status --probe
    
#

That will tell you whether auth is truly missing vs the key being invalid/restricted.

Safer version I’d use

Instead of inline plaintext, use an env-backed ref:

{
  models: {
    providers: {
      google: {
        apiKey: {
          source: "env",
          provider: "default",
          id: "GEMINI_API_KEY"
        }
#

If you paste the output of:

openclaw models status --probe

I can tell you pretty quickly whether this is:

  • config not loaded,
  • daemon env missing,
  • or Google rejecting the key.
worn acorn
#

openclaw models status --probe shows google/gemini-2.5-flash in configured models list but no google entry appears in auth probes at all. API key added to models.providers.google.apiKey in openclaw.json but Auth shows "no" in models list. Key was set via python script writing directly to openclaw.json. Is the key format correct or does it need to be set via env variable GEMINI_API_KEY instead?

blazing cragBOT
#

The key format is fine. A Google Gemini API key normally starts with AIza..., and it does not have to be an env var.

So this:

models: {
  providers: {
    google: {
      apiKey: "AIza..."
    }
  }
}

is valid in principle.

If openclaw models status --probe shows no Google auth entry at all, that means OpenClaw is not resolving a usable key from the effective config. It is not a “Gemini needs a different key format” problem.

#

My recommendation

Use an env-backed ref instead, because it makes the auth path less ambiguous:

{
  models: {
    providers: {
      google: {
        apiKey: {
          source: "env",
          provider: "default",
          id: "GEMINI_API_KEY"
        }
      }
    }
#
  }
}

Then make sure the gateway service/daemon actually has GEMINI_API_KEY in its environment, then:

openclaw gateway restart
openclaw models status --probe

Important gotcha

If you set GEMINI_API_KEY only in your current shell, but the gateway is running as a service, the daemon won’t see it.

So:

#
  • literal apiKey: "AIza..." should work
  • env var is not required
  • but env var + restart is the safest way to prove the auth path

If Google still does not appear in auth probes after that, I’d be pretty confident you’re hitting one of these:

  1. wrong config file
  2. daemon not using updated config
  3. script wrote the structure somewhere other than the effective models.providers.google.apiKey path

Docs:

If you paste the redacted models.providers.google block exactly as it exists in openclaw.json, I can sanity-check the shape fast.

worn acorn
#

The google provider block is empty — {} — even though the python script returned Done. The API key didn't save. Script used was:

c.setdefault('models',{}).setdefault('providers',{})['google']={'apiKey':key}

But models.providers.google is coming back as {}. The models block in openclaw.json uses "mode": "replace" — could that be wiping the google provider on gateway restart?

blazing cragBOT
#

No, models.mode: "replace" should not be wiping models.providers.google.apiKey out of openclaw.json.

replace only controls how config rewrites the generated model catalog (models.json) versus merge behavior. It is not supposed to blank provider auth fields in your main config.

So the important bit is:

  • your Python assignment line is basically fine
  • the empty {} is not explained by models.mode: "replace"
  • this points much more to a write/save path problem than a Google-provider problem

What this means

If models.providers.google ends up as {}:

  • either the script never actually wrote the key value
  • or something else later rewrote the config and dropped it
  • but it is not normal Google provider normalization, and it is not mode: "replace" doing that
#

Fastest unblock

I’d stop fighting inline writes and switch to an env-backed ref:

{
  models: {
    providers: {
      google: {
        apiKey: {
          source: "env",
          provider: "default",
          id: "GEMINI_API_KEY"
        }
      }
    }
#
  }
}

Then put the real key in the gateway service environment, restart, and probe again:

openclaw gateway restart
openclaw models status --probe

Useful sanity check

Check the file immediately after the Python script writes it, before restarting anything.

  • If it already shows {}, the bug is in the script/write path
worn acorn
#

Python script writes google provider block correctly and shows it immediately after write. But after systemctl restart openclaw-gateway.service, the block becomes {} again and google disappears from auth probes. Default reverts to ollama/kimi-k2.5:cloud. Something in the gateway startup is overwriting openclaw.json. Is there a known issue with gateway startup overwriting models.providers in openclaw.json?

blazing cragBOT
#

I do not see a known/intentional startup behavior that should wipe models.providers.google.apiKey out of openclaw.json.

From the code/docs:

  • models.mode: "replace" affects models.json catalog generation, not your main config file
  • Google provider normalization does not strip apiKey
  • Gateway startup should not normally rewrite models.providers.* just because the service restarted

So if a plain:

systemctl restart openclaw-gateway.service

turns:

#
models.providers.google = { apiKey: "..." }

into:

models.providers.google = {}

that looks like either:

  1. a service wrapper / ExecStartPre / ExecStartPost running something that rewrites config
  2. some other background process writing a normalized/redacted config back
  3. a real bug

Two practical next steps

#

1. Check the unit file

This is the first thing I’d inspect:

systemctl cat openclaw-gateway.service

If there is any ExecStartPre, ExecStartPost, openclaw doctor --fix, onboarding/configure call, or custom wrapper script, that is very suspicious.

2. Use env auth as the workaround

Even if config is being rewritten, service env is much harder for this bug to clobber:

{
  models: {
    providers: {
#
      google: {
        apiKey: {
          source: "env",
          provider: "default",
          id: "GEMINI_API_KEY"
        }
      }
    }
  }
}

Then put GEMINI_API_KEY in the service environment, restart, and probe again.

My read

#
  • No, mode: "replace" is not the cause
  • No, this does not look like normal provider behavior
  • if restart alone mutates openclaw.json, that is bug-worthy

If systemctl cat openclaw-gateway.service looks clean, I’d file a GitHub issue with that repro:

  • write models.providers.google.apiKey
  • confirm file contains it
  • restart service
  • file changes to {}
  • auth probe loses Google

Issue tracker: https://github.com/openclaw/openclaw/issues

If you paste the unit file contents, I can help spot whether the rewrite is coming from systemd or OpenClaw itself.

worn acorn
#

Unit file is clean — no ExecStartPre/Post. But GEMINI_API_KEY is not in the service environment even though we created a drop-in at /root/.config/systemd/user/openclaw-gateway.service.d/gemini.conf. Using env-backed ref in openclaw.json: apiKey: {source: "env", provider: "default", id: "GEMINI_API_KEY"}. Google still not appearing in auth probes. Should the env var be added directly to the unit file instead of a drop-in? And is the openclaw.json being rewritten on restart a known bug worth filing?

#

?

blazing cragBOT
#

No, don’t edit the main unit directly. A drop-in is fine, but it has to be attached to the same service manager the gateway is actually running under.

What’s probably wrong

OpenClaw on Linux defaults to a systemd user service, not a system service:

So:

  • if the gateway is running via systemctl --user ..., then the drop-in belongs under
    ~/.config/systemd/user/openclaw-gateway.service.d/*.conf
  • if it is running as a system service, then the drop-in belongs under
    /etc/systemd/system/openclaw-gateway.service.d/*.conf

/root/.config/systemd/user/... only applies if the gateway is literally running under root’s user manager. That is uncommon, and if you’ve been restarting with plain systemctl restart ... instead of systemctl --user restart ..., you may be talking to the wrong service entirely.

#

Best fix

Honestly, the cleanest path here is:

Put the key in:

~/.openclaw/.env

with:

GEMINI_API_KEY=AIza...

That is explicitly recommended for gateway installs when service env inheritance is flaky.

#

Then restart the correct service manager:

systemctl --user daemon-reload
systemctl --user restart openclaw-gateway.service

If you truly installed it as a system service, use:

sudo systemctl daemon-reload
sudo systemctl restart openclaw-gateway.service

So, direct answers

Should the env var be added directly to the unit file instead of a drop-in?

#

No. A drop-in is fine. The bigger issue is that it’s likely attached to the wrong unit scope/location.

Is openclaw.json being rewritten on restart a known bug worth filing?

Yes, if you can reproduce it cleanly.
That does not look like expected behavior, and it’s worth filing at https://github.com/openclaw/openclaw/issues.

Useful repro details to include:

  • exact OpenClaw version: 2026.4.21
  • exact restart command used
  • whether service is user or system
  • before/after openclaw.json snippet
  • that models.mode: "replace" is set or not set
  • that the unit has no ExecStartPre/Post

If you want, paste the output of these two read-only commands and I can tell you exactly which service manager is in play:

#
openclaw gateway status --deep --json
systemctl --user show openclaw-gateway.service -p FragmentPath -p DropInPaths -p Environment
#

Sorry, the short version is:

1. The 404 is very likely the model ref format

For OpenRouter models whose model ID already contains /, you must include the provider prefix:

openclaw models set openrouter/deepseek/deepseek-chat-v3-0324:free

Not:

deepseek/deepseek-chat-v3-0324:free

Docs explicitly call this out for OpenRouter-style IDs: https://docs.openclaw.ai/cli/models

#

That alone can explain both:

  • the 404s
  • OpenClaw seeming to “ignore” your configured primary

2. OpenClaw is probably not hardcoding Ollama

Model selection order is:

  1. current session model override
  2. configured fallbacks
  3. primary/default logic

Docs: https://docs.openclaw.ai/concepts/model-failover
and https://docs.openclaw.ai/cli/models

So if:

  • the OpenRouter primary ref is malformed, or
  • the session already got pinned/fell back to Ollama/Kimi earlier,
#

it can keep using Ollama until you do:

/new

or /reset, then set the corrected model again.

3. “Which free OpenRouter models support tools?”

I don’t want to give you a stale manual list here. The reliable answer is to probe live:

openclaw models scan --provider openrouter

With a key present, that can probe tool support live. Docs: https://docs.openclaw.ai/cli/models

#

4. Llama 4 Scout “billing error”

That is most likely OpenRouter-side account/quota/routing, not OpenClaw inventing it.

OpenClaw treats billing-shaped upstream errors as billing/failover signals, which is expected behavior. “Free” on OpenRouter does not always mean “currently usable on every account without limit”.

5. Best recovery path before Sunday

I’d do this:

  1. Put GEMINI_API_KEY=... in:

    ~/.openclaw/.env
    

    Docs: https://docs.openclaw.ai/help/faq#env-vars-and-env-loading

  2. Restart the user service, not the wrong scope:

#
   systemctl --user restart openclaw-gateway.service
  1. Start a fresh session:

    /new
    
  2. Set the OpenRouter model with the full ref:

    openclaw models set openrouter/deepseek/deepseek-chat-v3-0324:free
    
  3. Verify:

#
   openclaw models status --probe

6. Is the openclaw.json rewrite worth filing?

Yes. If restart alone is mutating openclaw.json, that sounds bug-worthy. File it here: https://github.com/openclaw/openclaw/issues

If you paste your agents.defaults.model block, I can sanity-check the exact ref format before you try again.

worn acorn
#

Gemini auth probe shows ok. Set google/gemini-2.5-flash as primary in openclaw.json. Cleared session overrides. Restarted gateway. Sent /new on Telegram. Getting: "Agent couldn't generate a response."

Log shows: incomplete turn detected: stopReason=stop payloads=0 — surfacing error to user

No model fallback errors. Gemini is being reached but returning empty response. What does stopReason=stop with payloads=0 mean for the Google provider?

blazing cragBOT
#

Yep, that log means something pretty specific.

For the Google provider, stopReason=stop with payloads=0 means:

  • the Gemini request did succeed at the HTTP/stream level
  • Google ended the candidate with finishReason: "STOP"
  • but OpenClaw got no usable text blocks and no tool-call blocks out of candidate.content.parts

So this is not an auth failure, and not a normal model-fallback case. It is basically:

“Gemini returned a cleanly-finished response, but it was empty.”

Why that happens

Most likely one of these:

  1. Gemini returned an empty candidate
#

This does happen in the wild with Gemini, unfortunately.

  1. Google blocked/suppressed the response in a way that did not become an HTTP error
    OpenClaw’s Google transport does not appear to surface extra Google block metadata here, so it collapses into a generic incomplete turn.

  2. Provider-side streaming bug / odd edge case
    Especially if it happens on simple prompts too.

Important part

This line:

incomplete turn detected: stopReason=stop payloads=0

does not mean OpenClaw chose Ollama, ignored config, or failed auth.
It means Gemini answered with nothing.

#

Fastest sanity check

Try a dead-simple prompt in a fresh session:

reply with exactly: ok

If that also comes back empty, I’d suspect a Gemini empty-response bug or Google-side filtering issue.

If a tiny prompt works, but your normal agent prompt fails, then the likely trigger is:

  • the current system prompt / context shape
  • missing system_prompt.md sections
  • something in the injected context that Gemini is silently choking on

My recommendation

  1. Keep Google auth as-is, since probe is ok
#
  1. Test with a tiny plain prompt
  2. If tiny prompts fail too, switch off Gemini for now and file an issue
  3. If tiny prompts work, focus on the broken prompt/context state, especially Lex’s missing Sections 1–4

Is this worth filing?

Yes, probably.
Especially if you can reproduce:

  • provider: google
  • model: google/gemini-2.5-flash
  • auth probe: ok
  • request completes
  • stopReason=stop
  • no text, no tool calls, payloads=0

That’s a solid bug report for OpenClaw because it should ideally surface a better Google-specific reason instead of a generic “couldn’t generate a response.”

#

If you want, paste the exact prompt that triggers it, or just whether “reply with exactly ok” succeeds. That’ll tell us very quickly whether this is Gemini itself or your prompt/context.