omp Model Roles Are Preferences, Not Boundaries: Locking Your Coding Agent to One Provider

Pointing every omp role at one provider does not keep your code there. What I tested on omp 18.13.3, how provider boundaries layer, and role maps for Anthropic-only, OpenAI-only, Ollama-local, Chinese-model, fast, exploration, and balanced setups.

18 min read Updated Nov 3, 2026
omp Model Roles Are Preferences, Not Boundaries: Locking Your Coding Agent to One Provider

You decide your coding agent should talk to one vendor only: for cost, for a compliance rule, or because the code must stay on your laptop. You open ~/.omp/agent/config.yml, point default, plan, slow, and smol at that vendor, and call it done.

Setting those roles is not enough. In omp (Oh My Pi), a role says which model you prefer for a kind of work, not which providers may see your code. Roles you did not set, a project file in the repo you opened, an API key exported in your shell, and the advisor role can all route a session somewhere else.

This article covers what the docs say, what I tested on November 3, 2026 (omp 18.13.3; as of that date the latest GitHub release was v18.16.0, published November 2, 2026), and role maps for seven common setups. For how roles, hashline edits, and subagents work, see the omp tour. This piece is about where the boundary actually is.

TL;DR

  • modelRoles is a preference, not an allowlist. Per the omp roles docs, a role you leave unassigned resolves automatically from any available, authenticated model. An unassigned advisor “seeks a separate strong reasoning model rather than inheriting default.”
  • disabledProviders is the boundary. In a scratch project it cut omp models from 6 providers to 1. With the merged default role still naming a disabled provider, the main response reported the remaining one.
  • Layers merge, they do not reset. Mappings deep-merge and arrays replace, so your global roles survive under a project file that sets only one. A --profile resets settings but not shell environment variables.
  • The advisor is a data-egress decision. It sends prompts, agent output, and tool results to its own provider. In a one-provider or offline setup, pin it inside the boundary and check for roster files that name another model.
  • Check every catalog kind. Plain omp models lists chat models only. omp models --kind all also showed 22 web search runners and 14 local runners here; both can be disabled.
  • Verify with three checks, shown below, instead of trusting the YAML. This is a model-routing boundary, not a network firewall.

The assumption that fails

The community examples I reviewed emphasize role maps and fallback chains rather than provider exclusion. DeepakNess’s setup (updated August 12, 2026) maps default, plan, vision, and tiny. The developersdigest overview (November 2, 2026) suggests starting with two roles. Both are fine for the goal they state: a cheap driver with a stronger planner and a fallback chain.

Neither is trying to restrict providers, and the docs say why that would not work. Here is the relevant text from the roles page:

With an empty modelRoles map, omp resolves roles from the models that are available and authenticated.

Setting four roles leaves the rest of the role list (commit, vision, tiny, designer, task, advisor, plus any custom role from an extension or agent) on automatic. Automatic selection draws on whatever is available: logged-in cloud providers, keyless local engines, and credentials from your shell. Some roles first reuse default. Pinning every role makes routing predictable. Excluding a provider is a separate job, and disabledProviders does it.

What I tested

I used a scratch directory with a project-level .omp/config.yml that sets one role and disables five providers. My global config (not the repo’s) names github-copilot/* for most roles and google-antigravity/* for a role called web.

# /tmp/omp-probe/.omp/config.yml
modelRoles:
  plan: anthropic/claude-opus-5-5:high
disabledProviders:
  - github-copilot
  - cursor
  - opencode-zen
  - opencode-go
  - google-antigravity
#CommandResult on omp 18.13.3
1omp models without the project file6 providers: anthropic (19 models), cursor (81), github-copilot (88), google-antigravity (11), opencode-go (53), opencode-zen (122)
2omp models with the project file1 provider: anthropic (19)
3omp config get modelRoles with the project fileplan is the project’s Anthropic value; default, slow, smol, advisor, code-review, pr-fixes, fast still name github-copilot/*, and web still names google-antigravity/*
4omp -p --mode json "reply with the single word ok", then the same prompt in text modeThe JSON run used provider: anthropic, model: claude-opus-5-5. The text-mode run printed prewalk disabled — provider "github-copilot" is disabled and answered ok
5omp --profile probe config get modelRoles{}
6omp --profile probe models, with OPENCODE_API_KEY set in my shellopencode-go (53 models) listed anyway
7the same, run as env -u OPENCODE_API_KEY omp --profile probe modelsNo models available. Set API keys in environment variables.
8omp models --kind all from the repo directory (no disabledProviders set)8 groups: anthropic (19), cursor (81), github-copilot (88), google-antigravity (13), local (14), opencode-go (53), opencode-zen (124), web (22)
9omp models --kind all in a second scratch project whose only setting is disabledProviders: [web, local]web and local no longer listed; the other six groups unchanged

What this shows, with its limits:

  1. disabledProviders removes providers from selection (rows 1 and 2), matching the providers doc: “disabledProviders is checked before credentials.”
  2. Roles you do not touch keep their old values (row 3). The settings docs say mappings deep-merge while scalars and arrays replace. Disabling github-copilot did not rewrite the roles that name it.
  3. With default still naming disabled Copilot, the main response reported anthropic/claude-opus-5-5 (row 4). This single observation neither identifies the resolution path nor audits auxiliary requests. The docs say omp “skips that route or shows an actionable model/authentication error” when no usable model satisfies a role. I did not read the resolution code, so I cannot give you the rule for every role.
  4. A profile resets settings, not the process environment (rows 5 to 7). --profile gives “isolated auth, sessions, settings, and caches” per the CLI reference, but an exported key still makes its provider available.
  5. Chat models are not the whole catalog (rows 8 and 9). Search (web), on-device (local), image, and judge runners sit under other kinds, and disabledProviders removes them too.

The four layers

LayerSettingWhat it controlsIs it a boundary?
PreferencemodelRolesWhich model a role picks firstNo. Unset roles auto-resolve.
BoundarydisabledProvidersWhich providers can be selected at allYes, but it is a denylist you maintain.
State--profile, project .omp/config.ymlWhich settings, auth, and sessions are loadedOnly for what they hold. Env vars still apply.
Recoveryretry.fallbackChainsWhere a failing role moves nextNo. Candidates still pass through disabledProviders. Remove out-of-policy entries so recovery works as you expect.

Two details on the last row. A default chain is “also used for configured roles that do not have their own chain.” And an explicit [] disables fallback for that role. Model-kind roles such as web, image, speech, dictation, and judge resolve their own named chains and never use default. tiny is an ordinary chat-style role, so set tiny: [] when you want no fallback for it.

There is no provider allowlist. enabledProviders controls foreign discovery sources (rules and skills read from other agents), not model providers, so disabledProviders plus a check that the catalog lists only what you expect is the closest thing you have. A new provider that appears later, because you export a new key, is allowed until you list it.

Arrays replace across layers, which makes one hazard easy to miss. If the global file says disabledProviders: [openai, github-copilot] and a project file says disabledProviders: [google], the effective list inside that project is [google]. The two globally excluded providers are available again.

Role maps for seven setups

Selectors marked “seen” appeared in omp models on November 3, 2026. Everything else needs omp models find <name> on your machine before you rely on it. Since v18.14.0 a near-miss provider-qualified selector fails instead of picking a sibling model, per the omp tour.

Single-provider setups

SetupProvider setdefaultplan / slowsmoladvisorStatus
Anthropic onlyoneanthropic/claude-sonnet-5-5:highanthropic/claude-opus-5-5:highanthropic/claude-haiku-5-5:mediumanthropic/claude-opus-5-5:mediumSelectors seen
OpenAI onlyoneopenai/gpt-6.1-sol:mediumsame model at :high / :xhighopenai/gpt-6-luna:lowopenai/gpt-6.1-sol:highNot verified: no OpenAI login here. A third-party gateway lists openai/gpt-6.1-sol.
Ollama localollama plus localollama/qwen3.8:27bsamesameollama/qwen3-coder:30bNot run: no Ollama on this machine

Pros

  • Smallest set to audit. One provider ID (or ollama plus local) is easy to compare against omp models --kind all.
  • The advisor stays inside the boundary. Every map pins advisor to the same provider, so the second reader does not add a vendor.
  • Ollama keeps requests on a machine you control, provided OLLAMA_BASE_URL and OLLAMA_HOST are unset or point at loopback.
  • Anthropic only is the one I could observe. Its selectors appeared in omp models, and it was the provider that remained in the denylist probe.

Cons

  • No cross-vendor recovery. A fallback chain entry that names another vendor is out of policy here. Keep chains inside the provider or set [].
  • An allowed provider ID does not name the server. models.yml endpoint overrides can send it elsewhere.
  • OpenAI and Ollama are drafts. I had no OpenAI login and no Ollama install, so neither map was run.
  • Ollama needs extra checks. Context length has to be set and confirmed with ollama ps, and the two models are 18 GB and 19 GB downloads. Those are download sizes, not memory needs.

Gateway setup: Chinese models through one key

SetupProvider setdefaultplan / slowsmoladvisorStatus
Chinese modelsone gatewayopencode-go/deepseek-v4.1-flash:highopencode-go/glm-5.3:high / opencode-go/kimi-k3.1-pro:highopencode-go/minimax-m3.1-flash:lowopencode-go/qwen3.8-max:highSelectors seen, all via the opencode-go gateway

Pros

  • One provider ID, several model labs. opencode-go listed 53 models here, and every selector in this map was seen.
  • One provider to allow. Roles, advisor, and fallback can all stay inside opencode-go, so the denylist check is the same as for a single-provider setup.

Cons

  • “Chinese models only” through opencode-go still sends code to the gateway. The model lab and the vendor in the path are different parties. Direct providers (deepseek, moonshot, zai, minimax, and others in the provider table) need their own keys, and DeepSeek’s integration guide, written for omp v14.5, says a custom models.yml compat block was needed then. I did not check whether 18.x still needs it.
  • Thinking levels differ per model. In my opencode-go catalog on November 3, 2026, kimi-k3.1-pro, deepseek-v4.1-flash, and glm-5.3 list low, high, max with no medium.
  • Vision needs a different model. glm-5.3 and deepseek-v4-pro list no image input; kimi-k3 lists images.

Mixed-provider setups: Fast, Exploration, Balanced

SetupProvider setdefaultplan / slowsmoladvisorStatus
Fast (candidate)mixedopencode-go/deepseek-v4.1-flash:lowanthropic/claude-sonnet-5-5:mediumopencode-go/minimax-m3.1-flash:minimalanthropic/claude-haiku-5-5:medium, with advisor.syncBacklog: "off"Selectors seen; unbenchmarked
Exploration (candidate)mixedanthropic/claude-sonnet-5-5:highsame modelopencode-go/qwen3.8-flash:low (1M context)github-copilot/gpt-6.1-sol:mediumSelectors seen; scouts via task.agentModelOverrides.scout: "@smol"
Balanced (candidate)mixedopencode-go/deepseek-v4.1-flash:highanthropic/claude-opus-5-5:highanthropic/claude-haiku-5-5:mediumopenai/gpt-6.1-sol:mediumSelectors seen, except openai/*; unbenchmarked

Pros

  • Each role gets its own model. A cheap or large-context driver can sit next to a stronger planner and advisor, which is the goal the community setups state.
  • Selectors were seen for Fast and Exploration, and for Balanced except the openai/* advisor.

Cons

  • Not a boundary setup. Fast mixes an aggregator and Anthropic; Exploration adds github-copilot; Balanced adds OpenAI. Every provider named here has to stay out of disabledProviders, and each one sees what its role sends.
  • Speed is unmeasured here. The only public number I found is a personal 10-task eval (three runs per configuration, private tasks) reported by haimaker on October 14, 2026, run on Pi 0.85.1 (not omp); see the original leaderboard and its methodology. It listed DeepSeek V4.1 Flash at max effort at 93.3% task success and 4 minutes 30 seconds per task, against Sonnet 5.5 at high effort at 100% and 46 seconds. Cheap per token did not mean quick per finished task there. Measure on your own repo before trusting “fast.”
  • Balanced depends on an unverified selector. openai/gpt-6.1-sol was not checked against a real OpenAI login.

One caveat for every map

Ignore YAML that uses api_base or a top-level roles:. One otherwise useful cost write-up (September 22, 2026) does. The documented keys are baseUrl in models.yml and modelRoles in config.yml.

Anthropic only: the boundary version

Putting a role map in config.yml is the easy half. The boundary half is the denylist and the check. The denylist below is illustrative, not exhaustive: add every non-target provider that omp models --kind all shows in your environment, including custom providers, and check the effective list from the repo you will work in. A project-level disabledProviders array replaces the global one. Also inspect models.yml for endpoint overrides, because an allowed provider ID does not by itself say which server receives the requests.

# ~/.omp/agent/config.yml
modelRoles:
  default: anthropic/claude-sonnet-5-5:high
  plan: anthropic/claude-opus-5-5:high
  slow: anthropic/claude-opus-5-5:high
  smol: anthropic/claude-haiku-5-5:medium
  commit: anthropic/claude-haiku-5-5:low
  vision: anthropic/claude-sonnet-5-5:medium
  advisor: anthropic/claude-opus-5-5:medium
  # Optional: pin every other role in `omp config get modelRoles`
  # (task, tiny, code-review, ...) for predictable routing. The denylist enforces the exclusion.

retry:
  fallbackChains:
    default:
      - anthropic/claude-sonnet-5:high

disabledProviders:
  - github-copilot
  - cursor
  - opencode-zen
  - opencode-go
  - google
  - google-antigravity
  - web   # also removes the 22 search runners; drop this line if you want omp web search
  - openai
  - openai-codex

Ollama local: use a profile and say what you did not test

For an offline or private setup, start from nothing: a separate profile, no project file, and the cloud providers disabled. The intended provider set here is ollama plus local (on-device tiny models), not a single provider ID.

# Prepare while online: pull both Ollama models and the on-device tiny model.
ollama pull qwen3.8:27b
ollama pull qwen3-coder:30b
omp --profile local tiny-models download lfm2.5-230m

# Optional shortcut. This appends a shell function to your shell startup file.
omp --profile local --alias omp-local

# The env -u form affects only this one invocation. Exported keys and .env files still apply.
env -u ANTHROPIC_API_KEY -u OPENAI_API_KEY -u OPENCODE_API_KEY omp --profile local models --kind all
# ~/.omp/profiles/local/agent/config.yml
modelRoles:
  default: ollama/qwen3.8:27b
  plan: ollama/qwen3.8:27b
  slow: ollama/qwen3.8:27b
  smol: ollama/qwen3.8:27b
  commit: ollama/qwen3.8:27b
  advisor: ollama/qwen3-coder:30b
  tiny: local/lfm2.5-230m

retry:
  fallbackChains:
    default: []
    tiny: []

disabledProviders:
  - anthropic
  - openai
  - openai-codex
  - google
  - google-antigravity
  - github-copilot
  - cursor
  - opencode-zen
  - opencode-go
  - web

What I confirmed: Ollama’s library lists qwen3.8:27b (18 GB download) and qwen3-coder:30b (19 GB) in listings checked November 3, 2026 (the Qwen3.8 page shows an October 30, 2026 update), and omp auto-discovers a keyless Ollama at http://127.0.0.1:11434 per the providers doc. Those sizes are download sizes, not memory needs. Check that OLLAMA_BASE_URL and OLLAMA_HOST are unset or point at loopback; otherwise “local” may be a remote server. I have not run this profile. The colon in qwen3.8:27b sits next to omp’s :thinking suffix, so run omp --profile local models ollama first and copy the selector it prints.

I left vision unset because I have not confirmed a local model with image input; the roles docs say an unusable role is skipped or reports an error.

Ollama’s context setting needs an explicit check. Its own pages disagree: the FAQ says “By default, Ollama uses a context window size of 4096 tokens.” The context-length guide sets 4k, 32k, or 256k by VRAM tier (under 24 GiB, 24 to 48 GiB, 48 GiB and up). The Modelfile page labels num_ctx “(Default: 2048)”. Do not guess. Start the server with OLLAMA_CONTEXT_LENGTH=32768 ollama serve and confirm with ollama ps.

The advisor is a data-egress decision

The advisor is a second model that reviews each turn. The advisor docs are direct about what it receives: “your prompts, the main agent’s output and reasoning, and relevant tool results; investigative tools can expose any files they read.” Session-managed secrets are obfuscated first, but the docs say that is not a substitute for choosing a provider allowed to see the repository.

On my install omp config list shows advisor.enabled = true, advisor.syncBacklog = 3, and advisor.immuneTurns = 3. The docs describe syncBacklog as defaulting to off, so trust your own omp config list. A value of 3 can pause the main agent for up to 30 seconds when review falls three turns behind, which is the wrong default for the fast setup.

Three consequences for single-provider setups:

  1. Set modelRoles.advisor inside the boundary, as every map above does.
  2. If you do not want a second reader, set advisor.enabled: false.
  3. A WATCHDOG.yml or WATCHDOG.yaml roster can give each advisor its own model, so one entry can quietly name another vendor. The docs say omp reads these from the active agent directory, the current directory, .omp/, and each parent up to the Git root. After a restart, /advisor status shows the models actually in use.

Verify the boundary in three checks

Run every command from the repo you will work in, with the same profile you intend to use. For the Ollama example, replace each omp with omp --profile local. Project files merge on top of global ones, so a check from another directory proves little.

# 1. Which providers can this session select, across every catalog kind?
omp models --kind all

# 2. What does the merged config say, including roles you never set?
omp config get modelRoles
omp config get disabledProviders
omp config get advisor.enabled

# 3. Smoke test: which provider and model answered the main request?
smoke_file="$(mktemp)"
omp -p --no-session --mode json "reply with the single word ok" > "$smoke_file" \
  && grep -o -E '"(provider|model)":"[^"]+"' "$smoke_file" | sort | uniq -c

Pass criteria: check 1 lists only your intended providers, check 2 shows a disabledProviders list that matches your policy, and check 3 exits with status 0, answers ok, and names the expected provider and model. Provider and model fields alone do not prove the request succeeded, so read the transcript in $smoke_file if anything looks off, then delete it if it holds sensitive output. Inspect advisors separately with /advisor status. Use these checks to see which providers are selectable and to observe one execution path, not to prove network isolation. Repeat them after changing credentials, project settings, endpoint overrides, or omp versions.

Common mistakes

  • Setting roles and stopping. Run omp models --kind all. If it lists providers you did not choose, you have a preference, not a boundary.
  • Trusting a profile to isolate credentials. It isolates stored auth, not exported variables.
  • Fallback chains to another vendor. The denylist still applies, but a stale entry means recovery fails when you need it. Keep every entry inside the boundary, or use [].
  • Confusing stale role values with a failed boundary. Clean up inherited roles for predictable routing, and use the effective denylist and catalog listing to check exclusion.

Sources