Repository navigation
fix: share option resolution between the opencode and Pi builders - #25
Closed
outofcoffee wants to merge 1 commit into
Closed
outofcoffee wants to merge 1 commit into
outofcoffee wants to merge 1 commit into
Conversation
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
force-pushed
the
claude/fix-pi-option-resolution
branch
from
August 1, 2026 15:12
8af1f07 to
a931ead
Compare
Collaborator
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes per-provider endpoint variables reach the Pi harness, by giving both harness builders one shared way to read a catalogue entry.
Summary
Provider.ResolveOptions(static options withoptionsFromEnvlayered over them) andProvider.RequireOptions(theoptionsRequiredcheck), and call both from both builders.BuildPiProvidernow resolves its base URL as: explicit override → the provider's ownoptionsFromEnvvariable → thepiblock'sbaseUrl→options.baseURL.BuildPiProvidernow fails when a required option is unset, instead of writing an entry silently missing it.optionsRequiredwith apiblock.Implementation details
The bug.
BuildProviderBlockappliedoptionsFromEnvand enforcedoptionsRequired;BuildPiProviderdid neither, readingp.Options["baseURL"]straight from the catalogue. SoOLLAMA_BASE_URL,LLAMACPP_BASE_URL,OMLX_BASE_URL,VLLM_BASE_URLandOPENAI_BASE_URLwere silently dropped under Pi —models.jsonkept pointing atlocalhostwhile the same Outfit under opencode pointed at the right server. Only the genericOUTFIT_BASE_URLworked, because it arrives by a different parameter.The dropped URL was not the damaging part.
IsLocalEndpointthen still sawlocalhost, so anapiKeyOptionalprovider took the keyless branch and Pi was handed the literal placeholderlocalto 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
optionsFromEnvintoBuildPiProviderwould restore correctness and leave the same trap for the next option kind.optionsRequiredis the proof that this happens: it was added to one builder only, long afteroptionsFromEnvwas, 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.baseURLfalls through tooptions.baseURL, and the only provider with api.baseUrl(openrouter) has no endpoint variable — so this ordering is about what happens when a provider first has both. The alternative, slotting it belowpi.baseUrl, has a nastier failure mode: adding api.baseUrlto 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 Vertexproject, 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
optionsRequiredhalf of the split is currently latent — the providers using it have nopiblock, soBuildPiProviderrejects them earlier — but it would become live the moment apiblock 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