Skip to content
This repository was archived by the owner on Nov 7, 2026. It is now read-only.

fix(core): load env-supplied config in V2 config - #347

Merged
brentyates-swx merged 3 commits into
swxtchfrom
fm/opencode-v2-provider-1952
Oct 23, 2026
Merged

brentyates-swx merged 3 commits into
swxtchfrom
fm/opencode-v2-provider-1952

Conversation

@brentyates-swx

@brentyates-swx brentyates-swx commented Oct 23, 2026 •

Copy link
Copy Markdown

Refs swxtchio/swx-abbe#1952

Root cause

The V2 core Config layer (packages/core/src/config.ts) read only the global config directory and discovered project files. It ignored OPENCODE_CONFIG and OPENCODE_CONFIG_CONTENT. The OpenCode jail supplies its whole config through those two variables: Dockerfile.jail sets OPENCODE_CONFIG=/jail/opencode.json, and opencode_server.py::configure_runtime_config renders the proxy URL and capability into OPENCODE_CONFIG_CONTENT. So the V2 catalog never held swxtch-egress or a configured model, and SessionRunnerModel.locationLayer failed a named egress model with ModelUnavailableError or picked another provider. V1 (packages/opencode/src/config/config.ts) reads both variables, which is why V1 turns reached the egress provider.

Change

  • Config takes explicit file and content sources, which default to Flag.OPENCODE_CONFIG and Flag.OPENCODE_CONFIG_CONTENT. Config.nodeWith(sources) is the test seam, in the same shape as Global.layerWith.
  • Precedence matches V1 and upstream 43c08387f1: global directory, then the explicit file, then project files, then .opencode directories, with inline content last. In the jail, the rendered content therefore overrides the file's {env:…} placeholders. V2 has no {env:} substitution, and it doesn't need one here.
  • V1-format sources migrate through the existing loader path (ConfigMigrateV1).

Tests (red on the reverted loader)

  • test/config/config.test.ts: checks the order of every source and that inline content wins.
  • test/config/environment.test.ts: runs the jail's own V1 config (the placeholder file plus the rendered content) through the real loader, the config provider plugin, the catalog and SessionRunnerModel.locationLayer:
    • A V2 session that names swxtch-egress/accounts/fireworks/models/deepseek-v4p1-flash resolves to the rendered proxy baseURL with Bearer <capability>. On the reverted loader this test fails with the issue's ModelUnavailableError.
    • A session that names no model, with model configured, selects egress over a competing global provider that would otherwise sort first. On the reverted loader it picks the other provider.

Review round 1: default for a session that names no model

On the env-config fix alone, a location-only V2 session in the jail still resolved a credential-free catalog model (opencode/hy3-free against the recorded models.dev corpus), because Catalog.model.default() fell back to the newest available model. V1's Provider.defaultModel instead takes its default from the config-declared providers when no model is configured. V2 now does the same:

  • ConfigProviderPlugin records the config-declared providers, in config order, through a new internal Catalog draft hook, model.default.prefer. The public plugin catalog API has no such hook, so the plugin registers the preference on the internal Catalog.Service under its own plugin scope.
  • Catalog.model.default() takes the newest available model of the first preferred provider that has one, and otherwise falls back to the newest available model. An explicit session model or a configured model still takes precedence.

Tests (red without the preference):

  • test/location-layer.test.ts runs the actual jail path: the production location runtime with every internal plugin, the recorded models.dev corpus (packages/opencode/test/tool/fixtures/models-api.json), and the jail config supplied through OPENCODE_CONFIG and OPENCODE_CONFIG_CONTENT. A location-only session resolves to swxtch-egress/accounts/fireworks/models/deepseek-v4p1-flash at the rendered proxy. Without the preference it resolves opencode/hy3-free when no provider credentials are set, or the newest credentialed model when some are.
  • test/config/provider.test.ts: the preference skips a declared provider that has no credentials and beats a newer model from an undeclared provider.

Abbe side

No producer change is required. With this PR, a location-only create_session_v2 session in the jail resolves to the egress model. B-16.7 (anomalyco#1792), which sends the model on create, also resolves now.

🤖 Generated with Claude Code

)

Plan for swxtchio/swx-abbe#1952.

Root cause: the V2 Config layer (packages/core/src/config.ts) reads only
the global config directory and discovered project files. It ignores
OPENCODE_CONFIG and OPENCODE_CONFIG_CONTENT. The jail supplies its whole
config through those two variables (Dockerfile.jail sets OPENCODE_CONFIG,
opencode_server.py renders OPENCODE_CONFIG_CONTENT), so the V2 catalog
never sees the swxtch-egress provider or a configured default model.
V1 (packages/opencode/src/config/config.ts) reads both, which is why V1
turns run on the egress provider.

Change:
- Config takes explicit file and content sources, defaulting to
  Flag.OPENCODE_CONFIG and Flag.OPENCODE_CONFIG_CONTENT, with a
  layerWith seam for tests (mirrors Global.layerWith and upstream's
  Config.Options file/content from 43c0838).
- Precedence matches V1 and upstream: global directory, then the
  OPENCODE_CONFIG file, then project files, then project .opencode
  directories, then OPENCODE_CONFIG_CONTENT last.
- V1-format sources migrate through the existing loader path.

Tests:
- Config loader orders explicit file and content entries.
- A jail-shaped V1 config supplied as content reaches the catalog and
  SessionRunnerModel resolves the named swxtch-egress model to an
  OpenAI-compatible route at the proxy URL.

Claude-Session: https://claude.ai/code/session_01HGGfNzV3BHB7W49PTT9MVp
@brentyates-swx

Copy link
Copy Markdown
Author

Session ID: 34738ff3-3dcf-4587-8fde-7c58d5478796
Working repo: /home/byates/fm-worktrees/fm-llmrouter/opencode-v2-provider-1952/swx-opencode

The V2 Config layer read only the global config directory and discovered
project files, ignoring OPENCODE_CONFIG and OPENCODE_CONFIG_CONTENT. The
OpenCode jail supplies its whole config through those variables, so the
V2 runner's catalog never held the swxtch-egress provider or the
configured default model: a named egress model failed with
ModelUnavailableError and an unnamed one fell to another provider
(swxtchio/swx-abbe#1952).

Config now takes explicit file and content sources, defaulting to the
two flags, and orders them as V1 does: global directory, explicit file,
project files, .opencode directories, then inline content. V1-format
sources migrate through the existing loader path. Config.nodeWith names
the sources directly for tests.

Tests drive the real loader: precedence of every source, and the jail's
V1 config (placeholder file plus rendered content) through the config
provider plugin and catalog to SessionRunnerModel, for a named model and
for the configured default against a competing provider.

Claude-Session: https://claude.ai/code/session_01HGGfNzV3BHB7W49PTT9MVp
Review round 1: with the env-config fix alone, a location-only V2
session in the jail (create_session_v2 names no model, and the jail
config sets none) still resolved a credential-free catalog model such
as opencode/hy3-free ahead of the config-declared swxtch-egress model,
so egress turns still failed.

V1's Provider.defaultModel takes its default from the config-declared
providers when no model is configured. The V2 catalog now does the
same: the config provider plugin records the declared providers in
config order, and Catalog.model.default picks the newest available
model of the first one that has any, before its newest-available
fallback. An explicit session model or configured `model` still wins.
The preference registers on the internal catalog under the plugin's
scope, because the public plugin catalog API has no such hook.

Tests: the jail path through the production location runtime (every
internal plugin, the recorded models.dev corpus, the jail config via
OPENCODE_CONFIG and OPENCODE_CONFIG_CONTENT) resolves a location-only
session to the egress model; and the preference skips an unavailable
declared provider and beats a newer undeclared model. Both are red
without the preference.

Claude-Session: https://claude.ai/code/session_01HGGfNzV3BHB7W49PTT9MVp
@brentyates-swx
brentyates-swx marked this pull request as ready for review October 23, 2026 15:24
@brentyates-swx
brentyates-swx merged commit 774d90c into swxtch Oct 23, 2026
3 checks passed
@brentyates-swx
brentyates-swx deleted the fm/opencode-v2-provider-1952 branch October 23, 2026 15:59
brentyates-swx added a commit that referenced this pull request Oct 23, 2026
Run 39983517672 (harness and source 774d90c) built and probed the clean
source natively; artifact 12515386562. Also retain the focused small-model
and #347 config test runs at the release source.

Claude-Session: https://claude.ai/code/session_01PTFci7pw9AaruJDJoHDsPr
brentyates-swx added a commit that referenced this pull request Oct 23, 2026
* docs(release): plan source-pinned release cut for 774d90c

Record pickup identity and ancestry for release v1.18.32-swxtch.774d90c7,
which carries #347 for Abbe B-16.10, and the native build/publish plan.

Claude-Session: https://claude.ai/code/session_01PTFci7pw9AaruJDJoHDsPr

* docs(release): retain native x64 candidate evidence for 774d90c

Run 39983517672 (harness and source 774d90c) built and probed the clean
source natively; artifact 12515386562. Also retain the focused small-model
and #347 config test runs at the release source.

Claude-Session: https://claude.ai/code/session_01PTFci7pw9AaruJDJoHDsPr

* docs(release): retain native arm64 candidate evidence for 774d90c

Built and probed natively on vm14-001 under firstmate's grant and the
single ACTIVE lease (acquired 16:35:05Z, released 16:41:03Z, no task
processes left).

Claude-Session: https://claude.ai/code/session_01PTFci7pw9AaruJDJoHDsPr

* docs(release): add release body for v1.18.32-swxtch.774d90c7

Claude-Session: https://claude.ai/code/session_01PTFci7pw9AaruJDJoHDsPr

* docs(release): retain publication read-back for v1.18.32-swxtch.774d90c7

Claude-Session: https://claude.ai/code/session_01PTFci7pw9AaruJDJoHDsPr

* docs(release): record release-cut-3 evidence, delivery and changeset

Claude-Session: https://claude.ai/code/session_01PTFci7pw9AaruJDJoHDsPr
brentyates-swx added a commit that referenced this pull request Oct 23, 2026
* docs(release): plan the cd3f0ce source-pinned release cut

Records the pickup identity and ancestry for source cd3f0ce (#350 on top
of #347) and the execution plan, including Abbe's jail V2 egress rig as a
publication gate on the unpublished packaged bytes.

Claude-Session: https://claude.ai/code/session_01NKeH8jbkJT2c3st6wJmeM1

* chore(release): record v1.18.32-swxtch.cd3f0ce2 native, jail-rig and publication evidence

Adds the published release body and the raw arm64 (vm14-001) and x64
(Actions run 40015678366) build and probe observations, the lease receipts,
the focused test runs, the Abbe jail V2 egress rig run on the unpublished
arm64 candidate, and the publication read-back and downloaded-byte checks.

Claude-Session: https://claude.ai/code/session_01NKeH8jbkJT2c3st6wJmeM1

* docs(release): record cd3f0ce release evidence, delivery and changeset entry

Adds RELEASE-CUT-4-EVIDENCE.md and the raw-evidence index, the SaaS wake-log
append receipt and peer outbox record, the unestablished Abbe B-16.10
recipient, and the #351 CHANGESET entry.

Claude-Session: https://claude.ai/code/session_01NKeH8jbkJT2c3st6wJmeM1

* docs(release): record the Abbe handoff as delivered by firstmate

Firstmate relayed the cd3f0ce release notice to Abbe through its peer
channel after this task's own sends were refused. The evidence now cites
and retains that relay, noting that it points to the published checksum
companions rather than quoting the digests.

Claude-Session: https://claude.ai/code/session_01NKeH8jbkJT2c3st6wJmeM1

* docs(release): record Abbe's acknowledgment and bump-brief citation as the handoff read-back

Abbe ruled that peer homes never write each other's runtime state, which
withdrew the task wake-log append. The evidence now records the two
read-back receipts Abbe named: acknowledgment p1792786679-2709706-0cf3 and
the bump brief's verbatim citation of the relay, plus the ruling's proof.

Claude-Session: https://claude.ai/code/session_01NKeH8jbkJT2c3st6wJmeM1

* docs(release): complete the cd3f0ce delivery receipt chain

Appends the delivery record to this task's producer wake log and keeps its
read-back receipt and verbatim copy. Records firstmate's disposition that
the Abbe captain's ruling waived the Abbe task wake-log append, and keeps
the rerun relay identity check against the archived and delivered copies.

Claude-Session: https://claude.ai/code/session_01NKeH8jbkJT2c3st6wJmeM1
brentyates-swx added a commit that referenced this pull request Oct 24, 2026
* docs(release): plan the d7f7888 source-pinned release cut

Records the pickup identity and ancestry for source d7f7888 (#354 on top
of #350 and #347), the brief's #353 discrepancy, and the execution plan: x64
via the release-candidate job, arm64 and Abbe's jail V2 egress rig (with the
compact mode) on vm14-001 under the lease, as the publication gate.

Claude-Session: https://claude.ai/code/session_013Se6FG9EEPtmFRtg1hLDMm

* chore(release): record v1.18.32-swxtch.d7f78881 native, jail-rig and publication evidence

Adds the published release body and the raw arm64 (vm14-001) and x64 (Actions
run 40052511379) build and probe observations, the three lease laps' receipts,
the focused test runs, Abbe's jail V2 egress rig in all four modes (including
compact) on the unpublished arm64 candidate, with the two setup-failed laps kept
as found, and the publication read-back and downloaded-byte checks.

Claude-Session: https://claude.ai/code/session_013Se6FG9EEPtmFRtg1hLDMm

* docs(release): record d7f7888 release evidence and changeset entry

Adds RELEASE-CUT-5-EVIDENCE.md and the raw-evidence index, and the #357
CHANGESET entry. Records the brief's #353 discrepancy, the three lease laps,
the arm64 jail rig pass in all four modes with the x64 caveat, why the V2 turn
modes' completed waits say nothing about #352, and that delivery is firstmate's.

Claude-Session: https://claude.ai/code/session_013Se6FG9EEPtmFRtg1hLDMm
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant