Repository navigation
Bind up to three upstream addresses per provider - #52
Merged
Merged
Conversation
A provider was one protocol at one URL, so a vendor that publishes Anthropic Messages, Chat Completions and Responses of its own had two of those three callers translated on the way to an upstream that already spoke their language. Conversion was decided by the shape of the config, not by what the upstream could actually serve. A provider is now one bare root address plus up to three addresses bound under it, one per protocol. Whatever is bound is passed through untouched; whatever is not is converted onto the primary. Conversion follows what the user configured. - Provider gains `protocolUrls`. A provider saved before this falls back to `baseUrl` under `protocol`, which is exactly the single upstream it has always had, so nothing changes for existing configurations. - `GatewayUpstreamURL.upstream` accepts either spelling a vendor documents. An endpoint spelling (`…/chat/completions`) is called exactly as typed, pinned through `request_path_overrides`, because a provider publishing that path need not answer `/v1/chat/completions` too. A base spelling keeps the original reconciliation. - `routedProviders` emits one Bifrost provider per bound address, since a Bifrost provider carries one base URL and one wire format. Failover stays between providers: a provider's other addresses do not occupy the fallback chain ahead of the next provider in the queue. - The editor's API address field is now a bare root; the protocol segmented control picks which address the field under it edits, with the conventional address offered from the root, a marker on each bound protocol and a line saying what gets converted. - The probe and model discovery work per address rather than per provider, so three bound addresses can be tested independently and a catalog is looked for at the API being bound before the root. Aggregator and third-party relay presets are removed. Their endpoints move, their protocol support is whatever their own upstream exposes that week, and listing them implied vetting this app cannot do. What remains is Anthropic, OpenAI, Google and the model vendors with a first-party coding endpoint, each with a pure root and the endpoints it publishes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbEBP29dNMEbw6LcVeucnQ
Discovery reads the address bound to the protocol being edited, not the provider root, so that address is what makes a previous "this host has no listing endpoint" answer meaningless. Only the root and the selected protocol were watched, which left an `.unsupported` verdict from one host greying the refresh control out against the next one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbEBP29dNMEbw6LcVeucnQ
Discovery took the bound address's resolved root and the provider's base URL as two places to look, deduplicated as raw strings. For a provider saved before per-protocol addresses those are the same API spelled two ways — `https://host/v1` and the `https://host` it resolves to — so the second root survived, and its candidate list put `…/v1/v1/models` back on the probe list, which is exactly what the candidate ordering exists to keep off it. Reconcile the base URL the same way the bound address is before deduplicating. A provider whose root and bound address name one API now contributes one root; one whose root genuinely differs, like Kimi For Coding's `https://api.kimi.com/coding/v1` under `https://api.kimi.com`, still contributes both, most specific first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbEBP29dNMEbw6LcVeucnQ
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.
A provider was one protocol at one URL, so a vendor that publishes Anthropic Messages, Chat Completions and Responses of its own had two of those three callers translated on the way to an upstream that already spoke their language. Conversion was decided by the shape of the config, not by what the upstream could actually serve.
A provider is now one bare root address plus up to three addresses bound under it, one per protocol. Whatever is bound is passed through untouched; whatever is not is converted onto the primary.
https://api.deepseek.comhttps://api.deepseek.com/anthropichttps://api.deepseek.com/chat/completionsWhat changed
ProvidergainsprotocolUrls. A provider saved before this falls back tobaseUrlunderprotocol, which is exactly the single upstream it has always had, so existing configurations route identically. Opening the editor writes that address out so the field shows what is in use instead of looking empty under a segment marked as bound.GatewayUpstreamURL.upstreamaccepts either spelling a vendor documents. An endpoint spelling (…/chat/completions) is called exactly as typed, pinned throughrequest_path_overrides, because a provider publishing that path need not also answer/v1/chat/completions. A base spelling (…/anthropic) keeps the original/v1reconciliation.routedProvidersemits one Bifrost provider per bound address, since a Bifrost provider carries one base URL and one wire format. Names are digested from provider id and protocol so a provider's own entries cannot collide.●marker on each bound protocol, and a line stating what gets converted.https://api.kimi.com/coding/v1lists modelshttps://api.kimi.comdoes not).Presets
Aggregator and third-party relay presets are removed — their endpoints move, their protocol support is whatever their own upstream exposes that week, and listing them implied vetting this app cannot do. What remains is Anthropic, OpenAI, Google and the model vendors with a first-party coding endpoint, each with a pure root plus the endpoints it publishes. Anything not listed is one 自定义 entry away.
Testing
Unit coverage added for the three-of-three / two-of-three routing cases on a single provider, provider-level fallbacks when one provider binds every protocol, endpoint-vs-base URL resolution, per-address probing, discovery roots, and normalization of blank addresses. Existing preset and upstream-URL invariants were rewritten against the per-protocol endpoints rather than the base URL.
The JavaScript suite passes locally. Swift compilation and the native unit tests only run on the macOS runner, so CI is the first build of this branch.
Note
Scoped to
native/(the shipped SwiftUI 2.x app). The legacysrc/renderer/provider modal is untouched — CI does not build it.🤖 Generated with Claude Code
https://claude.ai/code/session_01GbEBP29dNMEbw6LcVeucnQ
Generated by Claude Code