Move bare-host base URL correction out of the clients into a public helper#4
Merged
Conversation
Follow-up to posit-dev/ai-provider-bridge#4, which normalized a bare configured base URL (append the version segment the SDK expects, e.g. https://api.anthropic.com -> https://api.anthropic.com/v1) only on the constructor channel (credentials.baseUrl). The per-request channel (params.baseUrl -- the resolvedBaseUrl the ai-config catalog produces and that the Positron/Node model services pass to chat()) was left unnormalized, and it takes precedence in the client (params.baseUrl ?? this.baseURL). On Positron with the default 'prefer-direct' routing, authentication.*.baseUrl is folded into the resolved catalog and surfaces as resolvedBaseUrl, so a bare host reached the SDK unnormalized and 404'd. Normalize at the single convergence point inside each client's chat(), where both channels merge. Move the canonical host/version constants into the clients (ANTHROPIC_HOST/ANTHROPIC_API_VERSION, OPENAI_*, GEMINI_*), exported and reused by the provider modules for the direct model-fetch URL. Provider factories now pass the raw credentials.baseUrl; the client owns normalization for both channels. Only the version-requiring providers are affected (Anthropic v1, OpenAI v1, Gemini v1beta). DeepSeek, openai-compatible/Foundry, OpenRouter, and the local providers accept a bare host, so they are deliberately left unchanged (the exact-host guard also leaves custom openai-compatible/Foundry hosts untouched). Adds a parameterized base-url-normalization suite covering all three clients: per-request and constructor bare hosts, per-request override precedence, already-versioned passthrough, and unset default preservation.
…elper The chat()-time normalization in AnthropicClient/OpenAIClient/GeminiClient is reverted: the bridge now trusts the base URL it is given. The bare-host correction policy lives in a new normalizeBaseUrlForProvider helper (exported from the root), which consumers apply at the config read seam — Positron uses it via the new optional normalizeBaseUrl hook on PositronAuthSettingDescriptor (applied in buildApiKeyBlock). normalizeConfiguredBaseUrl is removed; normalizeProviderBaseUrl (model discovery) keeps the unset -> host/version fallback but no longer appends the version to a configured bare host — only a local trailing-slash/whitespace trim before URL composition.
…o base-url.ts base-url.ts is reachable from the root entrypoint, which browser bundles import via @assistant/core re-exports. Importing the client modules for their host constants dragged ai-sdk-helpers' Node 'crypto' import into browser code, blanking the standalone web UI. The constants now live in base-url.ts and the client modules re-export them for the provider modules.
wch
enabled auto-merge (squash)
July 8, 2026 22:30
wch
added a commit
that referenced
this pull request
Jul 9, 2026
Main's #4 moved bare-host correction out of the clients: base-url.ts owns the policy and clients trust URLs as given. The merge silently removed the bare-host courtesy from normalizeProviderBaseUrl, which CI caught in provider-test.test.ts. - Add lmstudio to KNOWN_HOSTS (LMSTUDIO_HOST/LMSTUDIO_API_VERSION in base-url.ts, re-exported from LMStudioClient for provider modules) - LMStudioClient no longer normalizes; endpoint trusted as given - The config read seam for stored endpoints is LocalProviderManager .getEndpoint, which corrects a stored bare default host to /v1 so previously stored values keep working - testLMStudioProvider corrects raw pre-save user input the same way
wch
added a commit
that referenced
this pull request
Jul 9, 2026
* Add LOCAL_PROVIDER_DEFAULT_ENDPOINTS canonical default endpoints Canonical default endpoints for Ollama and LM Studio, exported from the local-providers subpath entrypoint only (not the browser-reachable root). LM Studio is the bare server root -- LMStudioClient appends /v1 itself. * Fix LM Studio default endpoint: bare root, not /v1 LMStudioClient appends /v1 itself (and the model fetcher joins v1/models), so the /v1-suffixed default produced /v1/v1 URLs wherever the catalog default reached the client. Converges on the same bare-root shape as LOCAL_PROVIDER_DEFAULT_ENDPOINTS. * LM Studio endpoint includes /v1, matching OpenAI-compatible convention The configured endpoint now carries the version segment (default http://localhost:1234/v1), like other OpenAI-compatible providers, instead of the client appending /v1 itself. All three consumers (LMStudioClient, the model fetcher, testLMStudioProvider) use normalizeProviderBaseUrl, so the exact bare default host still gets /v1 appended for backward compatibility with previously stored endpoints. Ollama is unchanged: its client uses the native API (/api/...) where no version segment exists. * Adapt LM Studio to the base-url.ts correction policy after main merge Main's #4 moved bare-host correction out of the clients: base-url.ts owns the policy and clients trust URLs as given. The merge silently removed the bare-host courtesy from normalizeProviderBaseUrl, which CI caught in provider-test.test.ts. - Add lmstudio to KNOWN_HOSTS (LMSTUDIO_HOST/LMSTUDIO_API_VERSION in base-url.ts, re-exported from LMStudioClient for provider modules) - LMStudioClient no longer normalizes; endpoint trusted as given - The config read seam for stored endpoints is LocalProviderManager .getEndpoint, which corrects a stored bare default host to /v1 so previously stored values keep working - testLMStudioProvider corrects raw pre-save user input the same way
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.
Supersedes the chat()-time normalization approach on this branch (and the constructor-channel normalization from #4). The bridge no longer papers over incorrect base URLs at request time — it trusts the URL it is given, and exposes the correction policy as a helper for the consumer that actually has the bad values.
Background
The
@ai-sdk/*providers expectbaseURLto already include the version segment (/v1,/v1beta) and append only the operation path, so a bare host likehttps://api.anthropic.com404s. Those bare hosts only ever came from Positron: its sign-in dialog prefills a bare Anthropic host and persists it verbatim to user settings (posit-dev/positron#14775). Fixing that inside the clients at chat() time was a permanent workaround in the wrong layer — every request paid for a config-provisioning bug, and providers.json users got a silent rewrite of values they wrote themselves.The consumer-side fix (Posit Assistant's Positron extension) normalizes at the config read seam and rewrites the user's setting on disk; this PR provides the pieces it needs.
Changes
ai-provider-bridgenormalizeBaseUrlForProvider(providerId, url)(src/base-url.ts, exported from the root). Corrects a bare known host to its versioned form — Anthropic (/v1), OpenAI (/v1), Gemini (/v1beta); all other provider IDs and URLs pass through. Matching is tolerant (whitespace/trailing slashes trimmed before comparison), but non-matching input is returned byte-for-byte unchanged, soresult !== urlmeans precisely "bare-host fix applied" — consumers use that identity check directly as their write-back/notification criterion.ANTHROPIC_HOST,OPENAI_HOST,GEMINI_HOST+ versions) now live inbase-url.ts, re-exported by the client modules. The root entrypoint is reachable from browser bundles (consumers re-export runtime values from it), sobase-url.tsmust not import the client modules — that would dragai-sdk-helpers' Nodecryptoimport into browser code (it blanked the standalone web UI in live verification).AnthropicClient/OpenAIClient/GeminiClient:params.baseUrl ?? this.baseURLgoes to the SDK raw.normalizeConfiguredBaseUrlis deleted.normalizeProviderBaseUrlkeeps the unset →host/versionfallback but no longer appends the version to a configured bare host — only a trailing-slash/whitespace trim for${base}/modelscomposition.ai-configPositronAuthSettingDescriptorgains an optionalnormalizeBaseUrl?: (url: string) => string, applied inbuildApiKeyBlockbefore a setbaseUrlenters the host fragment. The function is injected by the consumer (from the bridge helper), preserving ai-config's no-import-edge invariant withai-provider-bridge.Deliberate regression
A bare host (like
https://api.anthropic.com) written in~/.posit/ai/providers.jsonis now a user error and will fail — no silent fix. providers.json authors are expected to write correct URLs.