Skip to content

web: config-declared models are hidden by default — released:0 is coerced to "1970-01-01" and fails the client visibility window #49605

Closed

Description

@sergiopadure

Summary

Custom, config-declared models (a provider entry whose models live only in config, with no models.dev catalog entry) are toggled hidden by default in every fresh client of the web/desktop app: the server reports time.released: 0 ("unknown"), the client normalizes that into the valid date "1970-01-01", and the default-visibility rule (visible() in the models context) then treats the model as 56 years old and hides it. Because visibility preferences are persisted only in client-local storage (browser localStorage / desktop storage), nothing on the server remembers the user's "show" toggle — every new browser profile, cleared storage, or another machine starts with these models hidden again.

Environment

  • opencode version: 2.0.6 (opencode serve --service, channel latest; same code path verified on 2.0.4 from the served bundle)
  • OS: Linux x86_64 (server host)
  • Terminal: N/A — defect is in the web/desktop app served by opencode serve
  • Shell: /bin/bash (server host)
  • Install/channel: npm, latest
  • Active plugins: none needed; reproduces with a single custom-provider config entry

Reproduction

  1. Add a custom provider whose models exist only in configuration (no models.dev match):
    {
      "$schema": "https://opencode.ai/config.json",
      "providers": {
        "acme-ai": {
          "name": "Acme AI",
          "package": "@opencode/ai/providers/openai-compatible",
          "settings": { "baseURL": "https://api.example.com/v1" },
          "models": {
            "acme-model-1": { "name": "Acme Model 1" }
          }
        }
      }
    }
  2. (Re)start the server. In GET /api/model, acme-ai/acme-model-1 is returned with "enabled": true but "time": { "released": 0 }.
  3. Open the web app in a fresh browser profile (or incognito window).
  4. Settings → Models: "Acme Model 1" is toggled OFF ("hidden") despite there being no stored preference. The model picker does not list it either.
  5. Toggle it ON — it becomes usable in that browser.
  6. Clear site data (or open another browser/machine). The model is hidden again.

Expected Behavior

Models whose release date is unknown should be visible by default — the client fallback even encodes that intent (!date.isValid → visible). Alternatively, configuration should let me set a model's release date so catalog-less models can be dated.

Actual Behavior

Every config-declared model is hidden by default in every fresh client. The only toggle lives in per-browser/per-app storage, so the setting appears to "reset" whenever client state is fresh or lost.

Root cause (verified on served v2.0.6 bundle + live /api/model output)

  1. Server: models without a catalog entry are emitted with "time": { "released": 0 } (epoch 0 = unknown).
  2. Client normalization coerces that epoch-0 into a real date string (in the served runtime chunk):
    release_date: new Date(e.time.released).toISOString().slice(0, 10)  // -> "1970-01-01"
  3. Default visibility rule (models context), minimally de-minified:
    visible(model) = stored !== "hide" &&
                     (stored === "show" || latestSet.has(key) || !releaseDate.isValid)
    where latestSet = models whose parsed release date is within the last 6 months. "1970-01-01" parses as a valid Luxon date, so the !isValid escape never fires, and the 6-month window fails → hidden.
  4. Storage: visible overrides are kept via Persist.global("model", ...) targeting client-local storage only; none of it is posted back to the server. Selecting a model also writes setVisibility(key, true), which is why "my main models stay visible in the browser I usually use" while other clients reset.
  5. Config cannot override: the native V2 config model schema offers exactly family, name, api, capabilities, request, variants, cost, disabled, limit — no date field. The V1 schema's models.<id>.release_date exists but is dropped silently by migrateModel() in the V1→V2 compatibility layer.

Suggested fix

Either of:

  • Treat released === 0 as unknown when building the client catalog entry (leave release_date unset/null), so the existing !isValid fallback defaults them to visible; and/or
  • expose a per-model release-date field in config (or preserve V1 release_date through migration).

Separately, persisting visibility preferences per-server (not per-browser) would fix the "loses toggles" half of the experience — tracked by #21310 (closed as not planned).

Additional Context

Activity

  1. github-actions commented on Sep 17, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

  2. sergiopadure commented on Sep 17, 2026

    @sergiopadure
    ContributorAuthor

    Note on the duplicate-detector link to #47557: related, but not a duplicate.

    Both defects funnel through the same visible()/latestSet logic in the models context, but they feed it differently:

    Two consequences:

    1. The family-override workaround documented in Web/Desktop model picker permanently hides catalog models that lack a family field (e.g. opencode-go/omen-alpha) #47557 does not help here — config-declared models are filtered out by the date coercion regardless of their family.
    2. A config-declared model that is also family-less is double-hidden, so the fixes compose rather than overlap.

    Suggesting one shared repair zone, though: making the visible() fallback handle "no meaningful date" (treat released === 0 / missing as unknowable → visible) addresses this issue, and keeping undefined-family models in the latest grouping addresses #47557. A config-level release date for custom models would fix this issue's class independently of either.

  3. added a commit that references this issue on Sep 17, 2026
    9185309
  4. hejackson2 commented on Sep 27, 2026

    @hejackson2

    Confirmed on OpenCode Desktop 2.0.18 on macOS (Darwin arm64).

    I configured a custom OpenAI-compatible provider in
    ~/.config/opencode/opencode.jsonc:

    • provider ID: mac-mini-01
    • package: @opencode/ai/providers/openai-compatible
    • local endpoint: http://localhost:11450/v1
    • configured models:
      • antares-1b-q8
      • gemma3-12b
      • qwen2.5-coder-7b
      • qwen3-8b

    The Desktop /model picker does not show the provider or any of these
    models. The CLI does show them.

    The shared service is healthy and returns all four in /api/model with
    enabled: true; two (antares-1b-q8 and qwen3-8b) also report
    capabilities.tools: true. Both the Desktop app and service are version
    2.0.18. Restarting Desktop and running opencode service restart did not
    change the result.

    Environment:

    • OpenCode Desktop/service: 2.0.18, latest channel
    • OS: macOS Darwin 27.0.0, arm64
    • Shell: /opt/homebrew/bin/bash
    • Plugins: none
  5. opencodejr commented on Oct 28, 2026

    @opencodejr

    Thanks for the detailed analysis. I could reproduce this on 2.0.6, and it still happens on the latest dev (950111ff2).

    What I did:

    • Created a project whose config has only the acme-ai provider with acme-model-1 (no models.dev entry), then ran opencode serve.
    • GET /api/model returns the model as active with time: {"released": 0}.
    • Passed the live /api/provider and /api/model responses through the app's own normalizeProviderList. The model's release_date comes out as "1970-01-01".
    • With no stored preference, the default-visibility rule in models.tsx then returns false. The "no parseable release date stays visible" fallback never applies, and the model is outside the ~6-month "latest" window, so it's hidden by default.

    The script below reproduces this. I couldn't open the web UI itself, since there's no browser in my sandbox, so I copied the visibility rule into the script.

    Possible workaround on current dev (I haven't tested it, and it isn't in 2.0.6): model config there accepts releaseDate (e.g. "releaseDate": "2026-10-01"), so a config-only model can be given a date.

    Related:

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Repro for #49605: a config-only model is hidden by default in the web app.
    # Starts `opencode serve` (from PATH, or from the checkout in $OC_DEV) on a project with one
    # config-declared model, then runs the app's own normalizeProviderList (from $REPO/packages/app)
    # on the live /api/provider + /api/model and applies the default-visibility rule of
    # packages/app/src/providers/models/models.tsx (no stored preference).
    set -euo pipefail
    REPO=${OC_DEV:-${REPO:-/workspace/repo}}
    WORK=$(mktemp -d); export HOME=$WORK/home XDG_CONFIG_HOME=$WORK/home/.config XDG_DATA_HOME=$WORK/home/.data XDG_CACHE_HOME=$WORK/home/.cache XDG_STATE_HOME=$WORK/home/.state
    mkdir -p "$HOME" "$WORK/proj"
    cat > "$WORK/proj/opencode.json" <<'JSON'
    { "$schema": "https://opencode.ai/config.json",
      "providers": { "acme-ai": { "name": "Acme AI", "package": "@opencode/ai/providers/openai-compatible",
        "settings": { "baseURL": "https://api.example.com/v1", "apiKey": "dummy" },
        "models": { "acme-model-1": { "name": "Acme Model 1" } } } } }
    JSON
    cd "$WORK/proj"
    if [ -n "${OC_DEV:-}" ]; then
      bun --conditions=browser --preload="$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js" "$OC_DEV/packages/cli/src/index.ts" serve --port 4610 > "$WORK/serve.log" 2>&1 &
    else
      opencode serve --port 4610 > "$WORK/serve.log" 2>&1 &
    fi
    SERVER=$!; trap 'kill $SERVER 2>/dev/null' EXIT
    for i in $(seq 60); do grep -q "server password" "$WORK/serve.log" && break; sleep 1; done
    PASS=$(sed -n 's/^server password //p' "$WORK/serve.log"); sleep 5
    cat > "$REPO/packages/app/repro-49605.ts" <<'TS'
    // Feeds a live server's /api/provider + /api/model into the app's own normalizeProviderList,
    // then applies the default-visibility rule of providers/models/models.tsx (no stored preference).
    import { normalizeProviderList } from "@/runtime/server/global-sync/utils"
    const base = process.env.URL!, dir = process.env.DIR!, auth = "Basic " + btoa("opencode:" + process.env.PASS)
    const get = (p: string) => fetch(`${base}${p}?directory=${encodeURIComponent(dir)}`, { headers: { authorization: auth } }).then((r) => r.json()).then((r: any) => r.data)
    let providers: any, models: any = []
    for (let i = 0; i < 60 && !models.some((m: any) => m.providerID === "acme-ai"); i++) {
      ;[providers, models] = await Promise.all([get("/api/provider"), get("/api/model")])
      if (!models.some((m: any) => m.providerID === "acme-ai")) await Bun.sleep(1000)
    }
    const raw = models.find((m: any) => m.providerID === "acme-ai")
    console.log("server time:", JSON.stringify(raw.time))
    const catalog = normalizeProviderList(providers, models)
    const available = [...catalog.all.values()].flatMap((p: any) => Object.values(p.models).map((m: any) => ({ ...m, provider: p })))
    const sixMonths = 6 * 30.436875 * 24 * 60 * 60 * 1000
    const release = new Map(available.map((m: any) => [`${m.provider.id}:${m.id}`, Date.parse(m.release_date)]))
    // models.tsx: only models released within ~6 months can enter latestSet (latestModels then keeps the newest per family)
    const recent = (key: string) => Math.abs(Date.now() - (release.get(key) ?? NaN)) < sixMonths
    // models.tsx visible(): stored pref, else latestSet, else "Models without a parseable release date stay visible."
    const visible = (key: string) => recent(key) || !Number.isFinite(release.get(key) ?? NaN)
    const m = available.find((x: any) => x.provider.id === "acme-ai")!
    console.log("client release_date:", JSON.stringify(m.release_date))
    console.log("default visible (no stored pref):", visible("acme-ai:acme-model-1"))
    TS
    cd "$REPO/packages/app" && URL=http://localhost:4610 DIR="$WORK/proj" PASS=$PASS bun run repro-49605.ts
    rm -f "$REPO/packages/app/repro-49605.ts"

    Run it: bash repro.sh

  6. sergiopadure commented on Oct 28, 2026

    @sergiopadure
    ContributorAuthor

    Confirmed the same chain on the current release channel. One correction worth adding for anyone landing here on a recent build: the releaseDate knob is not dev-only — it ships in v2.2.0.

    In the shipped v2.2.0 build:

    • Config.Model accepts releaseDate, validated as YYYY-MM-DD (or YYYY-MM), optional;
    • the model builder maps it into the emitted entry: if (c.releaseDate !== undefined) g.time = { released: Date.parse(c.releaseDate) }, i.e. the config value flows straight into time.released, replacing the forced epoch 0.

    So on v2.2.0+, a config-declared model can be made default-visible by dating it in config:

    "models": {
      "acme-model-1": { "name": "Acme Model 1", "releaseDate": "2026-10-01" }
    }

    That's a workable per-model workaround for this issue's class. A default-visible change for undated models (treat released === 0 as unknown, per the issue body's suggested fix, or preserving V1 release_date through migration) would still be the cleaner resolution.

  7. sergiopadure commented on Oct 28, 2026

    @sergiopadure
    ContributorAuthor

    A correction to my comment above, and a request.

    Correction. On v2.2.0, releaseDate alone does not make every config-declared model visible by default. The
    picker's default rule (packages/app/src/providers/models/models.tsx) has two more conditions:

    Making a whole custom provider visible therefore needs a distinct made-up family per model and a release date that
    someone keeps moving forward. The V1 provider.<id>.models.<id>.release_date field is still dropped (it is listed in
    unsupportedModel in packages/core/src/config/normalize.ts), so V1 configs have to move to providers first. For a
    large configuration, that is not a practical workaround.

    Request. Could there be a supported way to make all models visible by default, without per-browser toggles? Any of
    these would do:

    1. Treat time.released === 0 as unknown when building the client entry (no release_date), so config-declared
      models fall under the existing "no parseable release date stays visible" fallback (models.tsx lines 127-128).
      This is the fix suggested in the issue body.
    2. A config option that sets default visibility, per model or per provider (for example "visible": true), read by
      the picker before the date and family rules.
    3. An app setting such as "show all models" that is stored on the server rather than in browser storage (related:
      Model selections in the Model Choice tab reset after restarting the web app #21310).
  8. joe-ieta commented on Oct 30, 2026

    @joe-ieta
    Contributor

    Confirmed on Windows V2 desktop and the current dev catalog adapter. The service loads the global JSONC provider/model successfully and emits released: 0; the app adapter turns it into 1970-01-01 and the default filter hides it.

    I have a focused two-file fix at joe-ieta/opencode@anomalyco:opencode:dev...joe-ieta:opencode:model-release-date . It preserves the zero sentinel as an empty date, using the existing unknown-date visibility fallback. The updated upstream adapter tests fail before the fix (14 pass / 1 fail) and pass afterward (15 tests, repeated 10 times). Production UI checks also confirm default visibility, explicit hide/show and unchanged filtering for old dated models.

    The Windows follow-up report is #60479; this issue remains the original root-cause report. The focused fix is now submitted as #60496. Please classify the follow-up report as a bug if useful; my account cannot add repository labels.

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions