Originally created by: sosidudku1
Closes [#69].
Adding anything other than OpenRouter or AI/ML API meant typing a base URL from memory. That is how a user ended up with an OpenAI endpoint paired with a non-OpenAI key: the wizard offered no middle ground between the two curated catalogs and a blank URL field.
KIND_ROW_ORDER; the render layer derives its labels from it.openai-compatible kind with the base URL filled in and goes straight to the key step. Model lists still come from each server's own /v1/models (#31, [#41]), so nothing here needs updating when a vendor ships a new model. Base URLs are stored without the /v1 suffix, following the repo convention (call sites append /v1/...).GROQ_API_KEY, NOUS_API_KEY, ...), the entry records it as apiKeyEnvVar, and key resolution treats it as authoritative. Adding a second service cannot overwrite the first one's key, and the key screen names the variable that will actually be used. Replacing a key mid-session updates process.env unconditionally, so the running session resolves the new key without a restart..env, resolution returns undefined, and requests carry no Authorization header at all (an empty Bearer token is malformed).groq, nous, ...), and a second entry for the same service gets a numbered suffix (groq-2) via suggestPresetEntryId, now wired into the entry builder.groq updates groq in place instead of minting an openai-compatible duplicate and switching the active provider to it. Hand-added compat entries keep their custom ids on reconfigure too./v1/models with an OpenAI-shaped payload (200 with a data array, or 401/403 asking for a key, which confirms the path exists).The provider list is 13 rows now; renderPickList on current main (#67) windows every list to PICK_WINDOW rows with a position counter, so the pick_kind step clips on short terminals instead of overflowing.
New and updated tests across the preset layer: per-preset env vars (unique, never the shared compat or catalog names, named after the service), base URLs stored without /v1, keyless flags kept on the verified services (presence checks, not a pinned list), suggestPresetEntryId suffixing, presetForEntryId suffix recovery, entry-builder id selection (add, second entry, reconfigure, hand-added id), and save-path scenarios: a Groq key lands in GROQ_API_KEY, two services keep separate keys, a mid-session key replacement updates the live environment, an empty key is refused for Groq, LM Studio saves without a key, a second Groq becomes groq-2, reconfigure keeps groq and the active provider. Providers area plus the wizard component: 68 passing across 7 files. tsc clean. Full suite: 3516 passing, with the same 8 pre-existing failures as current main.
Originally posted by: Ooooze
Reviewed by applying the diff onto current
main(010998b), runningtscand the wizard tests, and curling every preset endpoint. The direction is right and the URLs are correct, but four functional issues need fixing before merge — the first one reproduces exactly the failure mode [#69] was filed about (a key paired with the wrong endpoint).What checks out
The diff applies cleanly onto current
mainwithgit apply --3way,npm run lintis clean, andsrc/tui/providers+providers-wizard.test.tsxare 40/40 green. All nine cloud endpoints are live:nousandollama.comreturn 200, the rest 401/403, so the path exists and asks for a key. The base URLs work becausenormalizeOpenAiBaseUrlstrips the trailing/v1, turninghttps://api.groq.com/openai/v1intohttps://api.groq.com/openai+/v1/models.Blockers
1. Every preset writes its key into the same
OPENAI_COMPAT_API_KEY. The env var name is derived fromkindalone, and every preset resolves toopenai-compatible:The read path is symmetric —
resolveLlmProviderApiKeyalso switches onentry.kind, andload-config.ts:499injects that key into every entry. Add Groq, then DeepSeek: the Groq key is overwritten, both config entries now resolve the DeepSeek key, and Groq returns 401. The config entries coexist, their keys do not, so the isolation the PR description claims only holds halfway. Separately,writeProviderApiKeyToDotenvonly assignsprocess.env[envKey]when it is currently empty, so inside a running session the runtime keeps using the first key even though.envhas already been rewritten. Fix by making the key per-entry — eitherentry.apiKeyinconfig.json(the schema already supports it) or an env name derived from the entry id.2. The LM Studio preset cannot be saved. The comment says a local server needs no key and "the api_key step accepts empty", which is true of the step — but
saveProviderWizardToConfigthen throwsAPI key is empty — paste a key or set it in .env firstwheneverOPENAI_COMPAT_API_KEYis absent from the environment. The one preset advertised as "no API key needed" fails on save in the common case.3.
suggestPresetEntryIdis dead code. It is exported and covered by three tests but never called:providerIdForKindreturnspresetIdverbatim, andmergeProviderIntoBlockreplaces by id, so a second Groq entry silently overwrites the first. The description's "a second entry for the same service gets a numbered suffix" does not match the code. Either wire it intobuildProviderEntryFromWizardor drop it together with its tests — tests on an unused function read as coverage that is not there.4. Reconfigure (
c) breaks preset entries.createProvidersWizardState("configure", …)never setspresetId, so on saveproviderIdForKind(kind, null)returns"openai-compatible". Opening a savedgroqentry and changing the model creates a newopenai-compatibleentry, orphansgroq, and switches the active text provider to the duplicate. This bug already existed for hand-added JSON entries; presets make it the default path. Minimal fix:providerIdForKind(kind, wizard.presetId ?? wizard.providerId).Smaller items
listsModelsWithoutKeyandlocalare never read outside their own test, even though the JSDoc promises the wizard can list models before a key is entered. Meanwhileexpect(keyless).toEqual(["nous", "ollama-cloud"])will break the moment anyone adds a preset with that flag, while asserting nothing real.The comment in
provider-presets.test.tssays the provider appends/chat/completionsrather than/v1/chat/completions. That is inverted: every call site (openai-provider.ts:86,94,134) appends/v1/..., and the codebase stores base URLs without/v1(OPENAI_COMPAT_DEFAULT_BASE_URL = "https://api.openai.com"). The presets only work because normalization strips the suffix, and the/\/v1$/assertion pins the opposite of the repo's convention.The provider list grew from 3 to 13 rows, but
renderPickListdraws every row forpick_kind— thePICK_WINDOW = 12viewport only applies to the discovered model list. With the border, title and hint that is ~17 lines, which overflows a short terminal. Related: row order now lives in two places,KIND_OPTIONSin the component andKIND_ROW_ORDERin the key bindings, with index alignment held together implicitly. Worth exporting one list.Minor: the key step shows
OPENAI_COMPAT_API_KEYas the env hint for Groq (a symptom of issue 1),apiKeyForWizardhardcodesid/kindasopenai-compatiblefor its fallback lookup, and droppingas constfromKIND_OPTIONSwidensidtostring— that field is never read, sincerenderPickListonly consumeslabel.Merge state
GitHub reports
mergeable: false / dirtyand the PR's base commit (5ea2167) is not inmain's history. The diff itself applies cleanly onto currentmain, so the conflict is mechanical — a rebase should clear it.Related
Tickets:
#69Originally posted by: sosidudku1
Thanks for the thorough pass. Rebased onto current
main(d113ebc) and addressed all four blockers plus the smaller items; the branch is now two commits.1. Per-preset keys. Each preset declares its own env var (
GROQ_API_KEY,NOUS_API_KEY, ...), the entry records it asapiKeyEnvVar, andresolveLlmProviderApiKeytreats it as authoritative with no fallback to the shared compat variables, so Groq plus DeepSeek keep two keys andload-configinjects the right one per entry. I went with the env-name route rather thanentry.apiKeyinconfig.jsonto keep secrets out of the config file. The key screen now hints the per-service variable, andapiKeyForWizardprobes it too instead of the hardcoded compat lookup. Also fixed the related bug you spotted:writeProviderApiKeyToDotenvassignsprocess.env[envKey]unconditionally now, so a running session resolves a replaced key immediately.2. Keyless saves.
saveProviderWizardToConfigno longer demands a key when the preset islocalorlistsModelsWithoutKey, so those flags are read for real now. An empty key is a valid state: nothing lands in.env, resolution returnsundefined, andbuildOpenAiHeadersomits theAuthorizationheader entirely instead of sending an emptyBearer. Tests cover both sides: LM Studio saves with an empty key, Groq with an empty key still refuses.3.
suggestPresetEntryIdis wired in. The entry builder takes the taken ids, so adding a second Groq lands asgroq-2next to the first instead of replacing it (test included).4. Reconfigure. The configure wizard recovers the preset behind the entry id (numbered suffixes included, via the new
presetForEntryId) and the save path reuses the existing entry id verbatim, socongroqupdatesgroqin place: noopenai-compatibleduplicate, no active-provider switch. That also fixes the pre-existing case you mentioned, since hand-added compat entries keep their custom ids on reconfigure too.Smaller items: the
/v1comment was indeed inverted, and I went with the repo convention instead of leaning on normalization: presets store API roots without/v1and the test now asserts the absence with a comment explaining that call sites append/v1/....KIND_ROW_ORDERis the single exported list (it moved intoproviders-wizard-phases.tsduring the rebase) and the component derives its labels from it; the widenedidfield onKIND_OPTIONSis gone entirely, since only labels were ever read, and the narrow typing lives onProvidersWizardKindRow. The keyless test checks presence rather than pinning the exact list.On the
pick_kindoverflow: the rebase resolves it for free. [#67] moved thePICK_WINDOWviewport intorenderPickListfor every list, so the 13-row provider list clips to 12 rows with the(1/13)counter instead of overflowing.The rebase conflict was mechanical as you predicted.
tscis clean and the full suite shows 3516 passing with the same 8 pre-existing failures asmain.Related
Tickets:
#67Ticket changed by: Ooooze