Skip to content

fix(ai-providers): let a keyless server be configured, and say what the model list and an http base are doing - #3043

Merged
datlechin merged 1 commit into
mainfrom
fix/ai-provider-settings-gaps
Sep 22, 2026
Merged

datlechin merged 1 commit into
mainfrom
fix/ai-provider-settings-gaps

Conversation

@datlechin

Copy link
Copy Markdown
Member

The four defects the #3040 investigation found in the AI provider settings and left for a follow-up (#3042 shipped the five the endpoint fix could not be verified without). Each was verified against the code before it was planned, and none depends on the others.

1. A Custom provider could not be saved against a server that wants no key

AIProviderType.authStyle named Cursor, xAI, OpenCode Zen and the local providers explicitly and let .custom fall through to default: .apiKey. Save stayed dim, Test Connection stayed disabled and the model list never loaded, so a self-hosted vLLM, an llama-server behind a reverse proxy, or LM Studio with API-key checking off could not be configured at all, while docs/features/ai-assistant.mdx advertises Custom as taking "any OpenAI-compatible endpoint".

.custom now returns .optionalApiKey. Nothing else needed changing: of the eleven places authStyle is read, seven already bucket .apiKey with .optionalApiKey, and the four that compare == .apiKey exactly are the four gates this is meant to open (isSaveEnabled, Test Connection's disabled, and the early returns in fetchModels and testProvider). authStyle is computed and never persisted, so no stored configuration migrates. AISettingsView.customStatusText already fell back to showing the host when no key was stored, which reads as though it was written expecting this.

2. The model list never reloaded when the API key changed

The Base URL field's onChange scheduled a refetch; all three API-key fields only cleared the test result. Typing the endpoint first and pasting the key second, which is the natural order, left the Model picker holding only Other… with no spinner, no error and no Reload, and the only way out was to go back and retype a character in the Base URL.

All three key fields now schedule the refetch. The Cursor and xAI fields matter as much as the generic one: both are .optionalApiKey, so the empty-key guard never fired for them and their key change was simply lost.

The guard itself moved out of the view into AIModelListFetchGate, which is pure and therefore testable, and the case that blocks the fetch now says so (Enter an API key to load this provider's models.) instead of clearing the error and rendering nothing.

3. A malformed model list read as "this server has no models"

fetchOpenAIModels and fetchOllamaModels each ran one combined guard over the JSON parse and the array cast and returned [] when either failed, so a gateway answering 200 with the wrong shape was indistinguishable from a server that genuinely serves no models. Each is now two guards: a parse failure and a missing array both report, and only a well-formed array, empty or not, returns.

This covers the six types that route through OpenAICompatibleProvider (OpenRouter, OpenCode Zen, Ollama, llama.cpp, MLX, Custom). Claude and Gemini keep their curated offline list, which the six have no equivalent of; OpenAIResponsesProvider already threw. OllamaDetector has the same shape and is deliberately left alone, since it is a silent launch probe with no error surface.

4. No caution when an API key goes over http to another machine

App Transport Security is NSAllowsArbitraryLoads app-wide, the Base URL field takes any http(s) URL, and nothing distinguished the two. The field now carries an inline caution when the resolved base is plaintext to a host that is not this machine.

Warn, do not refuse. MCPServerConfiguration.validate refuses a non-loopback http endpoint outright, and that is right for MCP, which grants remote tool execution. A chat request carrying the user's own key to a server they chose is theirs to decide, and LiteLLM, vLLM and Ollama on a LAN box are ordinary setups. ATS itself is untouched; changing it app-wide is a far larger change than this.

The wording follows what is actually sent: a key that will travel gets Your API key is sent unencrypted over http to this host., and a keyless Custom server or a remote Ollama gets Requests to this host are sent unencrypted over http.

The loopback predicate

Five copies of "is this host loopback" already exist and they disagree: one counts 0.0.0.0 and localhost.localdomain, another matches four exact spellings and misses the rest of 127.0.0.0/8. Rather than adding a sixth, the most complete one moves out of ExternalConnectionTrustKey into LoopbackHost, and both callers use it. ExternalConnectionTrustStoreTests' twelve cases are the proof that the extraction changed nothing. The other three copies keep their own semantics and are left alone; consolidating them is a separate change.

Verification

Step Result
verify.sh build PASS
verify.sh test (16 suites) PASS, 134 cases, 134 passed
verify.sh test (10 suites, after review fixes) PASS, 82 cases, 82 passed
verify.sh lint (12 files) 0 violations
verify.sh docs PASS

Suites run: AIModelListFetchGateTests, LoopbackHostTests, CustomProviderRegistrationTests, AIEndpointTests, AIProviderModelFetchTests, OpenAICompatibleProviderConnectionTests, OpenAICompatibleProviderParserTests, OpenAICompatibleProviderEncodingTests, LocalProviderRegistrationTests, ExternalConnectionTrustStoreTests, AIProviderCapabilitiesTests, AIProviderFactoryResolveTests, AIProviderFactoryCacheTests, AIProviderErrorTests, XAIRegistrationTests, CursorRegistrationTests.

xAI is unaffected throughout: both its paths build OpenAIResponsesProvider or XAIGrokProvider, never OpenAICompatibleProvider.

No UI automation. The reachable assertions here are a field label and two caption lines in the provider detail sheet; the behaviour worth pinning is the fetch gate and the loopback predicate, and both are covered as pure functions instead.

Review

Codex reviewed the working tree and raised four, all fixed in this branch:

  • A regression this PR introduced. Making Custom keyless meant its .onAppear fetch was no longer short-circuited, so a newly added Custom provider, whose Base URL starts empty, asked a transport with no URL for a model list and drew Invalid endpoint: before the user had typed anything. The gate gained a .missingEndpoint case ahead of the key check.
  • A stuck spinner. Clearing the key mid-fetch cancels the request, whose cancellation guard returns without clearing isFetchingModels, and the blocked branch returned without clearing it either. Every blocked branch now resets it.
  • The caution over-claimed. It named the API key regardless of whether one would be sent, which is wrong for a keyless Custom server and for remote Ollama, llama.cpp and MLX.
  • The docs did not describe the new keyless path. The Authenticate step now does.

Not in this PR

OpenAICompatibleProvider.testConnection's .ollama branch is unreachable: its sole caller is the Test Connection button, which lives inside authSection, and Ollama's authStyle is .none, so that section renders EmptyView(). Ollama, llama.cpp, MLX and Claude Agent have no Test Connection affordance at all. Giving the .none-auth providers one is a UI change with its own design questions, so it is reported rather than bundled here.

@mintlify

mintlify Bot commented Sep 21, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
TablePro 🟢 Ready View Preview Sep 21, 2026, 10:06 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

@datlechin
datlechin merged commit 7308593 into main Sep 22, 2026
8 of 14 checks passed
@datlechin
datlechin deleted the fix/ai-provider-settings-gaps branch September 22, 2026 06:16
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.

1 participant