Skip to content

fix(model-resolution): ensure subagents use free models when no paid providers configured (fixes #1883)#3529

Closed
MoerAI wants to merge 2 commits into
code-yeongyu:devfrom
MoerAI:fix/free-model-fallback-v2
Closed

fix(model-resolution): ensure subagents use free models when no paid providers configured (fixes #1883)#3529
MoerAI wants to merge 2 commits into
code-yeongyu:devfrom
MoerAI:fix/free-model-fallback-v2

Conversation

@MoerAI
Copy link
Copy Markdown
Contributor

@MoerAI MoerAI commented Apr 20, 2026

Summary

  • keep delegate-task subagents on free opencode/* models when only free providers are configured

Problem

Subagent model resolution could still select paid opencode/* entries from hardcoded category and agent fallback chains when the user had no paid providers configured. This let child agents resolve to models like gpt-5.4 instead of staying on free models, especially when the connected-provider cache only exposed opencode.

Fix

Free-only provider setups now strip paid defaults out of delegate-task category and fallback resolution and replace them with a stable free-model chain. The parsing helpers were extracted into focused modules, and targeted regression tests now cover both cached and cold-cache free-only resolution paths.

Changes

File Change
src/tools/delegate-task/model-selection.ts Apply free-only fallback logic during delegate-task model resolution
src/tools/delegate-task/free-model-fallback.ts Add free-only provider detection and fallback chain helpers
src/tools/delegate-task/model-selection-input.ts Extract model parsing helpers from the resolver
src/tools/delegate-task/model-selection.free-model-fallback.test.ts Add regressions for free-only subagent fallback behavior

Fixes #1883


Summary by cubic

Keeps delegate-task subagents on free opencode/* models when only free providers are connected by stripping paid defaults/fallbacks and using an updated, non-deprecated free chain.

  • Bug Fixes

    • Filter paid entries from category defaults and fallbacks in free-only setups; apply free chain: big-pickleminimax-m2.5-freehy3-preview-freenemotron-3-super-freegpt-5-nano.
    • Preserve explicit user-configured category models.
    • Add regression tests to ensure the chain excludes deprecated models and only contains known free models, and to verify free-only fallback behavior.
  • Refactors

    • Extract free-only logic to src/tools/delegate-task/free-model-fallback.ts.
    • Move model parsing helpers to src/tools/delegate-task/model-selection-input.ts.

Written for commit 49a1540. Summary will update on new commits. Review in cubic

@MoerAI
Copy link
Copy Markdown
Contributor Author

MoerAI commented Apr 20, 2026

I have read the CLA Document and I hereby sign the CLA

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 4 files

Confidence score: 3/5

  • There is a concrete regression risk in src/tools/delegate-task/model-selection.ts: deriving free-only mode from a potentially stale connected-providers.json can rewrite a valid paid resolution, which may cause incorrect model selection behavior for users.
  • src/tools/delegate-task/free-model-fallback.ts appears to keep the filtered single-model chain (for example gpt-5-nano) instead of the new stable free-only chain, so free-only resolution can still fail even when another free opencode model is available.
  • Given both issues are medium severity (6/10) with fairly high confidence (8/10) and affect runtime resolution paths, this looks like moderate merge risk rather than a safe-to-merge state.
  • Pay close attention to src/tools/delegate-task/model-selection.ts and src/tools/delegate-task/free-model-fallback.ts - free-only/paid resolution logic may select the wrong chain or fail fallback selection.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/tools/delegate-task/model-selection.ts">

<violation number="1" location="src/tools/delegate-task/model-selection.ts:37">
P2: Free-only mode is derived from the wrong cache source here. If `connected-providers.json` is stale but `availableModels` came from a newer provider-models cache, this can incorrectly rewrite a valid paid resolution to the free opencode fallback chain.</violation>
</file>

<file name="src/tools/delegate-task/free-model-fallback.ts">

<violation number="1" location="src/tools/delegate-task/free-model-fallback.ts:60">
P2: Returning the filtered chain here keeps single-model free fallbacks like `gpt-5-nano` and skips the new stable free-only chain, so free-only resolution can still fail when another free opencode model is available.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.


const connectedProviders = input.availableModels.size === 0 ? readConnectedProvidersCache() : null
const connectedProviders = readConnectedProvidersCache()
const freeOnlyProviderConfiguration = isFreeOnlyProviderConfiguration(connectedProviders)
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Free-only mode is derived from the wrong cache source here. If connected-providers.json is stale but availableModels came from a newer provider-models cache, this can incorrectly rewrite a valid paid resolution to the free opencode fallback chain.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/tools/delegate-task/model-selection.ts, line 37:

<comment>Free-only mode is derived from the wrong cache source here. If `connected-providers.json` is stale but `availableModels` came from a newer provider-models cache, this can incorrectly rewrite a valid paid resolution to the free opencode fallback chain.</comment>

<file context>
@@ -63,15 +33,18 @@ export function resolveModelForDelegateTask(input: {
 
-  const connectedProviders = input.availableModels.size === 0 ? readConnectedProvidersCache() : null
+  const connectedProviders = readConnectedProvidersCache()
+  const freeOnlyProviderConfiguration = isFreeOnlyProviderConfiguration(connectedProviders)
 
-  // Before provider cache is created (first run), skip model resolution entirely.
</file context>
Suggested change
const freeOnlyProviderConfiguration = isFreeOnlyProviderConfiguration(connectedProviders)
const providersForFreeOnlyDetection = input.availableModels.size > 0
? [...new Set([...input.availableModels].map((model) => model.split("/")[0]))]
: connectedProviders
const freeOnlyProviderConfiguration = isFreeOnlyProviderConfiguration(providersForFreeOnlyDetection)
Fix with Cubic

entry.providers.includes("opencode") && isKnownFreeModel(entry.model),
)

return freeEntries.length > 0 ? freeEntries : FREE_ONLY_FALLBACK_CHAIN
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Returning the filtered chain here keeps single-model free fallbacks like gpt-5-nano and skips the new stable free-only chain, so free-only resolution can still fail when another free opencode model is available.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/tools/delegate-task/free-model-fallback.ts, line 60:

<comment>Returning the filtered chain here keeps single-model free fallbacks like `gpt-5-nano` and skips the new stable free-only chain, so free-only resolution can still fail when another free opencode model is available.</comment>

<file context>
@@ -0,0 +1,61 @@
+    entry.providers.includes("opencode") && isKnownFreeModel(entry.model),
+  )
+
+  return freeEntries.length > 0 ? freeEntries : FREE_ONLY_FALLBACK_CHAIN
+}
</file context>
Fix with Cubic

@mrosnerr
Copy link
Copy Markdown
Contributor

mrosnerr commented Apr 29, 2026

hi - thanks for taking this on!

immediate thing: the new KNOWN_FREE_MODEL_IDS includes kimi-k2.5-free, which isn't on opencode anymore.

$ curl -s https://opencode.ai/zen/v1/models | jq -r '.data[].id' | grep kimi-k2.5-free
(empty)

$ curl -s https://models.dev/api.json | jq -r '.opencode.models[] | select(.id == "kimi-k2.5-free") | .status'
deprecated

so the new hardcoded list is shipping the same shape of artifact that broke the original issue.

separate thing: FREE_ONLY_PROVIDER_IDS = ["opencode"] reads "only opencode connected" as "free-only setup", but per cbrunnkvist's repro in #1883, both "no auth at all" and "authenticated with zen api key" produce connectedProviders === ["opencode"]. so paying subscribers would get force-downgraded by this. availableModels is the input that already knows which models are actually reachable.

+1 on both of cubic's findings.

bigger picture - hardcoded model names anywhere in source go stale on opencode's rotation cadence. wommy's jq from the original thread gives the current canonical set:

$ curl -s https://models.dev/api.json | jq -r '[.opencode.models[] | select(.status != "deprecated" and .cost.input == 0) | .id]'
[
  "big-pickle",
  "gpt-5-nano",
  "hy3-preview-free",
  "minimax-m2.5-free",
  "nemotron-3-super-free"
]

two of those (hy3-preview-free, nemotron-3-super-free) are live free models this PR's chain can't reach. if the chain came from a json snapshot rather than source literals, this drift becomes a json edit instead of a release. happy to help shape a snapshot + refresh-workflow if useful.

…fallback chain

Per code-yeongyu#3529 (comment) review and the live opencode catalog (https://opencode.ai/zen/v1/models, https://models.dev/api.json), kimi-k2.5-free is now marked deprecated and no longer resolves. Shipping it in FREE_ONLY_FALLBACK_CHAIN reproduces the same shape of artifact that triggered code-yeongyu#1883.

Replaces it with the currently live free opencode models that are non-deprecated and have cost.input == 0: hy3-preview-free, nemotron-3-super-free (plus the existing big-pickle, minimax-m2.5-free, gpt-5-nano). Keeps the chain ordered by capability (multimodal -> general).

Adds a regression test that asserts FREE_ONLY_FALLBACK_CHAIN never contains models in a known-deprecated list and that every entry passes isKnownFreeModel, so future drift fails CI instead of silently shipping a stale model.
@MoerAI
Copy link
Copy Markdown
Contributor Author

MoerAI commented Apr 30, 2026

@mrosnerr thanks for the careful read.

Concern 1 — kimi-k2.5-free is deprecated

Confirmed against both https://opencode.ai/zen/v1/models and the models.dev catalog (where it now reports status: "deprecated"). Just pushed 49a1540 which:

  • removes kimi-k2.5-free from KNOWN_FREE_MODEL_IDS and FREE_ONLY_FALLBACK_CHAIN
  • adds the currently live free opencode models (hy3-preview-free, nemotron-3-super-free) so the chain doesn't lose coverage
  • adds a regression test that asserts the chain never contains a known-deprecated model and that every entry passes isKnownFreeModel, so future drift fails CI instead of silently shipping a stale model

After fix the chain is: big-pickle -> minimax-m2.5-free -> hy3-preview-free -> nemotron-3-super-free -> gpt-5-nano.

Concern 2 — FREE_ONLY_PROVIDER_IDS = ["opencode"] is too broad

You're right that paid Zen subscribers also see connectedProviders === ["opencode"], so the "free-only" gate as written can downgrade a paying user. I deliberately kept this PR scoped to the deprecated-model fix because rewiring the detection (use availableModels as the source of truth) touches the resolver in a deeper way and changes the shape of all 3 existing tests in this file. I'd rather land the no-regret fix here, then open a follow-up that:

  1. drops FREE_ONLY_PROVIDER_IDS entirely
  2. derives "free-only" from availableModels.every(m => isKnownFreeModel(m)) (or the inverse: any paid model present -> not free-only)
  3. moves the canonical free-model list to a snapshot file refreshed on the same cadence as refresh-model-capabilities.yml (your bigger-picture suggestion)

Happy to take that follow-up if @code-yeongyu agrees with the direction. Want me to open a tracking issue?

Also +1 on cubic's findings — the current heuristic is doing two jobs (gating + chain construction) that should be separated.

@mrosnerr
Copy link
Copy Markdown
Contributor

Opened a PR against this branch with some test additions and a simplified approach to the free-only detection: MoerAI#1

The main concern we found: isFreeOnlyProviderConfiguration(["opencode"]) returns true for both free-tier and paid Zen subscribers, since both produce connectedProviders === ["opencode"]. This causes paid users to get force-downgraded to free models on the warm-cache path.

Our approach replaces the category-default gating and chain replacement with an append strategy — free models get added as a tail to the existing chain, and on warm cache, free-only is derived from availableModels instead of connectedProviders. The cold-cache path still uses provider-based detection as a fallback (noted with a TODO).

Feel free to merge it in, cherry-pick what's useful, or just use it as context for the edge cases — whatever works best for getting the right behavior out.

@MoerAI
Copy link
Copy Markdown
Contributor Author

MoerAI commented May 6, 2026

Closing — @mrosnerr identified a real bug in my approach: isFreeOnlyProviderConfiguration(["opencode"]) returns true for both free-tier and paid Zen subscribers, force-downgrading paid users. They opened MoerAI#1 against this branch with a corrected approach (derive free-only from availableModels instead of connectedProviders, append free models as a tail to the chain rather than replace).

Their approach is the right one — I'd rather they take this forward as their own PR against upstream/dev so the credit goes to them. Their PR description is also a much better explanation of the edge cases than mine. @mrosnerr if you want to open a PR against code-yeongyu/oh-my-openagent directly, please go ahead.

Thanks for the deep review @mrosnerr.

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.

Bug: Should use only free models when no providers configured

2 participants