Skip to content

fix: share option resolution between the opencode and Pi builders - #25

Closed
outofcoffee wants to merge 1 commit into
claude/omlx-provider-support-7h0comfrom
claude/fix-pi-option-resolution
Closed

outofcoffee wants to merge 1 commit into
claude/omlx-provider-support-7h0comfrom
claude/fix-pi-option-resolution

Conversation

@outofcoffee

Copy link
Copy Markdown
Collaborator

Makes per-provider endpoint variables reach the Pi harness, by giving both harness builders one shared way to read a catalogue entry.

Stacked on #24. The base is claude/omlx-provider-support-7h0com, so this diff shows only the fix. Merge #24 first, then this retargets to main automatically.

Summary

  • Extract Provider.ResolveOptions (static options with optionsFromEnv layered over them) and Provider.RequireOptions (the optionsRequired check), and call both from both builders.
  • BuildPiProvider now resolves its base URL as: explicit override → the provider's own optionsFromEnv variable → the pi block's baseUrl → options.baseURL.
  • BuildPiProvider now fails when a required option is unset, instead of writing an entry silently missing it.
  • Add regression tests per affected provider, a precedence test, and an integrity test asserting no embedded provider pairs optionsRequired with a pi block.

Implementation details

The bug. BuildProviderBlock applied optionsFromEnv and enforced optionsRequired; BuildPiProvider did neither, reading p.Options["baseURL"] straight from the catalogue. So OLLAMA_BASE_URL, LLAMACPP_BASE_URL, OMLX_BASE_URL, VLLM_BASE_URL and OPENAI_BASE_URL were silently dropped under Pi — models.json kept pointing at localhost while the same Outfit under opencode pointed at the right server. Only the generic OUTFIT_BASE_URL worked, because it arrives by a different parameter.

The dropped URL was not the damaging part. IsLocalEndpoint then still saw localhost, so an apiKeyOptional provider took the keyless branch and Pi was handed the literal placeholder local to authenticate a remote server with — a rejected request whose cause is nowhere near where it appears. Nothing errored at any point.

Why shared helpers rather than a second copy of the loop. Copying optionsFromEnv into BuildPiProvider would restore correctness and leave the same trap for the next option kind. optionsRequired is the proof that this happens: it was added to one builder only, long after optionsFromEnv was, by an author with no reason to suspect the other builder existed as a separate path. With the resolution shared, a third harness inherits both for free.

Where the per-provider variable sits in the precedence. Above both catalogue values, because it is the user describing their own machine, and below the explicit override, because that is the user being more specific. The two cannot actually collide today — every provider with an optionsFromEnv.baseURL falls through to options.baseURL, and the only provider with a pi.baseUrl (openrouter) has no endpoint variable — so this ordering is about what happens when a provider first has both. The alternative, slotting it below pi.baseUrl, has a nastier failure mode: adding a pi.baseUrl to a local provider later would silently stop its endpoint variable working, which is the same class of bug being fixed here.

Failing on a required option Pi cannot express. A Pi provider entry is {baseUrl, api, apiKey, models} — there is nowhere to put a Vertex project, so such a provider genuinely cannot be served by Pi. Two guards, covering different things: the runtime check catches a catalogue supplied via --providers/OUTFIT_PROVIDERS, which no test can inspect; the integrity test covers the embedded catalogue and states the invariant where a reader will find it.

The optionsRequired half of the split is currently latent — the providers using it have no pi block, so BuildPiProvider rejects them earlier — but it would become live the moment a pi block met a required option.

Both regression tests were confirmed to fail against the pre-fix builder before being committed. The opencode path is behaviourally unchanged; it was already correct.


Generated by Claude Code

The two harness builders in internal/catalog had drifted apart on how they
read a catalogue entry. BuildProviderBlock applies optionsFromEnv and enforces
optionsRequired; BuildPiProvider did neither, reading p.Options["baseURL"]
straight from the catalogue.

The visible symptom was that per-provider endpoint variables did nothing under
Pi: OLLAMA_BASE_URL, LLAMACPP_BASE_URL, OMLX_BASE_URL, VLLM_BASE_URL and
OPENAI_BASE_URL were silently dropped, so models.json kept pointing at
localhost while the same Outfit under opencode pointed at the right server.
Only the generic OUTFIT_BASE_URL worked, because it arrives by a different
parameter.

The dropped URL was not the damaging part. IsLocalEndpoint then still saw
localhost, so an apiKeyOptional provider took the keyless branch and Pi was
handed the literal placeholder "local" to authenticate a remote server with —
a rejected request whose cause is nowhere near where it appears.

The optionsRequired half is the same split, one commit younger, and currently
latent: the providers using it have no pi block, so BuildPiProvider rejects
them earlier. It would become live the moment a pi block met a required
option — including via a runtime catalogue supplied with --providers, which no
integrity test can inspect.

Copying the optionsFromEnv loop into BuildPiProvider would restore correctness
and leave the same trap for the next option kind, so the resolution moves to
Provider.ResolveOptions and Provider.RequireOptions and both builders call
them. A third harness inherits both for free.

Pi's base URL now resolves as: explicit override, then the provider's own
optionsFromEnv variable, then pi.baseUrl, then options.baseURL. The
per-provider variable sits above both catalogue values because it is the user
describing their own machine. The two cannot collide today, so that ordering
is about what happens when someone adds a provider having both — slotting it
below pi.baseUrl would mean adding a pi.baseUrl to omlx later silently stopped
OMLX_BASE_URL working, which is the bug being fixed here.

BuildPiProvider now also fails on an unset required option: Pi's schema is
{baseUrl, api, apiKey, models}, so there is nowhere to put a Vertex project,
and failing beats writing an entry missing something essential. An integrity
test states the matching invariant for the embedded catalogue.

Both regression tests were confirmed to fail against the pre-fix builder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CipwSCMUZRE61zWWiYEcDx
@outofcoffee
outofcoffee force-pushed the claude/fix-pi-option-resolution branch from 8af1f07 to a931ead Compare August 1, 2026 15:12
@outofcoffee
outofcoffee deleted the branch claude/omlx-provider-support-7h0com August 1, 2026 15:13
@outofcoffee outofcoffee closed this Aug 1, 2026
@outofcoffee

Copy link
Copy Markdown
Collaborator Author

Superseded by #26 — GitHub auto-closed this when its base branch claude/omlx-provider-support-7h0com was deleted on the squash-merge of #24. The same fix commit is now rebased onto main in #26.

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.

2 participants