Skip to content

[feat] model_catalog: discover OpenCode free models live with cache + fallback (§55)#439

Merged
SaJaToGu merged 1 commit into
developfrom
fix/opencode-dynamic-models
Jun 25, 2026
Merged

[feat] model_catalog: discover OpenCode free models live with cache + fallback (§55)#439
SaJaToGu merged 1 commit into
developfrom
fix/opencode-dynamic-models

Conversation

@SaJaToGu

Copy link
Copy Markdown
Owner

Implements Backlog §55

The static OPENCODE_FREE_MODELS tuple in scripts/model_catalog.py was drifting against the live OpenCode registry: opencode/minimax-m3-free was no longer listed but still in the static fallback, and 4 new free models appeared in the registry that the static list did not know about:

  • opencode/gemini-3-flash
  • opencode/gpt-5.1-codex-mini
  • opencode/gpt-5.4-mini
  • opencode/north-mini-code-free

After this PR, the catalog is built from the live opencode models listing (cached for 1h), with the static list as a fallback when the binary is unavailable.

What changed in scripts/model_catalog.py

  • New fetch_opencode_free_models() calls opencode models, filters the line-delimited output for free-tier models (-free substring, plus an explicit exact-match list for slugs whose free status is implied by provider convention), and returns an OpencodeModelCache value object with fetched_at, models, and source (live | cache | fallback).
  • File-based cache at $XDG_CACHE_HOME/ai-issue-solver/opencode_models.json (or ~/.cache/...), TTL 1h. One cache file keeps the lookup deterministic across the same batch of Solver runs.
  • opencode binary resolution honours $OPENCODE_BIN (override) and falls back to ~/.opencode/bin/opencode (standard install). If neither is found, returns the static OPENCODE_FREE_MODELS list as fallback.
  • build_opencode_catalog() now takes a live_models parameter (defaults to calling fetch_opencode_free_models()).
  • OPENCODE_DEFAULT_MODEL is only applied as default_for when it is in the live list; otherwise no entry gets default_for, avoiding a silent 'first one wins' fallback.
  • The dead opencode/minimax-m3-free slug is removed from the static fallback list.
  • build_model_catalog() forwards an optional live_opencode_models parameter to build_opencode_catalog().

Tests

11 new tests + 1 regression guard in tests/test_model_catalog.py:

  • FetchOpencodeFreeModelsTests (7): live fetch + filter, binary missing, subprocess error, cache fresh, cache stale, use_cache=False override, pattern recognition.
  • test_opencode_live_only_models_show_up_when_discovered — ensures brand-new free models surface via the live path.
  • test_opencode_default_is_first_live_model_when_static_default_missing — no implicit default_for when static default is not in live list.
  • test_opencode_no_models_returns_empty_catalog — empty live list yields an empty opencode section.
  • test_dead_minimax_m3_free_is_not_in_static_fallback — regression guard for the dead slug removal.

17 unit tests in test_model_catalog green (was 6, +11 new). 15 unit tests in test_model_selection still green.

Files

  • scripts/model_catalog.py — +218/-8
  • tests/test_model_catalog.py — +197/0

Out of scope

  • Cross-provider free-model aggregation (e.g. minimax/MiniMax-M2.7 is also free in some configurations but lives under a different provider slug).
  • Auto-update of role_routing.yaml model: fields when the discovered free list changes.

@SaJaToGu SaJaToGu force-pushed the fix/opencode-dynamic-models branch from 04de2c5 to e7504b0 Compare June 25, 2026 15:05
…fallback

Implements Backlog §55. The static `OPENCODE_FREE_MODELS` tuple
in scripts/model_catalog.py was drifting against the live OpenCode
registry: `opencode/minimax-m3-free` is no longer listed but was
still in the static fallback, and 4 new free models
(`gemini-3-flash`, `gpt-5.1-codex-mini`, `gpt-5.4-mini`,
`north-mini-code-free`) appeared in the registry that the static
list did not know about.

## What changed in scripts/model_catalog.py

- New `fetch_opencode_free_models()` calls `opencode models`,
  filters the line-delimited output for free-tier models
  (`-free` substring, plus an explicit exact-match list for
  slugs whose free status is implied by provider convention), and
  returns an `OpencodeModelCache` value object with
  `fetched_at`, `models`, and `source` (`live` | `cache` |
  `fallback`).
- File-based cache at `$XDG_CACHE_HOME/ai-issue-solver/opencode_models.json`
  (or `~/.cache/...`), TTL 1h. One cache file keeps the lookup
  deterministic across the same batch of Solver runs.
- `opencode` binary resolution honours $OPENCODE_BIN (override)
  and falls back to `~/.opencode/bin/opencode` (standard
  install). If neither is found, returns the static
  `OPENCODE_FREE_MODELS` list as fallback.
- `build_opencode_catalog()` now takes a `live_models` parameter
  (defaults to calling `fetch_opencode_free_models()`).
- `OPENCODE_DEFAULT_MODEL` is only applied as `default_for` when
  it is in the live list; otherwise no entry gets `default_for`,
  avoiding a silent 'first one wins' fallback.
- The dead `opencode/minimax-m3-free` slug is removed from the
  static fallback list. If the live discovery ever fails AND this
  slug is still wanted, the regression test `test_dead_minimax_m3_free_is_not_in_static_fallback`
  will catch it.
- `build_model_catalog()` forwards an optional
  `live_opencode_models` parameter to `build_opencode_catalog()`.

## Tests

- 11 new tests in tests/test_model_catalog.py:
  - `FetchOpencodeFreeModelsTests` (7): live fetch + filter,
    binary missing, subprocess error, cache fresh, cache stale,
    use_cache=False override, pattern recognition (including the
    dead-slug regression comment).
  - `test_opencode_live_only_models_show_up_when_discovered` —
    ensures brand-new free models surface via the live path.
  - `test_opencode_default_is_first_live_model_when_static_default_missing` —
    no implicit default_for when static default is not in live
    list.
  - `test_opencode_no_models_returns_empty_catalog` — empty live
    list yields an empty opencode section.
  - `test_dead_minimax_m3_free_is_not_in_static_fallback` —
    regression guard for the dead slug removal.
- Existing `test_opencode_free_models_are_catalogued` updated to
  inject `live_opencode_models=OPENCODE_FREE_MODELS` for
  deterministic behaviour.

17 unit tests in test_model_catalog green (was 6, +11 new).
15 unit tests in test_model_selection still green.

## Out of scope (deliberately)

- Cross-provider free-model aggregation (e.g. `minimax/MiniMax-M2.7`
  is also free in some configurations but lives under a different
  provider slug). Defer to a follow-up if the Solver pipeline ever
  picks models across providers.
- Auto-update of role_routing.yaml `model:` fields when the
  discovered free list changes. Today the catalog is informational
  only; Solver still uses `--model-name` as configured.
@SaJaToGu SaJaToGu force-pushed the fix/opencode-dynamic-models branch from e7504b0 to a80ad28 Compare June 25, 2026 15:05
@SaJaToGu SaJaToGu merged commit fc62e46 into develop Jun 25, 2026
2 checks passed
@SaJaToGu SaJaToGu deleted the fix/opencode-dynamic-models branch June 25, 2026 16:19
SaJaToGu pushed a commit that referenced this pull request Jun 25, 2026
- open.md: §58 stubbed out as DONE-via-PR-#443 (squash 11eafc1).
  Notes that the docs/AGENTS.md Recently-Removed-Patterns list is
  now maintainer-pflegbar; future PRs that intentionally remove a
  pattern should add a row in the same PR. Cross-references the
  still-open potential §59 (patch-mismatch hardening).
- done.md: full §58 closure entry with problem (PR #441 episode),
  fix locations (docs/AGENTS.md, scripts/solve_issues.py,
  tests/test_solve_issues.py), the user-found path-leak fix from
  live review (now repo-relative when under PROJECT_ROOT, verbatim
  for explicit env-var override), verification matrix (168 OK +
  GitHub CI green + manual probe + user live review 'mergebereit'),
  and the maintainer obligation going forward.

Backlog is now EMPTY: §51/§52/§53/§54 done-via-PRs, §55 done-via-PR-#439,
§56/§57/§58 documented as done. The implicit §59 (patch-mismatch
hardening for the normal solve path) remains a possible future
item but is not filed.
SaJaToGu pushed a commit that referenced this pull request Jun 25, 2026
§59 = 'Watchlist: Patch-mismatch hardening for the normal solve
path'. Priority 4, labels kind/watchlist + theme/solver +
area/prompt.

Status: WATCHLIST ONLY — not an active backlog commitment. No code
or architecture work until the activation trigger (≥3 Mode-C
patch-mismatch runs on the normal solve path, ideally across
different files) is met.

Rationale:
- The Mode-C failure (worker produces a patch that git apply
  rejects because it does not match the current working tree) is
  the same patch-mismatch symptom §56 addressed for --rework-pr.
- Before §57, Mode-C on the normal solve path could produce
  regression-PRs (PR #441 vs. PR #439). After §57, Mode-C is a
  clean failure with no PR — acceptable behavior.
- A §59 fix would turn these clean failures into clean successes,
  but the architectural cost (prompt anchoring, per-file
  git apply --check, optional re-prompt loop) is only worth it
  if the failure pattern is systematic. One data point
  (Issue #389 re-run, scripts/model_selection.py:52, 1 patch,
  0 applied, no PR) is not enough to justify the work.
- When a Mode-C failure appears on the normal solve path, log
  the file + issue + date here so we can see when the threshold
  is met.

Non-goal while parked: no code changes. §57 + §58 are sufficient
to keep the pipeline correct.
SaJaToGu pushed a commit that referenced this pull request Jun 25, 2026
…ed-patterns guards

Extends PR #447 (liquid/lfm-2.5-1.2b-instruct:free initial fix that
hard-stopped cleanly after §57+§60, but covered only partial scope
of Issue #446). User reviewed and asked for two missing points:

1. **OpenCode Free Models (dynamisch)**: new block in README that
   explains scripts/model_catalog.py as the live source of truth
   for free-model discovery (with cache + static fallback,
   'missing' / 'stale' classification, --verify-opencode and
   --list-opencode-free-models CLI paths).

2. **Recently-Removed-Patterns-Guard**: new block that explains the
   docs/AGENTS.md 'Recently Removed Patterns' table the solve
   prompt now consults before re-introducing a recently-removed
   pattern. Lists the two current entries (static free_models
   removed by PR #439; $20/$20 cost-cap defaults replaced by
   $15/$50 + staggered warnings in PR #437). Notes that the
   guard is 'soft' (worker explains in the PR body if needed) and
   that maintainers update the table in every removal PR.

PR #447 also kept its earlier additions (Hard-Stop-Sicherheit
absatz + Backlog-Status-Update).

Closes #446 (now full scope).
Sign up for free to 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.

1 participant