Repository navigation
Connect Ask AI to any OpenAI-compatible endpoint - #84
Merged
Merged
Conversation
Adds a Custom endpoint provider: a base URL, an optional API key, and a model ID. It works with any server that accepts OpenAI-style Chat Completions, such as another provider's API, a company gateway, or a local model in Ollama or LM Studio. - The address must use https, except for this computer or the local network. Credentials, query strings and fragments in the address are refused. - Connecting checks GET /models before anything is saved. A key is stored only with system credential protection, as the OpenRouter key is. Keyless servers send no Authorization header. - The model list is filled from /models when the server has one; otherwise a typed model ID works. - The OpenRouter stream reader and error messages move to chat-completions.cjs and are shared. OpenRouter behaves as before. ModelPicker is shared by both providers. Refs Forward-Future#78 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he approved one Per the maintainer's requirement on Forward-Future#78: a local address goes through the existing local-network confirmation before Folio sends anything. - Connecting resolves the address first. http is allowed only when it resolves to this computer or the local network, and any local address (http or https) shows the same "Open a local address?" question Folio asks for links. Declining sends nothing. - The approved origin and addresses are saved with the endpoint. Every request runs in its own in-memory session behind network.cjs's guard, the one URL downloads use, so it can reach only those local addresses; a name that later resolves somewhere local unapproved is refused with a message to reconnect. - The smoke check now uses a real server on folio-local.test and the real question. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
Screenshots from the Windows run (they're hosted in a gist, since the GitHub CLI can't attach images): Settings → Ask AI → Custom endpoint, connected to a Z.ai endpoint with a model chosen: Ask AI panel, with an answer from that endpoint and its page citation. The footer shows the model and "Sent to api.z.ai": |
…wrong base URLs A plain http endpoint was only checked to be local when it was connected. If its name later resolved to a public address (a company name off the VPN, for example), requests went there unencrypted. Its requests now run behind a local-only guard and are refused instead. An answer that runs past the 5-minute limit now says so instead of showing the raw abort error, and a 404 without an API error points at the base URL (such as a missing /v1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…th its address when a save fails A name that resolves to both a local and a public address was treated as local, so plain http could reach the public one. Plain http now needs every address to be local, both when saving and in the request guard. Any local address still means Folio asks first. The key and the address are saved as two files. If the address can't be written, the previous key is put back (or removed), so the saved address is never sent another server's key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
Before screenshot, to go with the ones above (CONTRIBUTING asks for before and after). Same screen and window size, from unmodified Before: Settings → Ask AI. The provider list has only ChatGPT, Claude and OpenRouter, and OpenRouter's address is fixed: After: the "Custom endpoint" screenshot in the comment above. |
…-settings # Conflicts: # .github/workflows/ci.yml
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.



Problem
Ask AI works only with ChatGPT, Claude Code and OpenRouter. Many people use a provider Folio doesn't list (Z.ai, DeepSeek, Mistral, Groq, a company gateway) or a model on their own computer (Ollama, LM Studio). All of them accept OpenAI-style Chat Completions, which Folio's OpenRouter client already sends, but the OpenRouter address was fixed.
Change
A Custom endpoint provider in Settings → Ask AI, with a base URL, an optional API key and a model ID. It reuses the OpenRouter client's Chat Completions code:
electron/chat-completions.cjsholds the stream reader, the error messages, the secure-storage check and the write ordering. Bothopenrouter.cjsand the newcustom-endpoint.cjsuse it. OpenRouter's behavior and its existing tests are unchanged.ModelPickeris shared by both settings screens.GET {base}/modelsbefore it saves anything.Authorizationheader./modelswhen the server has one; otherwise you type a model ID.https://. Plainhttp://is accepted only when every address the name resolves to is on this computer or the local network. Its requests also run behind a local-only guard, so if the name later resolves to a public address (a company name off the VPN, for example), nothing is sent.confirmLocalAddressinmain.cjs, now passed toai.register) before Folio sends anything. Declining sends nothing.network.cjs'sguardSession, the guard URL downloads use. So requests reach only the approved local addresses, and Folio connects to addresses it resolved itself.Issue or approval
Approved in #78: #78 (comment) (Matthew Berman, "Approved as you scoped it"), with two requirements:
How I checked it
All on Windows 11 with Node 26.5. I didn't test on macOS or Linux.
npx tsc --noEmitandnpm run buildpass.node --test scripts/test-custom-endpoint.cjs scripts/test-openrouter.cjs scripts/test-network-security.cjs scripts/test-claude-path.cjs: 27 passed, 0 failed (1 Unix-only test skipped). There are 8 new tests for the endpoint and 2 for the guard. They use a stand-in for the system's encryption. They cover:/v1).scripts/smoke-custom-endpoint.mjs, added tosmoke-test.mjs): runs the real app against a real HTTP server onfolio-local.test(the existing host-resolver rule). It covers the question being asked and declined with nothing sent, then connecting, models, a streamed answer, a citation jumping to page 2, Stop closing the connection at the server, choosing a typed model ID, and removal. This passes locally.npm run smokestops later at the highlight step (smoke-test.mjs,.highlightEditor). It stops at the same step on unmodifiedmainon this machine, so I think it's a local environment difference (Node 26 here, Node 22 in CI), not this change.https://api.z.ai/api/coding/paas/v4, modelglm-5.3-flash) answered "The ferry leaves at dawn (p. 2)" in about 2 seconds, and the citation jumped to page 2. No local-address question was asked.http://127.0.0.1triggered the question once, received noAuthorizationheader, and streamed its answer.Screenshots of the settings screen and an answer are attached below.
Code written by Claude Opus 5.5 in Claude Code. I reviewed and tested it.
🤖 Generated with Claude Code