Add provider smokes and model preference - #379

Closed
bussyjd wants to merge 2 commits into
mainfrom
feature/agent-provider-smokes-model-prefer
Closed

Add provider smokes and model preference#379
bussyjd wants to merge 2 commits into
mainfrom
feature/agent-provider-smokes-model-prefer

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

  • Add obol model prefer <model-name> to move a configured LiteLLM model to the front of the preference order and sync stack-managed agent model config.
  • Honor LiteLLM model order when choosing OpenClaw primary/fallback models, while keeping cloud setup primary by preferring the configured cloud model.
  • Add flow-12 provider smokes for local obol-agent inference plus optional Anthropic/OpenAI smokes when API keys are present.

Tests

  • bash -n flows/flow-12-agent-provider-smokes.sh flows/release-smoke.sh
  • go test ./cmd/obol ./internal/model ./internal/openclaw
  • go test ./... -run ^'$' -count=0

Closes#378

@bussyjd

Copy link
Copy Markdown
ContributorAuthor

This still makes sense — does not duplicate or conflict with #386 / #380 / #387. Two small adjustments needed before it can land alongside them; I've prepared a rebased branch with both changes ready: feature/agent-provider-smokes-model-prefer-rebased.

What changed in the rebase:

  1. Rename flow-12-agent-provider-smokes.shflow-14-agent-provider-smokes.sh.Integrate PR #377 (OBOL Permit2) + PR #381 (Hermes runtime), harden flows, add flow-13 dual-stack OBOL #386 (which integrates Integrate OBOL x402 payment rollout #377) ships flow-12-obol-payment.sh, so two different flow-12-*.sh files would coexist after merge. Rename keeps numbering monotonic (12 = OBOL payment, 13 = dual-stack OBOL, 14 = provider smokes). flows/release-smoke.sh and the README are updated to match.

  2. Resolve cmd/obol/model.go cherry-pick conflict.Integrate PR #377 (OBOL Permit2) + PR #381 (Hermes runtime), harden flows, add flow-13 dual-stack OBOL #386 introduced syncAgentModels() (Hermes default-agent path) replacing syncOpenClawModels(). The rebase keeps bothIntegrate PR #377 (OBOL Permit2) + PR #381 (Hermes runtime), harden flows, add flow-13 dual-stack OBOL #386's syncAgentModels() AND Add provider smokes and model preference #379's PreferModel() call — they're complementary: PreferModel pins the configured cloud model so it survives the subsequent sync.

Validation against the rebase tip:

go build ./... → clean
go test ./cmd/obol/... ./internal/model/... ./internal/openclaw/... \
./internal/tunnel/... ./internal/serviceoffercontroller/... \
./internal/stack/... ./internal/hermes/... ./internal/erc8004/...
→ all packages pass
bash -n flows/flow-14-agent-provider-smokes.sh → clean
bash -n flows/release-smoke.sh → clean

Recommended ship order: #387#380#386 → swap this PR's branch for the rebased one (or merge it as-is and close this). The rebased branch will continue to apply cleanly even if the order changes; git merge-tree reports zero conflict markers against any of the others.

Note on rankModels semantics: this PR's rewrite (LiteLLM-order ranking) is intentional and supersedes #386's cloud-bias version. That's the whole point of obol model prefer — without it, the implicit cloud-first override negates the new CLI. Worth flagging in the description so reviewers don't flip it back.

@bussyjd
bussyjd marked this pull request as ready for review April 27, 2026 12:46
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Self-update for the maintainer: I pushed a34442e on this branch — renames flow-12-agent-provider-smokes.shflow-14-agent-provider-smokes.sh (and the release-smoke.sh reference) so this PR doesn't collide with flow-12-obol-payment.sh once #386 lands. Pure rename, no functional change.

The cmd/obol/model.go cherry-pick conflict I described in the previous comment is not addressed here — it only manifests after #386 merges and replaces syncOpenClawModels() with syncAgentModels(). Resolving it now would force this PR to depend on #386 unnecessarily. When #386 lands, the merge here will be one line:

- return syncOpenClawModels(cfg, u)+ return syncAgentModels(cfg, u)

Both call sites (the setup and prefer commands) need the same trivial change.

The original PR moved the chosen model to the front of the LiteLLM
model_list and assumed downstream resolution would honor that order.
But on main, `rankModels` delegates to `model.Rank` for capability-aware
ranking — cloud > local, larger local > smaller local — which silently
overrode the user's pick on every stack restart, defaults refresh, or
auto-configure pass that re-read the model list.
This commit makes the preference load-bearing:
- `internal/model/preference.go` — persist the user's pick in
`$OBOL_CONFIG_DIR/preferred-model`. Survives ConfigMap rewrites,
unlike model_list[0] which any auto-config path can clobber.
- `internal/model/rank.go` — new `RankWithPreference(models, pref)`
that returns the preferred model as primary when present, falling
through to plain `Rank` when the preference is empty or stale.
Preserves provider-prefix tolerance so `claude-sonnet-4-6` matches
`anthropic/claude-sonnet-4-6`.
- `internal/openclaw/openclaw.go` and `internal/hermes/hermes.go` —
`rankModels` now takes cfg and consults the preference file; the
ConfigMap reorder in `PreferModel` is kept purely as a UX hint so
`obol model list` shows the preferred entry on top.
- `PreferModel` writes the preference file FIRST and patches the
ConfigMap second — the preference sticks even when the cluster is
down.
Tests:
- `TestRankWithPreference_*` — five cases covering no-preference,
explicit preference winning over capability, provider-prefix
tolerance, stale preference falling back, empty input.
- `TestPreferenceRoundTrip`, `TestPreferenceClear`,
`TestPreferenceSurvivesArbitraryFiles` — persistence contract.
- `TestRankModelsCapabilityWinsWithoutPreference`,
`TestRankModelsHonorsExplicitPreference`,
`TestRankModelsStalePreferenceFallsBackToCapability` — replace the
old `TestRankModelsHonorsConfiguredOrder`, which asserted the bug
itself (that rankModels returned models[0] regardless of capability).
The defaults-refresh path (autoConfigureLLM) is unaffected — it
patches the LiteLLM ConfigMap but never touches the preference file,
so the preference survives every restart and refresh.
Resolves the rank-overrides-prefer concern raised in #379 review.
@bussyjd
bussyjdforce-pushed the feature/agent-provider-smokes-model-prefer branch from a34442e to 6156195CompareApril 29, 2026 09:55
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Rebased onto current main + addressed the substantive design issue Oisin flagged.

What changed

Rebase: dropped a34442e (the rename to flow-14, which now collides with flow-14-live-obol-base-sepolia.sh on main). Kept the smokes flow at flows/flow-12-agent-provider-smokes.sh — number is open. Cherry-picked 0e7352f cleanly after fixing two collisions: cmd/obol/model.go now calls syncAgentModels (Hermes runtime) instead of syncOpenClawModels, and internal/openclaw/openclaw.go keeps mainʼs better model.Rank (capability-aware) instead of the old models[0] reduction.

The substantive fix (new commit on top: 6156195)

The original PR moved the chosen model to LiteLLM model_list[0] and assumed downstream resolution would honor that order. But on main, rankModels delegates to model.Rank for capability ranking (cloud > local, larger > smaller). Result: every stack restart, defaults refresh, or auto-configure pass silently overrode the userʼs pick. Thatʼs the rank-wins-over-prefer bug Oisin called out.

This commit makes the preference load-bearing:

  • internal/model/preference.go — persist the userʼs pick at $OBOL_CONFIG_DIR/preferred-model. Survives ConfigMap rewrites, unlike model_list[0] which any auto-config path can clobber.
  • internal/model/rank.go::RankWithPreference(models, pref) — preferred wins as primary; empty/stale preference falls through to plain Rank. Provider-prefix tolerant (claude-sonnet-4-6 matches anthropic/claude-sonnet-4-6).
  • internal/openclaw/openclaw.go + internal/hermes/hermes.gorankModels(cfg, models) consults the preference file. The ConfigMap reorder is kept purely as UX (so obol model list shows the preferred entry on top).
  • PreferModel writes the preference file first and patches the ConfigMap second; the preference sticks even when the cluster is down.

Regression coverage

Replaced the old TestRankModelsHonorsConfiguredOrder — that test asserted the bug itself (rank returns models[0] regardless of capability). New tests:

  • TestRankModelsCapabilityWinsWithoutPreference — no preference set → cloud beats local.
  • TestRankModelsHonorsExplicitPreference — preference set on disk → preferred model wins over Claude.
  • TestRankModelsStalePreferenceFallsBackToCapability — preference points at a no-longer-configured model → silently falls back to rank.
  • Plus 6 unit tests in internal/model/preference_test.go covering the persistence + RankWithPreference contract.

Validation

  • go build ./... — clean.
  • go test ./... -timeout 180s — 25 packages, 0 failures.
  • The autoConfigureLLM / defaults-refresh path (internal/stack/stack.go) is unaffected: it patches the LiteLLM ConfigMap but never touches the preference file, so the preference survives every restart and refresh.

Closes the rank-overrides-prefer concern. Tracked as task #38.

@OisinKyne

Copy link
Copy Markdown
Contributor

closing for now in favour of doing model control on the calling agent side rather than the litellm side

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add obol-agent provider inference smoke coverage and model preference command

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add provider smokes and model preference - #379

Closed
bussyjd wants to merge 2 commits into
mainfrom
feature/agent-provider-smokes-model-prefer
Closed

Add provider smokes and model preference#379
bussyjd wants to merge 2 commits into
mainfrom
feature/agent-provider-smokes-model-prefer

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

  • Add obol model prefer <model-name> to move a configured LiteLLM model to the front of the preference order and sync stack-managed agent model config.
  • Honor LiteLLM model order when choosing OpenClaw primary/fallback models, while keeping cloud setup primary by preferring the configured cloud model.
  • Add flow-12 provider smokes for local obol-agent inference plus optional Anthropic/OpenAI smokes when API keys are present.

Tests

  • bash -n flows/flow-12-agent-provider-smokes.sh flows/release-smoke.sh
  • go test ./cmd/obol ./internal/model ./internal/openclaw
  • go test ./... -run ^'$' -count=0

Closes#378

@bussyjd

Copy link
Copy Markdown
ContributorAuthor

This still makes sense — does not duplicate or conflict with #386 / #380 / #387. Two small adjustments needed before it can land alongside them; I've prepared a rebased branch with both changes ready: feature/agent-provider-smokes-model-prefer-rebased.

What changed in the rebase:

  1. Rename flow-12-agent-provider-smokes.shflow-14-agent-provider-smokes.sh.Integrate PR #377 (OBOL Permit2) + PR #381 (Hermes runtime), harden flows, add flow-13 dual-stack OBOL #386 (which integrates Integrate OBOL x402 payment rollout #377) ships flow-12-obol-payment.sh, so two different flow-12-*.sh files would coexist after merge. Rename keeps numbering monotonic (12 = OBOL payment, 13 = dual-stack OBOL, 14 = provider smokes). flows/release-smoke.sh and the README are updated to match.

  2. Resolve cmd/obol/model.go cherry-pick conflict.Integrate PR #377 (OBOL Permit2) + PR #381 (Hermes runtime), harden flows, add flow-13 dual-stack OBOL #386 introduced syncAgentModels() (Hermes default-agent path) replacing syncOpenClawModels(). The rebase keeps bothIntegrate PR #377 (OBOL Permit2) + PR #381 (Hermes runtime), harden flows, add flow-13 dual-stack OBOL #386's syncAgentModels() AND Add provider smokes and model preference #379's PreferModel() call — they're complementary: PreferModel pins the configured cloud model so it survives the subsequent sync.

Validation against the rebase tip:

go build ./... → clean
go test ./cmd/obol/... ./internal/model/... ./internal/openclaw/... \
./internal/tunnel/... ./internal/serviceoffercontroller/... \
./internal/stack/... ./internal/hermes/... ./internal/erc8004/...
→ all packages pass
bash -n flows/flow-14-agent-provider-smokes.sh → clean
bash -n flows/release-smoke.sh → clean

Recommended ship order: #387#380#386 → swap this PR's branch for the rebased one (or merge it as-is and close this). The rebased branch will continue to apply cleanly even if the order changes; git merge-tree reports zero conflict markers against any of the others.

Note on rankModels semantics: this PR's rewrite (LiteLLM-order ranking) is intentional and supersedes #386's cloud-bias version. That's the whole point of obol model prefer — without it, the implicit cloud-first override negates the new CLI. Worth flagging in the description so reviewers don't flip it back.

@bussyjd
bussyjd marked this pull request as ready for review April 27, 2026 12:46
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Self-update for the maintainer: I pushed a34442e on this branch — renames flow-12-agent-provider-smokes.shflow-14-agent-provider-smokes.sh (and the release-smoke.sh reference) so this PR doesn't collide with flow-12-obol-payment.sh once #386 lands. Pure rename, no functional change.

The cmd/obol/model.go cherry-pick conflict I described in the previous comment is not addressed here — it only manifests after #386 merges and replaces syncOpenClawModels() with syncAgentModels(). Resolving it now would force this PR to depend on #386 unnecessarily. When #386 lands, the merge here will be one line:

- return syncOpenClawModels(cfg, u)+ return syncAgentModels(cfg, u)

Both call sites (the setup and prefer commands) need the same trivial change.

The original PR moved the chosen model to the front of the LiteLLM
model_list and assumed downstream resolution would honor that order.
But on main, `rankModels` delegates to `model.Rank` for capability-aware
ranking — cloud > local, larger local > smaller local — which silently
overrode the user's pick on every stack restart, defaults refresh, or
auto-configure pass that re-read the model list.
This commit makes the preference load-bearing:
- `internal/model/preference.go` — persist the user's pick in
`$OBOL_CONFIG_DIR/preferred-model`. Survives ConfigMap rewrites,
unlike model_list[0] which any auto-config path can clobber.
- `internal/model/rank.go` — new `RankWithPreference(models, pref)`
that returns the preferred model as primary when present, falling
through to plain `Rank` when the preference is empty or stale.
Preserves provider-prefix tolerance so `claude-sonnet-4-6` matches
`anthropic/claude-sonnet-4-6`.
- `internal/openclaw/openclaw.go` and `internal/hermes/hermes.go` —
`rankModels` now takes cfg and consults the preference file; the
ConfigMap reorder in `PreferModel` is kept purely as a UX hint so
`obol model list` shows the preferred entry on top.
- `PreferModel` writes the preference file FIRST and patches the
ConfigMap second — the preference sticks even when the cluster is
down.
Tests:
- `TestRankWithPreference_*` — five cases covering no-preference,
explicit preference winning over capability, provider-prefix
tolerance, stale preference falling back, empty input.
- `TestPreferenceRoundTrip`, `TestPreferenceClear`,
`TestPreferenceSurvivesArbitraryFiles` — persistence contract.
- `TestRankModelsCapabilityWinsWithoutPreference`,
`TestRankModelsHonorsExplicitPreference`,
`TestRankModelsStalePreferenceFallsBackToCapability` — replace the
old `TestRankModelsHonorsConfiguredOrder`, which asserted the bug
itself (that rankModels returned models[0] regardless of capability).
The defaults-refresh path (autoConfigureLLM) is unaffected — it
patches the LiteLLM ConfigMap but never touches the preference file,
so the preference survives every restart and refresh.
Resolves the rank-overrides-prefer concern raised in #379 review.
@bussyjd
bussyjdforce-pushed the feature/agent-provider-smokes-model-prefer branch from a34442e to 6156195CompareApril 29, 2026 09:55
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Rebased onto current main + addressed the substantive design issue Oisin flagged.

What changed

Rebase: dropped a34442e (the rename to flow-14, which now collides with flow-14-live-obol-base-sepolia.sh on main). Kept the smokes flow at flows/flow-12-agent-provider-smokes.sh — number is open. Cherry-picked 0e7352f cleanly after fixing two collisions: cmd/obol/model.go now calls syncAgentModels (Hermes runtime) instead of syncOpenClawModels, and internal/openclaw/openclaw.go keeps mainʼs better model.Rank (capability-aware) instead of the old models[0] reduction.

The substantive fix (new commit on top: 6156195)

The original PR moved the chosen model to LiteLLM model_list[0] and assumed downstream resolution would honor that order. But on main, rankModels delegates to model.Rank for capability ranking (cloud > local, larger > smaller). Result: every stack restart, defaults refresh, or auto-configure pass silently overrode the userʼs pick. Thatʼs the rank-wins-over-prefer bug Oisin called out.

This commit makes the preference load-bearing:

  • internal/model/preference.go — persist the userʼs pick at $OBOL_CONFIG_DIR/preferred-model. Survives ConfigMap rewrites, unlike model_list[0] which any auto-config path can clobber.
  • internal/model/rank.go::RankWithPreference(models, pref) — preferred wins as primary; empty/stale preference falls through to plain Rank. Provider-prefix tolerant (claude-sonnet-4-6 matches anthropic/claude-sonnet-4-6).
  • internal/openclaw/openclaw.go + internal/hermes/hermes.gorankModels(cfg, models) consults the preference file. The ConfigMap reorder is kept purely as UX (so obol model list shows the preferred entry on top).
  • PreferModel writes the preference file first and patches the ConfigMap second; the preference sticks even when the cluster is down.

Regression coverage

Replaced the old TestRankModelsHonorsConfiguredOrder — that test asserted the bug itself (rank returns models[0] regardless of capability). New tests:

  • TestRankModelsCapabilityWinsWithoutPreference — no preference set → cloud beats local.
  • TestRankModelsHonorsExplicitPreference — preference set on disk → preferred model wins over Claude.
  • TestRankModelsStalePreferenceFallsBackToCapability — preference points at a no-longer-configured model → silently falls back to rank.
  • Plus 6 unit tests in internal/model/preference_test.go covering the persistence + RankWithPreference contract.

Validation

  • go build ./... — clean.
  • go test ./... -timeout 180s — 25 packages, 0 failures.
  • The autoConfigureLLM / defaults-refresh path (internal/stack/stack.go) is unaffected: it patches the LiteLLM ConfigMap but never touches the preference file, so the preference survives every restart and refresh.

Closes the rank-overrides-prefer concern. Tracked as task #38.

@OisinKyne

Copy link
Copy Markdown
Contributor

closing for now in favour of doing model control on the calling agent side rather than the litellm side

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add obol-agent provider inference smoke coverage and model preference command

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add provider smokes and model preference - #379

Closed
bussyjd wants to merge 2 commits into
mainfrom
feature/agent-provider-smokes-model-prefer
Closed

Add provider smokes and model preference#379
bussyjd wants to merge 2 commits into
mainfrom
feature/agent-provider-smokes-model-prefer

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

  • Add obol model prefer <model-name> to move a configured LiteLLM model to the front of the preference order and sync stack-managed agent model config.
  • Honor LiteLLM model order when choosing OpenClaw primary/fallback models, while keeping cloud setup primary by preferring the configured cloud model.
  • Add flow-12 provider smokes for local obol-agent inference plus optional Anthropic/OpenAI smokes when API keys are present.

Tests

  • bash -n flows/flow-12-agent-provider-smokes.sh flows/release-smoke.sh
  • go test ./cmd/obol ./internal/model ./internal/openclaw
  • go test ./... -run ^'$' -count=0

Closes#378

@bussyjd

Copy link
Copy Markdown
ContributorAuthor

This still makes sense — does not duplicate or conflict with #386 / #380 / #387. Two small adjustments needed before it can land alongside them; I've prepared a rebased branch with both changes ready: feature/agent-provider-smokes-model-prefer-rebased.

What changed in the rebase:

  1. Rename flow-12-agent-provider-smokes.shflow-14-agent-provider-smokes.sh.Integrate PR #377 (OBOL Permit2) + PR #381 (Hermes runtime), harden flows, add flow-13 dual-stack OBOL #386 (which integrates Integrate OBOL x402 payment rollout #377) ships flow-12-obol-payment.sh, so two different flow-12-*.sh files would coexist after merge. Rename keeps numbering monotonic (12 = OBOL payment, 13 = dual-stack OBOL, 14 = provider smokes). flows/release-smoke.sh and the README are updated to match.

  2. Resolve cmd/obol/model.go cherry-pick conflict.Integrate PR #377 (OBOL Permit2) + PR #381 (Hermes runtime), harden flows, add flow-13 dual-stack OBOL #386 introduced syncAgentModels() (Hermes default-agent path) replacing syncOpenClawModels(). The rebase keeps bothIntegrate PR #377 (OBOL Permit2) + PR #381 (Hermes runtime), harden flows, add flow-13 dual-stack OBOL #386's syncAgentModels() AND Add provider smokes and model preference #379's PreferModel() call — they're complementary: PreferModel pins the configured cloud model so it survives the subsequent sync.

Validation against the rebase tip:

go build ./... → clean
go test ./cmd/obol/... ./internal/model/... ./internal/openclaw/... \
./internal/tunnel/... ./internal/serviceoffercontroller/... \
./internal/stack/... ./internal/hermes/... ./internal/erc8004/...
→ all packages pass
bash -n flows/flow-14-agent-provider-smokes.sh → clean
bash -n flows/release-smoke.sh → clean

Recommended ship order: #387#380#386 → swap this PR's branch for the rebased one (or merge it as-is and close this). The rebased branch will continue to apply cleanly even if the order changes; git merge-tree reports zero conflict markers against any of the others.

Note on rankModels semantics: this PR's rewrite (LiteLLM-order ranking) is intentional and supersedes #386's cloud-bias version. That's the whole point of obol model prefer — without it, the implicit cloud-first override negates the new CLI. Worth flagging in the description so reviewers don't flip it back.

@bussyjd
bussyjd marked this pull request as ready for review April 27, 2026 12:46
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Self-update for the maintainer: I pushed a34442e on this branch — renames flow-12-agent-provider-smokes.shflow-14-agent-provider-smokes.sh (and the release-smoke.sh reference) so this PR doesn't collide with flow-12-obol-payment.sh once #386 lands. Pure rename, no functional change.

The cmd/obol/model.go cherry-pick conflict I described in the previous comment is not addressed here — it only manifests after #386 merges and replaces syncOpenClawModels() with syncAgentModels(). Resolving it now would force this PR to depend on #386 unnecessarily. When #386 lands, the merge here will be one line:

- return syncOpenClawModels(cfg, u)+ return syncAgentModels(cfg, u)

Both call sites (the setup and prefer commands) need the same trivial change.

The original PR moved the chosen model to the front of the LiteLLM
model_list and assumed downstream resolution would honor that order.
But on main, `rankModels` delegates to `model.Rank` for capability-aware
ranking — cloud > local, larger local > smaller local — which silently
overrode the user's pick on every stack restart, defaults refresh, or
auto-configure pass that re-read the model list.
This commit makes the preference load-bearing:
- `internal/model/preference.go` — persist the user's pick in
`$OBOL_CONFIG_DIR/preferred-model`. Survives ConfigMap rewrites,
unlike model_list[0] which any auto-config path can clobber.
- `internal/model/rank.go` — new `RankWithPreference(models, pref)`
that returns the preferred model as primary when present, falling
through to plain `Rank` when the preference is empty or stale.
Preserves provider-prefix tolerance so `claude-sonnet-4-6` matches
`anthropic/claude-sonnet-4-6`.
- `internal/openclaw/openclaw.go` and `internal/hermes/hermes.go` —
`rankModels` now takes cfg and consults the preference file; the
ConfigMap reorder in `PreferModel` is kept purely as a UX hint so
`obol model list` shows the preferred entry on top.
- `PreferModel` writes the preference file FIRST and patches the
ConfigMap second — the preference sticks even when the cluster is
down.
Tests:
- `TestRankWithPreference_*` — five cases covering no-preference,
explicit preference winning over capability, provider-prefix
tolerance, stale preference falling back, empty input.
- `TestPreferenceRoundTrip`, `TestPreferenceClear`,
`TestPreferenceSurvivesArbitraryFiles` — persistence contract.
- `TestRankModelsCapabilityWinsWithoutPreference`,
`TestRankModelsHonorsExplicitPreference`,
`TestRankModelsStalePreferenceFallsBackToCapability` — replace the
old `TestRankModelsHonorsConfiguredOrder`, which asserted the bug
itself (that rankModels returned models[0] regardless of capability).
The defaults-refresh path (autoConfigureLLM) is unaffected — it
patches the LiteLLM ConfigMap but never touches the preference file,
so the preference survives every restart and refresh.
Resolves the rank-overrides-prefer concern raised in #379 review.
@bussyjd
bussyjdforce-pushed the feature/agent-provider-smokes-model-prefer branch from a34442e to 6156195CompareApril 29, 2026 09:55
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Rebased onto current main + addressed the substantive design issue Oisin flagged.

What changed

Rebase: dropped a34442e (the rename to flow-14, which now collides with flow-14-live-obol-base-sepolia.sh on main). Kept the smokes flow at flows/flow-12-agent-provider-smokes.sh — number is open. Cherry-picked 0e7352f cleanly after fixing two collisions: cmd/obol/model.go now calls syncAgentModels (Hermes runtime) instead of syncOpenClawModels, and internal/openclaw/openclaw.go keeps mainʼs better model.Rank (capability-aware) instead of the old models[0] reduction.

The substantive fix (new commit on top: 6156195)

The original PR moved the chosen model to LiteLLM model_list[0] and assumed downstream resolution would honor that order. But on main, rankModels delegates to model.Rank for capability ranking (cloud > local, larger > smaller). Result: every stack restart, defaults refresh, or auto-configure pass silently overrode the userʼs pick. Thatʼs the rank-wins-over-prefer bug Oisin called out.

This commit makes the preference load-bearing:

  • internal/model/preference.go — persist the userʼs pick at $OBOL_CONFIG_DIR/preferred-model. Survives ConfigMap rewrites, unlike model_list[0] which any auto-config path can clobber.
  • internal/model/rank.go::RankWithPreference(models, pref) — preferred wins as primary; empty/stale preference falls through to plain Rank. Provider-prefix tolerant (claude-sonnet-4-6 matches anthropic/claude-sonnet-4-6).
  • internal/openclaw/openclaw.go + internal/hermes/hermes.gorankModels(cfg, models) consults the preference file. The ConfigMap reorder is kept purely as UX (so obol model list shows the preferred entry on top).
  • PreferModel writes the preference file first and patches the ConfigMap second; the preference sticks even when the cluster is down.

Regression coverage

Replaced the old TestRankModelsHonorsConfiguredOrder — that test asserted the bug itself (rank returns models[0] regardless of capability). New tests:

  • TestRankModelsCapabilityWinsWithoutPreference — no preference set → cloud beats local.
  • TestRankModelsHonorsExplicitPreference — preference set on disk → preferred model wins over Claude.
  • TestRankModelsStalePreferenceFallsBackToCapability — preference points at a no-longer-configured model → silently falls back to rank.
  • Plus 6 unit tests in internal/model/preference_test.go covering the persistence + RankWithPreference contract.

Validation

  • go build ./... — clean.
  • go test ./... -timeout 180s — 25 packages, 0 failures.
  • The autoConfigureLLM / defaults-refresh path (internal/stack/stack.go) is unaffected: it patches the LiteLLM ConfigMap but never touches the preference file, so the preference survives every restart and refresh.

Closes the rank-overrides-prefer concern. Tracked as task #38.

@OisinKyne

Copy link
Copy Markdown
Contributor

closing for now in favour of doing model control on the calling agent side rather than the litellm side

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add obol-agent provider inference smoke coverage and model preference command

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add provider smokes and model preference - #379

Closed
bussyjd wants to merge 2 commits into
mainfrom
feature/agent-provider-smokes-model-prefer
Closed

Add provider smokes and model preference#379
bussyjd wants to merge 2 commits into
mainfrom
feature/agent-provider-smokes-model-prefer

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

  • Add obol model prefer <model-name> to move a configured LiteLLM model to the front of the preference order and sync stack-managed agent model config.
  • Honor LiteLLM model order when choosing OpenClaw primary/fallback models, while keeping cloud setup primary by preferring the configured cloud model.
  • Add flow-12 provider smokes for local obol-agent inference plus optional Anthropic/OpenAI smokes when API keys are present.

Tests

  • bash -n flows/flow-12-agent-provider-smokes.sh flows/release-smoke.sh
  • go test ./cmd/obol ./internal/model ./internal/openclaw
  • go test ./... -run ^'$' -count=0

Closes#378

@bussyjd

Copy link
Copy Markdown
ContributorAuthor

This still makes sense — does not duplicate or conflict with #386 / #380 / #387. Two small adjustments needed before it can land alongside them; I've prepared a rebased branch with both changes ready: feature/agent-provider-smokes-model-prefer-rebased.

What changed in the rebase:

  1. Rename flow-12-agent-provider-smokes.shflow-14-agent-provider-smokes.sh.Integrate PR #377 (OBOL Permit2) + PR #381 (Hermes runtime), harden flows, add flow-13 dual-stack OBOL #386 (which integrates Integrate OBOL x402 payment rollout #377) ships flow-12-obol-payment.sh, so two different flow-12-*.sh files would coexist after merge. Rename keeps numbering monotonic (12 = OBOL payment, 13 = dual-stack OBOL, 14 = provider smokes). flows/release-smoke.sh and the README are updated to match.

  2. Resolve cmd/obol/model.go cherry-pick conflict.Integrate PR #377 (OBOL Permit2) + PR #381 (Hermes runtime), harden flows, add flow-13 dual-stack OBOL #386 introduced syncAgentModels() (Hermes default-agent path) replacing syncOpenClawModels(). The rebase keeps bothIntegrate PR #377 (OBOL Permit2) + PR #381 (Hermes runtime), harden flows, add flow-13 dual-stack OBOL #386's syncAgentModels() AND Add provider smokes and model preference #379's PreferModel() call — they're complementary: PreferModel pins the configured cloud model so it survives the subsequent sync.

Validation against the rebase tip:

go build ./... → clean
go test ./cmd/obol/... ./internal/model/... ./internal/openclaw/... \
./internal/tunnel/... ./internal/serviceoffercontroller/... \
./internal/stack/... ./internal/hermes/... ./internal/erc8004/...
→ all packages pass
bash -n flows/flow-14-agent-provider-smokes.sh → clean
bash -n flows/release-smoke.sh → clean

Recommended ship order: #387#380#386 → swap this PR's branch for the rebased one (or merge it as-is and close this). The rebased branch will continue to apply cleanly even if the order changes; git merge-tree reports zero conflict markers against any of the others.

Note on rankModels semantics: this PR's rewrite (LiteLLM-order ranking) is intentional and supersedes #386's cloud-bias version. That's the whole point of obol model prefer — without it, the implicit cloud-first override negates the new CLI. Worth flagging in the description so reviewers don't flip it back.

@bussyjd
bussyjd marked this pull request as ready for review April 27, 2026 12:46
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Self-update for the maintainer: I pushed a34442e on this branch — renames flow-12-agent-provider-smokes.shflow-14-agent-provider-smokes.sh (and the release-smoke.sh reference) so this PR doesn't collide with flow-12-obol-payment.sh once #386 lands. Pure rename, no functional change.

The cmd/obol/model.go cherry-pick conflict I described in the previous comment is not addressed here — it only manifests after #386 merges and replaces syncOpenClawModels() with syncAgentModels(). Resolving it now would force this PR to depend on #386 unnecessarily. When #386 lands, the merge here will be one line:

- return syncOpenClawModels(cfg, u)+ return syncAgentModels(cfg, u)

Both call sites (the setup and prefer commands) need the same trivial change.

The original PR moved the chosen model to the front of the LiteLLM
model_list and assumed downstream resolution would honor that order.
But on main, `rankModels` delegates to `model.Rank` for capability-aware
ranking — cloud > local, larger local > smaller local — which silently
overrode the user's pick on every stack restart, defaults refresh, or
auto-configure pass that re-read the model list.
This commit makes the preference load-bearing:
- `internal/model/preference.go` — persist the user's pick in
`$OBOL_CONFIG_DIR/preferred-model`. Survives ConfigMap rewrites,
unlike model_list[0] which any auto-config path can clobber.
- `internal/model/rank.go` — new `RankWithPreference(models, pref)`
that returns the preferred model as primary when present, falling
through to plain `Rank` when the preference is empty or stale.
Preserves provider-prefix tolerance so `claude-sonnet-4-6` matches
`anthropic/claude-sonnet-4-6`.
- `internal/openclaw/openclaw.go` and `internal/hermes/hermes.go` —
`rankModels` now takes cfg and consults the preference file; the
ConfigMap reorder in `PreferModel` is kept purely as a UX hint so
`obol model list` shows the preferred entry on top.
- `PreferModel` writes the preference file FIRST and patches the
ConfigMap second — the preference sticks even when the cluster is
down.
Tests:
- `TestRankWithPreference_*` — five cases covering no-preference,
explicit preference winning over capability, provider-prefix
tolerance, stale preference falling back, empty input.
- `TestPreferenceRoundTrip`, `TestPreferenceClear`,
`TestPreferenceSurvivesArbitraryFiles` — persistence contract.
- `TestRankModelsCapabilityWinsWithoutPreference`,
`TestRankModelsHonorsExplicitPreference`,
`TestRankModelsStalePreferenceFallsBackToCapability` — replace the
old `TestRankModelsHonorsConfiguredOrder`, which asserted the bug
itself (that rankModels returned models[0] regardless of capability).
The defaults-refresh path (autoConfigureLLM) is unaffected — it
patches the LiteLLM ConfigMap but never touches the preference file,
so the preference survives every restart and refresh.
Resolves the rank-overrides-prefer concern raised in #379 review.
@bussyjd
bussyjdforce-pushed the feature/agent-provider-smokes-model-prefer branch from a34442e to 6156195CompareApril 29, 2026 09:55
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Rebased onto current main + addressed the substantive design issue Oisin flagged.

What changed

Rebase: dropped a34442e (the rename to flow-14, which now collides with flow-14-live-obol-base-sepolia.sh on main). Kept the smokes flow at flows/flow-12-agent-provider-smokes.sh — number is open. Cherry-picked 0e7352f cleanly after fixing two collisions: cmd/obol/model.go now calls syncAgentModels (Hermes runtime) instead of syncOpenClawModels, and internal/openclaw/openclaw.go keeps mainʼs better model.Rank (capability-aware) instead of the old models[0] reduction.

The substantive fix (new commit on top: 6156195)

The original PR moved the chosen model to LiteLLM model_list[0] and assumed downstream resolution would honor that order. But on main, rankModels delegates to model.Rank for capability ranking (cloud > local, larger > smaller). Result: every stack restart, defaults refresh, or auto-configure pass silently overrode the userʼs pick. Thatʼs the rank-wins-over-prefer bug Oisin called out.

This commit makes the preference load-bearing:

  • internal/model/preference.go — persist the userʼs pick at $OBOL_CONFIG_DIR/preferred-model. Survives ConfigMap rewrites, unlike model_list[0] which any auto-config path can clobber.
  • internal/model/rank.go::RankWithPreference(models, pref) — preferred wins as primary; empty/stale preference falls through to plain Rank. Provider-prefix tolerant (claude-sonnet-4-6 matches anthropic/claude-sonnet-4-6).
  • internal/openclaw/openclaw.go + internal/hermes/hermes.gorankModels(cfg, models) consults the preference file. The ConfigMap reorder is kept purely as UX (so obol model list shows the preferred entry on top).
  • PreferModel writes the preference file first and patches the ConfigMap second; the preference sticks even when the cluster is down.

Regression coverage

Replaced the old TestRankModelsHonorsConfiguredOrder — that test asserted the bug itself (rank returns models[0] regardless of capability). New tests:

  • TestRankModelsCapabilityWinsWithoutPreference — no preference set → cloud beats local.
  • TestRankModelsHonorsExplicitPreference — preference set on disk → preferred model wins over Claude.
  • TestRankModelsStalePreferenceFallsBackToCapability — preference points at a no-longer-configured model → silently falls back to rank.
  • Plus 6 unit tests in internal/model/preference_test.go covering the persistence + RankWithPreference contract.

Validation

  • go build ./... — clean.
  • go test ./... -timeout 180s — 25 packages, 0 failures.
  • The autoConfigureLLM / defaults-refresh path (internal/stack/stack.go) is unaffected: it patches the LiteLLM ConfigMap but never touches the preference file, so the preference survives every restart and refresh.

Closes the rank-overrides-prefer concern. Tracked as task #38.

@OisinKyne

Copy link
Copy Markdown
Contributor

closing for now in favour of doing model control on the calling agent side rather than the litellm side

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add obol-agent provider inference smoke coverage and model preference command

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add provider smokes and model preference - #379

Closed
bussyjd wants to merge 2 commits into
mainfrom
feature/agent-provider-smokes-model-prefer
Closed

Add provider smokes and model preference#379
bussyjd wants to merge 2 commits into
mainfrom
feature/agent-provider-smokes-model-prefer

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

  • Add obol model prefer <model-name> to move a configured LiteLLM model to the front of the preference order and sync stack-managed agent model config.
  • Honor LiteLLM model order when choosing OpenClaw primary/fallback models, while keeping cloud setup primary by preferring the configured cloud model.
  • Add flow-12 provider smokes for local obol-agent inference plus optional Anthropic/OpenAI smokes when API keys are present.

Tests

  • bash -n flows/flow-12-agent-provider-smokes.sh flows/release-smoke.sh
  • go test ./cmd/obol ./internal/model ./internal/openclaw
  • go test ./... -run ^'$' -count=0

Closes#378

@bussyjd

Copy link
Copy Markdown
ContributorAuthor

This still makes sense — does not duplicate or conflict with #386 / #380 / #387. Two small adjustments needed before it can land alongside them; I've prepared a rebased branch with both changes ready: feature/agent-provider-smokes-model-prefer-rebased.

What changed in the rebase:

  1. Rename flow-12-agent-provider-smokes.shflow-14-agent-provider-smokes.sh.Integrate PR #377 (OBOL Permit2) + PR #381 (Hermes runtime), harden flows, add flow-13 dual-stack OBOL #386 (which integrates Integrate OBOL x402 payment rollout #377) ships flow-12-obol-payment.sh, so two different flow-12-*.sh files would coexist after merge. Rename keeps numbering monotonic (12 = OBOL payment, 13 = dual-stack OBOL, 14 = provider smokes). flows/release-smoke.sh and the README are updated to match.

  2. Resolve cmd/obol/model.go cherry-pick conflict.Integrate PR #377 (OBOL Permit2) + PR #381 (Hermes runtime), harden flows, add flow-13 dual-stack OBOL #386 introduced syncAgentModels() (Hermes default-agent path) replacing syncOpenClawModels(). The rebase keeps bothIntegrate PR #377 (OBOL Permit2) + PR #381 (Hermes runtime), harden flows, add flow-13 dual-stack OBOL #386's syncAgentModels() AND Add provider smokes and model preference #379's PreferModel() call — they're complementary: PreferModel pins the configured cloud model so it survives the subsequent sync.

Validation against the rebase tip:

go build ./... → clean
go test ./cmd/obol/... ./internal/model/... ./internal/openclaw/... \
./internal/tunnel/... ./internal/serviceoffercontroller/... \
./internal/stack/... ./internal/hermes/... ./internal/erc8004/...
→ all packages pass
bash -n flows/flow-14-agent-provider-smokes.sh → clean
bash -n flows/release-smoke.sh → clean

Recommended ship order: #387#380#386 → swap this PR's branch for the rebased one (or merge it as-is and close this). The rebased branch will continue to apply cleanly even if the order changes; git merge-tree reports zero conflict markers against any of the others.

Note on rankModels semantics: this PR's rewrite (LiteLLM-order ranking) is intentional and supersedes #386's cloud-bias version. That's the whole point of obol model prefer — without it, the implicit cloud-first override negates the new CLI. Worth flagging in the description so reviewers don't flip it back.

@bussyjd
bussyjd marked this pull request as ready for review April 27, 2026 12:46
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Self-update for the maintainer: I pushed a34442e on this branch — renames flow-12-agent-provider-smokes.shflow-14-agent-provider-smokes.sh (and the release-smoke.sh reference) so this PR doesn't collide with flow-12-obol-payment.sh once #386 lands. Pure rename, no functional change.

The cmd/obol/model.go cherry-pick conflict I described in the previous comment is not addressed here — it only manifests after #386 merges and replaces syncOpenClawModels() with syncAgentModels(). Resolving it now would force this PR to depend on #386 unnecessarily. When #386 lands, the merge here will be one line:

- return syncOpenClawModels(cfg, u)+ return syncAgentModels(cfg, u)

Both call sites (the setup and prefer commands) need the same trivial change.

The original PR moved the chosen model to the front of the LiteLLM
model_list and assumed downstream resolution would honor that order.
But on main, `rankModels` delegates to `model.Rank` for capability-aware
ranking — cloud > local, larger local > smaller local — which silently
overrode the user's pick on every stack restart, defaults refresh, or
auto-configure pass that re-read the model list.
This commit makes the preference load-bearing:
- `internal/model/preference.go` — persist the user's pick in
`$OBOL_CONFIG_DIR/preferred-model`. Survives ConfigMap rewrites,
unlike model_list[0] which any auto-config path can clobber.
- `internal/model/rank.go` — new `RankWithPreference(models, pref)`
that returns the preferred model as primary when present, falling
through to plain `Rank` when the preference is empty or stale.
Preserves provider-prefix tolerance so `claude-sonnet-4-6` matches
`anthropic/claude-sonnet-4-6`.
- `internal/openclaw/openclaw.go` and `internal/hermes/hermes.go` —
`rankModels` now takes cfg and consults the preference file; the
ConfigMap reorder in `PreferModel` is kept purely as a UX hint so
`obol model list` shows the preferred entry on top.
- `PreferModel` writes the preference file FIRST and patches the
ConfigMap second — the preference sticks even when the cluster is
down.
Tests:
- `TestRankWithPreference_*` — five cases covering no-preference,
explicit preference winning over capability, provider-prefix
tolerance, stale preference falling back, empty input.
- `TestPreferenceRoundTrip`, `TestPreferenceClear`,
`TestPreferenceSurvivesArbitraryFiles` — persistence contract.
- `TestRankModelsCapabilityWinsWithoutPreference`,
`TestRankModelsHonorsExplicitPreference`,
`TestRankModelsStalePreferenceFallsBackToCapability` — replace the
old `TestRankModelsHonorsConfiguredOrder`, which asserted the bug
itself (that rankModels returned models[0] regardless of capability).
The defaults-refresh path (autoConfigureLLM) is unaffected — it
patches the LiteLLM ConfigMap but never touches the preference file,
so the preference survives every restart and refresh.
Resolves the rank-overrides-prefer concern raised in #379 review.
@bussyjd
bussyjdforce-pushed the feature/agent-provider-smokes-model-prefer branch from a34442e to 6156195CompareApril 29, 2026 09:55
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Rebased onto current main + addressed the substantive design issue Oisin flagged.

What changed

Rebase: dropped a34442e (the rename to flow-14, which now collides with flow-14-live-obol-base-sepolia.sh on main). Kept the smokes flow at flows/flow-12-agent-provider-smokes.sh — number is open. Cherry-picked 0e7352f cleanly after fixing two collisions: cmd/obol/model.go now calls syncAgentModels (Hermes runtime) instead of syncOpenClawModels, and internal/openclaw/openclaw.go keeps mainʼs better model.Rank (capability-aware) instead of the old models[0] reduction.

The substantive fix (new commit on top: 6156195)

The original PR moved the chosen model to LiteLLM model_list[0] and assumed downstream resolution would honor that order. But on main, rankModels delegates to model.Rank for capability ranking (cloud > local, larger > smaller). Result: every stack restart, defaults refresh, or auto-configure pass silently overrode the userʼs pick. Thatʼs the rank-wins-over-prefer bug Oisin called out.

This commit makes the preference load-bearing:

  • internal/model/preference.go — persist the userʼs pick at $OBOL_CONFIG_DIR/preferred-model. Survives ConfigMap rewrites, unlike model_list[0] which any auto-config path can clobber.
  • internal/model/rank.go::RankWithPreference(models, pref) — preferred wins as primary; empty/stale preference falls through to plain Rank. Provider-prefix tolerant (claude-sonnet-4-6 matches anthropic/claude-sonnet-4-6).
  • internal/openclaw/openclaw.go + internal/hermes/hermes.gorankModels(cfg, models) consults the preference file. The ConfigMap reorder is kept purely as UX (so obol model list shows the preferred entry on top).
  • PreferModel writes the preference file first and patches the ConfigMap second; the preference sticks even when the cluster is down.

Regression coverage

Replaced the old TestRankModelsHonorsConfiguredOrder — that test asserted the bug itself (rank returns models[0] regardless of capability). New tests:

  • TestRankModelsCapabilityWinsWithoutPreference — no preference set → cloud beats local.
  • TestRankModelsHonorsExplicitPreference — preference set on disk → preferred model wins over Claude.
  • TestRankModelsStalePreferenceFallsBackToCapability — preference points at a no-longer-configured model → silently falls back to rank.
  • Plus 6 unit tests in internal/model/preference_test.go covering the persistence + RankWithPreference contract.

Validation

  • go build ./... — clean.
  • go test ./... -timeout 180s — 25 packages, 0 failures.
  • The autoConfigureLLM / defaults-refresh path (internal/stack/stack.go) is unaffected: it patches the LiteLLM ConfigMap but never touches the preference file, so the preference survives every restart and refresh.

Closes the rank-overrides-prefer concern. Tracked as task #38.

@OisinKyne

Copy link
Copy Markdown
Contributor

closing for now in favour of doing model control on the calling agent side rather than the litellm side

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add obol-agent provider inference smoke coverage and model preference command

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add provider smokes and model preference - #379

Closed
bussyjd wants to merge 2 commits into
mainfrom
feature/agent-provider-smokes-model-prefer
Closed

Add provider smokes and model preference#379
bussyjd wants to merge 2 commits into
mainfrom
feature/agent-provider-smokes-model-prefer

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

  • Add obol model prefer <model-name> to move a configured LiteLLM model to the front of the preference order and sync stack-managed agent model config.
  • Honor LiteLLM model order when choosing OpenClaw primary/fallback models, while keeping cloud setup primary by preferring the configured cloud model.
  • Add flow-12 provider smokes for local obol-agent inference plus optional Anthropic/OpenAI smokes when API keys are present.

Tests

  • bash -n flows/flow-12-agent-provider-smokes.sh flows/release-smoke.sh
  • go test ./cmd/obol ./internal/model ./internal/openclaw
  • go test ./... -run ^'$' -count=0

Closes#378

@bussyjd

Copy link
Copy Markdown
ContributorAuthor

This still makes sense — does not duplicate or conflict with #386 / #380 / #387. Two small adjustments needed before it can land alongside them; I've prepared a rebased branch with both changes ready: feature/agent-provider-smokes-model-prefer-rebased.

What changed in the rebase:

  1. Rename flow-12-agent-provider-smokes.shflow-14-agent-provider-smokes.sh.Integrate PR #377 (OBOL Permit2) + PR #381 (Hermes runtime), harden flows, add flow-13 dual-stack OBOL #386 (which integrates Integrate OBOL x402 payment rollout #377) ships flow-12-obol-payment.sh, so two different flow-12-*.sh files would coexist after merge. Rename keeps numbering monotonic (12 = OBOL payment, 13 = dual-stack OBOL, 14 = provider smokes). flows/release-smoke.sh and the README are updated to match.

  2. Resolve cmd/obol/model.go cherry-pick conflict.Integrate PR #377 (OBOL Permit2) + PR #381 (Hermes runtime), harden flows, add flow-13 dual-stack OBOL #386 introduced syncAgentModels() (Hermes default-agent path) replacing syncOpenClawModels(). The rebase keeps bothIntegrate PR #377 (OBOL Permit2) + PR #381 (Hermes runtime), harden flows, add flow-13 dual-stack OBOL #386's syncAgentModels() AND Add provider smokes and model preference #379's PreferModel() call — they're complementary: PreferModel pins the configured cloud model so it survives the subsequent sync.

Validation against the rebase tip:

go build ./... → clean
go test ./cmd/obol/... ./internal/model/... ./internal/openclaw/... \
./internal/tunnel/... ./internal/serviceoffercontroller/... \
./internal/stack/... ./internal/hermes/... ./internal/erc8004/...
→ all packages pass
bash -n flows/flow-14-agent-provider-smokes.sh → clean
bash -n flows/release-smoke.sh → clean

Recommended ship order: #387#380#386 → swap this PR's branch for the rebased one (or merge it as-is and close this). The rebased branch will continue to apply cleanly even if the order changes; git merge-tree reports zero conflict markers against any of the others.

Note on rankModels semantics: this PR's rewrite (LiteLLM-order ranking) is intentional and supersedes #386's cloud-bias version. That's the whole point of obol model prefer — without it, the implicit cloud-first override negates the new CLI. Worth flagging in the description so reviewers don't flip it back.

@bussyjd
bussyjd marked this pull request as ready for review April 27, 2026 12:46
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Self-update for the maintainer: I pushed a34442e on this branch — renames flow-12-agent-provider-smokes.shflow-14-agent-provider-smokes.sh (and the release-smoke.sh reference) so this PR doesn't collide with flow-12-obol-payment.sh once #386 lands. Pure rename, no functional change.

The cmd/obol/model.go cherry-pick conflict I described in the previous comment is not addressed here — it only manifests after #386 merges and replaces syncOpenClawModels() with syncAgentModels(). Resolving it now would force this PR to depend on #386 unnecessarily. When #386 lands, the merge here will be one line:

- return syncOpenClawModels(cfg, u)+ return syncAgentModels(cfg, u)

Both call sites (the setup and prefer commands) need the same trivial change.

The original PR moved the chosen model to the front of the LiteLLM
model_list and assumed downstream resolution would honor that order.
But on main, `rankModels` delegates to `model.Rank` for capability-aware
ranking — cloud > local, larger local > smaller local — which silently
overrode the user's pick on every stack restart, defaults refresh, or
auto-configure pass that re-read the model list.
This commit makes the preference load-bearing:
- `internal/model/preference.go` — persist the user's pick in
`$OBOL_CONFIG_DIR/preferred-model`. Survives ConfigMap rewrites,
unlike model_list[0] which any auto-config path can clobber.
- `internal/model/rank.go` — new `RankWithPreference(models, pref)`
that returns the preferred model as primary when present, falling
through to plain `Rank` when the preference is empty or stale.
Preserves provider-prefix tolerance so `claude-sonnet-4-6` matches
`anthropic/claude-sonnet-4-6`.
- `internal/openclaw/openclaw.go` and `internal/hermes/hermes.go` —
`rankModels` now takes cfg and consults the preference file; the
ConfigMap reorder in `PreferModel` is kept purely as a UX hint so
`obol model list` shows the preferred entry on top.
- `PreferModel` writes the preference file FIRST and patches the
ConfigMap second — the preference sticks even when the cluster is
down.
Tests:
- `TestRankWithPreference_*` — five cases covering no-preference,
explicit preference winning over capability, provider-prefix
tolerance, stale preference falling back, empty input.
- `TestPreferenceRoundTrip`, `TestPreferenceClear`,
`TestPreferenceSurvivesArbitraryFiles` — persistence contract.
- `TestRankModelsCapabilityWinsWithoutPreference`,
`TestRankModelsHonorsExplicitPreference`,
`TestRankModelsStalePreferenceFallsBackToCapability` — replace the
old `TestRankModelsHonorsConfiguredOrder`, which asserted the bug
itself (that rankModels returned models[0] regardless of capability).
The defaults-refresh path (autoConfigureLLM) is unaffected — it
patches the LiteLLM ConfigMap but never touches the preference file,
so the preference survives every restart and refresh.
Resolves the rank-overrides-prefer concern raised in #379 review.
@bussyjd
bussyjdforce-pushed the feature/agent-provider-smokes-model-prefer branch from a34442e to 6156195CompareApril 29, 2026 09:55
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Rebased onto current main + addressed the substantive design issue Oisin flagged.

What changed

Rebase: dropped a34442e (the rename to flow-14, which now collides with flow-14-live-obol-base-sepolia.sh on main). Kept the smokes flow at flows/flow-12-agent-provider-smokes.sh — number is open. Cherry-picked 0e7352f cleanly after fixing two collisions: cmd/obol/model.go now calls syncAgentModels (Hermes runtime) instead of syncOpenClawModels, and internal/openclaw/openclaw.go keeps mainʼs better model.Rank (capability-aware) instead of the old models[0] reduction.

The substantive fix (new commit on top: 6156195)

The original PR moved the chosen model to LiteLLM model_list[0] and assumed downstream resolution would honor that order. But on main, rankModels delegates to model.Rank for capability ranking (cloud > local, larger > smaller). Result: every stack restart, defaults refresh, or auto-configure pass silently overrode the userʼs pick. Thatʼs the rank-wins-over-prefer bug Oisin called out.

This commit makes the preference load-bearing:

  • internal/model/preference.go — persist the userʼs pick at $OBOL_CONFIG_DIR/preferred-model. Survives ConfigMap rewrites, unlike model_list[0] which any auto-config path can clobber.
  • internal/model/rank.go::RankWithPreference(models, pref) — preferred wins as primary; empty/stale preference falls through to plain Rank. Provider-prefix tolerant (claude-sonnet-4-6 matches anthropic/claude-sonnet-4-6).
  • internal/openclaw/openclaw.go + internal/hermes/hermes.gorankModels(cfg, models) consults the preference file. The ConfigMap reorder is kept purely as UX (so obol model list shows the preferred entry on top).
  • PreferModel writes the preference file first and patches the ConfigMap second; the preference sticks even when the cluster is down.

Regression coverage

Replaced the old TestRankModelsHonorsConfiguredOrder — that test asserted the bug itself (rank returns models[0] regardless of capability). New tests:

  • TestRankModelsCapabilityWinsWithoutPreference — no preference set → cloud beats local.
  • TestRankModelsHonorsExplicitPreference — preference set on disk → preferred model wins over Claude.
  • TestRankModelsStalePreferenceFallsBackToCapability — preference points at a no-longer-configured model → silently falls back to rank.
  • Plus 6 unit tests in internal/model/preference_test.go covering the persistence + RankWithPreference contract.

Validation

  • go build ./... — clean.
  • go test ./... -timeout 180s — 25 packages, 0 failures.
  • The autoConfigureLLM / defaults-refresh path (internal/stack/stack.go) is unaffected: it patches the LiteLLM ConfigMap but never touches the preference file, so the preference survives every restart and refresh.

Closes the rank-overrides-prefer concern. Tracked as task #38.

@OisinKyne

Copy link
Copy Markdown
Contributor

closing for now in favour of doing model control on the calling agent side rather than the litellm side

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add obol-agent provider inference smoke coverage and model preference command

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add provider smokes and model preference - #379

Closed
bussyjd wants to merge 2 commits into
mainfrom
feature/agent-provider-smokes-model-prefer
Closed

Add provider smokes and model preference#379
bussyjd wants to merge 2 commits into
mainfrom
feature/agent-provider-smokes-model-prefer

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

  • Add obol model prefer <model-name> to move a configured LiteLLM model to the front of the preference order and sync stack-managed agent model config.
  • Honor LiteLLM model order when choosing OpenClaw primary/fallback models, while keeping cloud setup primary by preferring the configured cloud model.
  • Add flow-12 provider smokes for local obol-agent inference plus optional Anthropic/OpenAI smokes when API keys are present.

Tests

  • bash -n flows/flow-12-agent-provider-smokes.sh flows/release-smoke.sh
  • go test ./cmd/obol ./internal/model ./internal/openclaw
  • go test ./... -run ^'$' -count=0

Closes#378

@bussyjd

Copy link
Copy Markdown
ContributorAuthor

This still makes sense — does not duplicate or conflict with #386 / #380 / #387. Two small adjustments needed before it can land alongside them; I've prepared a rebased branch with both changes ready: feature/agent-provider-smokes-model-prefer-rebased.

What changed in the rebase:

  1. Rename flow-12-agent-provider-smokes.shflow-14-agent-provider-smokes.sh.Integrate PR #377 (OBOL Permit2) + PR #381 (Hermes runtime), harden flows, add flow-13 dual-stack OBOL #386 (which integrates Integrate OBOL x402 payment rollout #377) ships flow-12-obol-payment.sh, so two different flow-12-*.sh files would coexist after merge. Rename keeps numbering monotonic (12 = OBOL payment, 13 = dual-stack OBOL, 14 = provider smokes). flows/release-smoke.sh and the README are updated to match.

  2. Resolve cmd/obol/model.go cherry-pick conflict.Integrate PR #377 (OBOL Permit2) + PR #381 (Hermes runtime), harden flows, add flow-13 dual-stack OBOL #386 introduced syncAgentModels() (Hermes default-agent path) replacing syncOpenClawModels(). The rebase keeps bothIntegrate PR #377 (OBOL Permit2) + PR #381 (Hermes runtime), harden flows, add flow-13 dual-stack OBOL #386's syncAgentModels() AND Add provider smokes and model preference #379's PreferModel() call — they're complementary: PreferModel pins the configured cloud model so it survives the subsequent sync.

Validation against the rebase tip:

go build ./... → clean
go test ./cmd/obol/... ./internal/model/... ./internal/openclaw/... \
./internal/tunnel/... ./internal/serviceoffercontroller/... \
./internal/stack/... ./internal/hermes/... ./internal/erc8004/...
→ all packages pass
bash -n flows/flow-14-agent-provider-smokes.sh → clean
bash -n flows/release-smoke.sh → clean

Recommended ship order: #387#380#386 → swap this PR's branch for the rebased one (or merge it as-is and close this). The rebased branch will continue to apply cleanly even if the order changes; git merge-tree reports zero conflict markers against any of the others.

Note on rankModels semantics: this PR's rewrite (LiteLLM-order ranking) is intentional and supersedes #386's cloud-bias version. That's the whole point of obol model prefer — without it, the implicit cloud-first override negates the new CLI. Worth flagging in the description so reviewers don't flip it back.

@bussyjd
bussyjd marked this pull request as ready for review April 27, 2026 12:46
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Self-update for the maintainer: I pushed a34442e on this branch — renames flow-12-agent-provider-smokes.shflow-14-agent-provider-smokes.sh (and the release-smoke.sh reference) so this PR doesn't collide with flow-12-obol-payment.sh once #386 lands. Pure rename, no functional change.

The cmd/obol/model.go cherry-pick conflict I described in the previous comment is not addressed here — it only manifests after #386 merges and replaces syncOpenClawModels() with syncAgentModels(). Resolving it now would force this PR to depend on #386 unnecessarily. When #386 lands, the merge here will be one line:

- return syncOpenClawModels(cfg, u)+ return syncAgentModels(cfg, u)

Both call sites (the setup and prefer commands) need the same trivial change.

The original PR moved the chosen model to the front of the LiteLLM
model_list and assumed downstream resolution would honor that order.
But on main, `rankModels` delegates to `model.Rank` for capability-aware
ranking — cloud > local, larger local > smaller local — which silently
overrode the user's pick on every stack restart, defaults refresh, or
auto-configure pass that re-read the model list.
This commit makes the preference load-bearing:
- `internal/model/preference.go` — persist the user's pick in
`$OBOL_CONFIG_DIR/preferred-model`. Survives ConfigMap rewrites,
unlike model_list[0] which any auto-config path can clobber.
- `internal/model/rank.go` — new `RankWithPreference(models, pref)`
that returns the preferred model as primary when present, falling
through to plain `Rank` when the preference is empty or stale.
Preserves provider-prefix tolerance so `claude-sonnet-4-6` matches
`anthropic/claude-sonnet-4-6`.
- `internal/openclaw/openclaw.go` and `internal/hermes/hermes.go` —
`rankModels` now takes cfg and consults the preference file; the
ConfigMap reorder in `PreferModel` is kept purely as a UX hint so
`obol model list` shows the preferred entry on top.
- `PreferModel` writes the preference file FIRST and patches the
ConfigMap second — the preference sticks even when the cluster is
down.
Tests:
- `TestRankWithPreference_*` — five cases covering no-preference,
explicit preference winning over capability, provider-prefix
tolerance, stale preference falling back, empty input.
- `TestPreferenceRoundTrip`, `TestPreferenceClear`,
`TestPreferenceSurvivesArbitraryFiles` — persistence contract.
- `TestRankModelsCapabilityWinsWithoutPreference`,
`TestRankModelsHonorsExplicitPreference`,
`TestRankModelsStalePreferenceFallsBackToCapability` — replace the
old `TestRankModelsHonorsConfiguredOrder`, which asserted the bug
itself (that rankModels returned models[0] regardless of capability).
The defaults-refresh path (autoConfigureLLM) is unaffected — it
patches the LiteLLM ConfigMap but never touches the preference file,
so the preference survives every restart and refresh.
Resolves the rank-overrides-prefer concern raised in #379 review.
@bussyjd
bussyjdforce-pushed the feature/agent-provider-smokes-model-prefer branch from a34442e to 6156195CompareApril 29, 2026 09:55
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Rebased onto current main + addressed the substantive design issue Oisin flagged.

What changed

Rebase: dropped a34442e (the rename to flow-14, which now collides with flow-14-live-obol-base-sepolia.sh on main). Kept the smokes flow at flows/flow-12-agent-provider-smokes.sh — number is open. Cherry-picked 0e7352f cleanly after fixing two collisions: cmd/obol/model.go now calls syncAgentModels (Hermes runtime) instead of syncOpenClawModels, and internal/openclaw/openclaw.go keeps mainʼs better model.Rank (capability-aware) instead of the old models[0] reduction.

The substantive fix (new commit on top: 6156195)

The original PR moved the chosen model to LiteLLM model_list[0] and assumed downstream resolution would honor that order. But on main, rankModels delegates to model.Rank for capability ranking (cloud > local, larger > smaller). Result: every stack restart, defaults refresh, or auto-configure pass silently overrode the userʼs pick. Thatʼs the rank-wins-over-prefer bug Oisin called out.

This commit makes the preference load-bearing:

  • internal/model/preference.go — persist the userʼs pick at $OBOL_CONFIG_DIR/preferred-model. Survives ConfigMap rewrites, unlike model_list[0] which any auto-config path can clobber.
  • internal/model/rank.go::RankWithPreference(models, pref) — preferred wins as primary; empty/stale preference falls through to plain Rank. Provider-prefix tolerant (claude-sonnet-4-6 matches anthropic/claude-sonnet-4-6).
  • internal/openclaw/openclaw.go + internal/hermes/hermes.gorankModels(cfg, models) consults the preference file. The ConfigMap reorder is kept purely as UX (so obol model list shows the preferred entry on top).
  • PreferModel writes the preference file first and patches the ConfigMap second; the preference sticks even when the cluster is down.

Regression coverage

Replaced the old TestRankModelsHonorsConfiguredOrder — that test asserted the bug itself (rank returns models[0] regardless of capability). New tests:

  • TestRankModelsCapabilityWinsWithoutPreference — no preference set → cloud beats local.
  • TestRankModelsHonorsExplicitPreference — preference set on disk → preferred model wins over Claude.
  • TestRankModelsStalePreferenceFallsBackToCapability — preference points at a no-longer-configured model → silently falls back to rank.
  • Plus 6 unit tests in internal/model/preference_test.go covering the persistence + RankWithPreference contract.

Validation

  • go build ./... — clean.
  • go test ./... -timeout 180s — 25 packages, 0 failures.
  • The autoConfigureLLM / defaults-refresh path (internal/stack/stack.go) is unaffected: it patches the LiteLLM ConfigMap but never touches the preference file, so the preference survives every restart and refresh.

Closes the rank-overrides-prefer concern. Tracked as task #38.

@OisinKyne

Copy link
Copy Markdown
Contributor

closing for now in favour of doing model control on the calling agent side rather than the litellm side

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add obol-agent provider inference smoke coverage and model preference command

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add provider smokes and model preference - #379

Closed
bussyjd wants to merge 2 commits into
mainfrom
feature/agent-provider-smokes-model-prefer
Closed

Add provider smokes and model preference#379
bussyjd wants to merge 2 commits into
mainfrom
feature/agent-provider-smokes-model-prefer

Conversation

@bussyjd

Copy link
Copy Markdown
Contributor

Summary

  • Add obol model prefer <model-name> to move a configured LiteLLM model to the front of the preference order and sync stack-managed agent model config.
  • Honor LiteLLM model order when choosing OpenClaw primary/fallback models, while keeping cloud setup primary by preferring the configured cloud model.
  • Add flow-12 provider smokes for local obol-agent inference plus optional Anthropic/OpenAI smokes when API keys are present.

Tests

  • bash -n flows/flow-12-agent-provider-smokes.sh flows/release-smoke.sh
  • go test ./cmd/obol ./internal/model ./internal/openclaw
  • go test ./... -run ^'$' -count=0

Closes#378

@bussyjd

Copy link
Copy Markdown
ContributorAuthor

This still makes sense — does not duplicate or conflict with #386 / #380 / #387. Two small adjustments needed before it can land alongside them; I've prepared a rebased branch with both changes ready: feature/agent-provider-smokes-model-prefer-rebased.

What changed in the rebase:

  1. Rename flow-12-agent-provider-smokes.shflow-14-agent-provider-smokes.sh.Integrate PR #377 (OBOL Permit2) + PR #381 (Hermes runtime), harden flows, add flow-13 dual-stack OBOL #386 (which integrates Integrate OBOL x402 payment rollout #377) ships flow-12-obol-payment.sh, so two different flow-12-*.sh files would coexist after merge. Rename keeps numbering monotonic (12 = OBOL payment, 13 = dual-stack OBOL, 14 = provider smokes). flows/release-smoke.sh and the README are updated to match.

  2. Resolve cmd/obol/model.go cherry-pick conflict.Integrate PR #377 (OBOL Permit2) + PR #381 (Hermes runtime), harden flows, add flow-13 dual-stack OBOL #386 introduced syncAgentModels() (Hermes default-agent path) replacing syncOpenClawModels(). The rebase keeps bothIntegrate PR #377 (OBOL Permit2) + PR #381 (Hermes runtime), harden flows, add flow-13 dual-stack OBOL #386's syncAgentModels() AND Add provider smokes and model preference #379's PreferModel() call — they're complementary: PreferModel pins the configured cloud model so it survives the subsequent sync.

Validation against the rebase tip:

go build ./... → clean
go test ./cmd/obol/... ./internal/model/... ./internal/openclaw/... \
./internal/tunnel/... ./internal/serviceoffercontroller/... \
./internal/stack/... ./internal/hermes/... ./internal/erc8004/...
→ all packages pass
bash -n flows/flow-14-agent-provider-smokes.sh → clean
bash -n flows/release-smoke.sh → clean

Recommended ship order: #387#380#386 → swap this PR's branch for the rebased one (or merge it as-is and close this). The rebased branch will continue to apply cleanly even if the order changes; git merge-tree reports zero conflict markers against any of the others.

Note on rankModels semantics: this PR's rewrite (LiteLLM-order ranking) is intentional and supersedes #386's cloud-bias version. That's the whole point of obol model prefer — without it, the implicit cloud-first override negates the new CLI. Worth flagging in the description so reviewers don't flip it back.

@bussyjd
bussyjd marked this pull request as ready for review April 27, 2026 12:46
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Self-update for the maintainer: I pushed a34442e on this branch — renames flow-12-agent-provider-smokes.shflow-14-agent-provider-smokes.sh (and the release-smoke.sh reference) so this PR doesn't collide with flow-12-obol-payment.sh once #386 lands. Pure rename, no functional change.

The cmd/obol/model.go cherry-pick conflict I described in the previous comment is not addressed here — it only manifests after #386 merges and replaces syncOpenClawModels() with syncAgentModels(). Resolving it now would force this PR to depend on #386 unnecessarily. When #386 lands, the merge here will be one line:

- return syncOpenClawModels(cfg, u)+ return syncAgentModels(cfg, u)

Both call sites (the setup and prefer commands) need the same trivial change.

The original PR moved the chosen model to the front of the LiteLLM
model_list and assumed downstream resolution would honor that order.
But on main, `rankModels` delegates to `model.Rank` for capability-aware
ranking — cloud > local, larger local > smaller local — which silently
overrode the user's pick on every stack restart, defaults refresh, or
auto-configure pass that re-read the model list.
This commit makes the preference load-bearing:
- `internal/model/preference.go` — persist the user's pick in
`$OBOL_CONFIG_DIR/preferred-model`. Survives ConfigMap rewrites,
unlike model_list[0] which any auto-config path can clobber.
- `internal/model/rank.go` — new `RankWithPreference(models, pref)`
that returns the preferred model as primary when present, falling
through to plain `Rank` when the preference is empty or stale.
Preserves provider-prefix tolerance so `claude-sonnet-4-6` matches
`anthropic/claude-sonnet-4-6`.
- `internal/openclaw/openclaw.go` and `internal/hermes/hermes.go` —
`rankModels` now takes cfg and consults the preference file; the
ConfigMap reorder in `PreferModel` is kept purely as a UX hint so
`obol model list` shows the preferred entry on top.
- `PreferModel` writes the preference file FIRST and patches the
ConfigMap second — the preference sticks even when the cluster is
down.
Tests:
- `TestRankWithPreference_*` — five cases covering no-preference,
explicit preference winning over capability, provider-prefix
tolerance, stale preference falling back, empty input.
- `TestPreferenceRoundTrip`, `TestPreferenceClear`,
`TestPreferenceSurvivesArbitraryFiles` — persistence contract.
- `TestRankModelsCapabilityWinsWithoutPreference`,
`TestRankModelsHonorsExplicitPreference`,
`TestRankModelsStalePreferenceFallsBackToCapability` — replace the
old `TestRankModelsHonorsConfiguredOrder`, which asserted the bug
itself (that rankModels returned models[0] regardless of capability).
The defaults-refresh path (autoConfigureLLM) is unaffected — it
patches the LiteLLM ConfigMap but never touches the preference file,
so the preference survives every restart and refresh.
Resolves the rank-overrides-prefer concern raised in #379 review.
@bussyjd
bussyjdforce-pushed the feature/agent-provider-smokes-model-prefer branch from a34442e to 6156195CompareApril 29, 2026 09:55
@bussyjd

Copy link
Copy Markdown
ContributorAuthor

Rebased onto current main + addressed the substantive design issue Oisin flagged.

What changed

Rebase: dropped a34442e (the rename to flow-14, which now collides with flow-14-live-obol-base-sepolia.sh on main). Kept the smokes flow at flows/flow-12-agent-provider-smokes.sh — number is open. Cherry-picked 0e7352f cleanly after fixing two collisions: cmd/obol/model.go now calls syncAgentModels (Hermes runtime) instead of syncOpenClawModels, and internal/openclaw/openclaw.go keeps mainʼs better model.Rank (capability-aware) instead of the old models[0] reduction.

The substantive fix (new commit on top: 6156195)

The original PR moved the chosen model to LiteLLM model_list[0] and assumed downstream resolution would honor that order. But on main, rankModels delegates to model.Rank for capability ranking (cloud > local, larger > smaller). Result: every stack restart, defaults refresh, or auto-configure pass silently overrode the userʼs pick. Thatʼs the rank-wins-over-prefer bug Oisin called out.

This commit makes the preference load-bearing:

  • internal/model/preference.go — persist the userʼs pick at $OBOL_CONFIG_DIR/preferred-model. Survives ConfigMap rewrites, unlike model_list[0] which any auto-config path can clobber.
  • internal/model/rank.go::RankWithPreference(models, pref) — preferred wins as primary; empty/stale preference falls through to plain Rank. Provider-prefix tolerant (claude-sonnet-4-6 matches anthropic/claude-sonnet-4-6).
  • internal/openclaw/openclaw.go + internal/hermes/hermes.gorankModels(cfg, models) consults the preference file. The ConfigMap reorder is kept purely as UX (so obol model list shows the preferred entry on top).
  • PreferModel writes the preference file first and patches the ConfigMap second; the preference sticks even when the cluster is down.

Regression coverage

Replaced the old TestRankModelsHonorsConfiguredOrder — that test asserted the bug itself (rank returns models[0] regardless of capability). New tests:

  • TestRankModelsCapabilityWinsWithoutPreference — no preference set → cloud beats local.
  • TestRankModelsHonorsExplicitPreference — preference set on disk → preferred model wins over Claude.
  • TestRankModelsStalePreferenceFallsBackToCapability — preference points at a no-longer-configured model → silently falls back to rank.
  • Plus 6 unit tests in internal/model/preference_test.go covering the persistence + RankWithPreference contract.

Validation

  • go build ./... — clean.
  • go test ./... -timeout 180s — 25 packages, 0 failures.
  • The autoConfigureLLM / defaults-refresh path (internal/stack/stack.go) is unaffected: it patches the LiteLLM ConfigMap but never touches the preference file, so the preference survives every restart and refresh.

Closes the rank-overrides-prefer concern. Tracked as task #38.

@OisinKyne

Copy link
Copy Markdown
Contributor

closing for now in favour of doing model control on the calling agent side rather than the litellm side

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add obol-agent provider inference smoke coverage and model preference command

2 participants

@bussyjd@OisinKyne