feat: Alibaba Bailian support, custom OpenAI-compatible providers, and an interactive /config panel - #7
Conversation
…nAI-compatible providers
Minerva only knew anthropic/openai as a hardcoded union, so pointing it
at Alibaba Bailian (DashScope) or any other OpenAI-compatible endpoint
was impossible. Replace the union with a data-driven registry: built-ins
plus user-defined providers (name + baseUrl + apiKeyEnv + defaultModel),
with bailian shipped as a preset. Overriding a built-in keeps its kind
so an endpoint override (e.g. the intl DashScope host) can't change the
wire protocol.
Uses @ai-sdk/openai-compatible rather than createOpenAI({baseURL})
because @ai-sdk/openai v4 routes the callable provider to the Responses
API, which compatible endpoints like DashScope don't implement, and its
chat path emits strict-JSON-schema fields they commonly reject.
resolveApiKey centralizes key precedence (explicit > env var > stored
key) ahead of the settings-stored keys added in the next slices.
Settings gain a default model ref and per-provider entries (baseUrl, apiKeyEnv, defaultModel, apiKey) so the upcoming config panel has somewhere durable to write. API keys are honored only from the global layer — a project settings file is shareable/committable, so a key found there is stripped during merge rather than trusted. Runtime.writeTextFile learns an optional mode, and the new updateGlobalSettings helper always writes the global file as 0600: writeFile's mode applies only at creation, so an explicit chmod also tightens files that predate key storage. defaultDataDir is extracted so the CLI can compute the same ~/.minerva path as the kernel.
Frontends need a way to change provider/model without restarting the process (the /config panel applies changes to the very next prompt). The new extension method persists the model ref — and optionally a provider definition plus API key — to global settings, then swaps the kernel's live provider. Provider construction stays out of the kernel: hosts inject a resolveProvider factory, keeping the kernel free of AI SDK knowledge (design decision: kernel only sees ModelProvider). Hosts that omit it (none today) reject the method. Persist happens before resolve so a resolver that re-reads settings sees the just-entered key; a resolver failure rolls the model ref back so a rejected switch can't brick the next startup. Each open session logs a session.model_changed audit event, which replay ignores, keeping old kernels' logs resumable.
The entrypoint now loads settings before building the kernel: the model ref resolves as --model flag > MINERVA_MODEL > settings.model > default, and API keys as env var > settings-stored key, honoring custom provider definitions (baseUrl/apiKeyEnv) from settings. It also injects the resolveProvider factory for minerva/config/set_model, which re-reads settings on every switch so a key the config panel just persisted is picked up without a restart. The missing-key path still exits, but the message now knows about custom key env vars (e.g. DASHSCOPE_API_KEY) and points at /config; the TUI path stops exiting in the next slice when the panel lands.
Missing API keys used to kill the process before the TUI even mounted. Now the TUI opens an interactive config panel instead: pick a provider (built-ins, the bailian preset, or a custom OpenAI-compatible endpoint with name + baseUrl), enter a key (masked input), confirm a model. The panel persists via minerva/config/set_model and the swap applies to the very next prompt — no restart. /config reopens it any time, and the header reflects the new model live. The acp path keeps its hard exit since stdout there belongs to the protocol. The panel follows PermissionPrompt's inline-replacement pattern and sits below `pending` in the render ternary, so it can never hide a running turn's permission request. Provider selection tracks the highlight in a ref as well as state: a rapid ↓+enter reaches React as one batch, and selecting via the state closure picked the previously highlighted row — caught by a PTY e2e run, regression-tested with a single-chunk write.
Document the new provider surface: the bailian preset (with the intl baseUrl override), custom OpenAI-compatible providers in settings, key precedence (env over stored, global-only, 0600), the /config panel and first-run flow, and the minerva/config/set_model extension method with its host-injected resolveProvider design.
The full-stack TUI tests only drive the happy preset flow, leaving the custom-provider steps, validation errors, esc navigation, and rejected saves untested — below CI's per-file coverage threshold. Exercise them against the component directly, without the kernel plumbing.
|
Warning Review limit reached
Next review available in: 36 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (13)
📝 WalkthroughWalkthroughAdds registry-driven provider configuration, protected settings persistence, the ChangesProvider configuration and live model switching
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant ConfigPanel
participant MinervaClient
participant MinervaKernel
participant ProviderRegistry
User->>ConfigPanel: Select provider, key, and model
ConfigPanel->>MinervaClient: setModel(configuration)
MinervaClient->>MinervaKernel: configSetModel(request)
MinervaKernel->>ProviderRegistry: resolveProvider(modelRef)
ProviderRegistry-->>MinervaKernel: active model provider
MinervaKernel-->>MinervaClient: providerId
MinervaClient-->>ConfigPanel: configuration applied
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
packages/providers/src/registry.ts (1)
181-209: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider aligning conditional-check style across provider branches.
The Anthropic branch uses
!== undefinedchecks forapiKeyandbaseURL(lines 207-208), while the OpenAI andopenai-compatiblebranches use truthy checks (lines 183, 184, 199). Both approaches are safe — empty strings are filtered downstream increateAnthropicProvider— but the inconsistency may cause confusion about intent. Pick one style and apply it uniformly.♻️ Optional: align on truthy checks (or
!== undefinedconsistently)default: { const modelId = model || DEFAULT_ANTHROPIC_MODEL; return createAnthropicProvider({ model: modelId, - ...(options.apiKey !== undefined ? { apiKey: options.apiKey } : {}), - ...(def.baseURL !== undefined ? { baseURL: def.baseURL } : {}), + ...(options.apiKey ? { apiKey: options.apiKey } : {}), + ...(def.baseURL ? { baseURL: def.baseURL } : {}), }); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/providers/src/registry.ts` around lines 181 - 209, Align the optional apiKey and baseURL checks across the openai, openai-compatible, and default Anthropic branches in the provider registry. Choose one consistent style—truthy checks or explicit !== undefined checks—and apply it to the option spreads in the openai provider creation, createOpenAICompatible, and createAnthropicProvider calls.packages/kernel/src/kernel.ts (1)
367-370: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winLog rollback failures in
#configSetModelfor diagnosability.The
.catch(() => {})on the rollbackupdateGlobalSettingscall silently swallows write failures. If the rollback fails (e.g., disk full, permissions), the persistedmodelstays as the rejectedmodelRef— which can brick the next startup. The resolver error is still thrown to the caller, but the failed rollback is completely invisible, making the issue impossible to diagnose.🔊 Proposed fix: surface rollback errors to stderr
- })).catch(() => {}); + })).catch((rollbackError) => { + process.stderr.write( + `minerva: failed to roll back model ref after rejected switch: ${rollbackError}\n`, + ); + });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/kernel/src/kernel.ts` around lines 367 - 370, In `#configSetModel`, replace the empty catch on the rollback updateGlobalSettings call with error handling that reports the rollback failure and its details to stderr, while preserving the original resolver error thrown to the caller.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/cli/src/index.tsx`:
- Around line 73-76: Normalize empty or whitespace-only environment API key
values as absent before calling resolveApiKey, so valid stored keys are selected
when env values are empty; update both the primary resolution and the additional
provider-resolution block around the corresponding logic, preserving
env-over-settings precedence for non-empty values and keeping selector status
consistent.
- Around line 102-110: The ACP hard exit and needsConfig logic incorrectly
assume every missing apiKey requires authentication, blocking keyless custom
OpenAI-compatible providers. Update the provider-selection/authentication logic
around needsConfig and the command === "acp" branch to derive or persist whether
the selected provider requires auth, and gate setup and process.exit(1) on that
auth-required flag rather than !apiKey.
---
Nitpick comments:
In `@packages/kernel/src/kernel.ts`:
- Around line 367-370: In `#configSetModel`, replace the empty catch on the
rollback updateGlobalSettings call with error handling that reports the rollback
failure and its details to stderr, while preserving the original resolver error
thrown to the caller.
In `@packages/providers/src/registry.ts`:
- Around line 181-209: Align the optional apiKey and baseURL checks across the
openai, openai-compatible, and default Anthropic branches in the provider
registry. Choose one consistent style—truthy checks or explicit !== undefined
checks—and apply it to the option spreads in the openai provider creation,
createOpenAICompatible, and createAnthropicProvider calls.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 33501fd4-29c0-4684-b9f6-20bf82d786f0
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (24)
CHANGELOG.mdREADME.mddocs/DESIGN.mddocs/PROTOCOL.mdpackages/cli/src/app.tsxpackages/cli/src/args.tspackages/cli/src/config-panel.tsxpackages/cli/src/index.tsxpackages/cli/test/app.test.tsxpackages/cli/test/args.test.tspackages/cli/test/config-panel.test.tsxpackages/cli/test/fixtures/tui-scripted.tsxpackages/client/src/client.tspackages/kernel/src/events.tspackages/kernel/src/kernel.tspackages/kernel/src/runtime.tspackages/kernel/src/settings.tspackages/kernel/test/config.test.tspackages/kernel/test/settings.test.tspackages/protocol/src/types.tspackages/providers/package.jsonpackages/providers/src/ai-sdk.tspackages/providers/src/registry.tspackages/providers/test/registry.test.ts
Bailian hosts third-party models alongside qwen (e.g. Zhipu's GLM), but nothing surfaced them: the config panel prefilled only qwen-plus and any other id had to be known and typed blind. Providers now carry an optional models list — suggestions, never a restriction — which the panel cycles with up/down at the model step while free text keeps working. The bailian preset ships qwen-plus/-max/-turbo and glm-5.2; settings can replace the list per provider. The cycle position lives in a ref for the same reason as the provider selector's highlight: rapid key events can reach React as one batch.
The model step showed a free-text input with the known models buried in
a dim wrapped hint line ("known: … (↑/↓ cycle)") — users looking to pick
glm-5.2 on bailian couldn't see anything selectable. Render the list as
highlighted rows like the provider step instead: ↑/↓ moves, enter saves,
the provider's default is annotated, and a trailing "other…" row drops
into free-text entry for ids the preset doesn't know (esc returns to
the list). Providers without a models list keep the plain text input.
Verified against the real binary over a PTY: first run → bailian → key
→ arrow down to glm-5.2 → enter persists bailian/glm-5.2.
The provider list showed exactly one model id per row (the default), so the opening screen read as a three-model picker — someone looking for glm-5.2 on bailian saw only qwen-plus and concluded it wasn't there. Each provider row now carries a dim second line with its full known- models list (falling back to the default model when no list exists), so what's selectable is visible before committing to a provider. Also drop the "— <name>" title suffix on this step: it echoed whichever row was highlighted and read as an already-made choice.
Running the TUI inside this repo persists "always allow" permission rules to <cwd>/.minerva/settings.json, which then shows up as untracked noise (with machine-specific paths) after every dev or e2e session. Ignore it; a team that wants deliberately shared project settings can force-add the file.
Review round on PR #7 surfaced two real gaps and two nitpicks: - Blank keys count as absent: an exported-but-empty env var used to win the ?? chain in resolveApiKey, masking a usable stored key while the provider row claimed "key: saved in settings". resolveApiKey and the panel's keySource computation now ignore empty/whitespace values. - Keyless endpoints are first-class: the panel allowed saving a custom provider without a key, but startup still gated on !apiKey, forcing local endpoints (e.g. Ollama) back into setup every launch and blocking `minerva acp` outright. Providers now carry requiresApiKey: false — persisted automatically when the panel saves a keyless custom provider, settable in settings — and both the first-run gate and the acp hard exit honor it. - The model-ref rollback after a rejected switch no longer swallows write failures; they surface on stderr, since settings would be left pointing at the rejected model with no other trace of why. - Align the anthropic branch's option spreads in createProviderFromRef with the other branches (truthy checks).
What
bailian/qwen-plusetc., key viaDASHSCOPE_API_KEY, China endpoint by default with the international endpoint onebaseUrloverride away.baseUrl, key env var defaulting to<NAME>_API_KEY./configpanel in the TUI: pick a provider, enter an API key (masked), confirm a model. Opens automatically on first launch when no key is found — replacing the old print-error-and-exit behavior — and applies live: the next prompt uses the new provider, no restart.modelandprovidersfields. API keys are honored from the global~/.minerva/settings.jsononly (never the shareable project file), and any file that may hold keys is written with mode0600(existing files are chmod-tightened).How
anthropic | openaiunion becomes a data-driven provider registry (buildProviderRegistry): built-ins + presets + settings-defined entries. Overriding a built-in keeps itskind, so an endpoint override can't change the wire protocol.@ai-sdk/openai-compatible, notcreateOpenAI({baseURL})—@ai-sdk/openaiv4 routes its callable provider to the Responses API, which DashScope doesn't implement (requests would 404), and its chat path emits strict-JSON-schema fields compatible endpoints commonly reject.minerva/config/set_modelextension method. The kernel stays AI-SDK-free: hosts inject aresolveProviderfactory that re-reads settings on each switch (so a just-persisted key is picked up). Persist happens before resolve; a resolver failure rolls the model ref back so a rejected switch can't brick the next startup. Open sessions log asession.model_changedaudit event, which replay ignores.--model>MINERVA_MODEL>settings.model> default; key = env var > stored key.acppath keeps a hard exit on missing keys (stdout belongs to the protocol) but now honors stored keys — configuring once in the TUI fixes editor integrations too.Testing
bun run verifygreen on every commit; 134 tests total, knip clean, per-file coverage above threshold.0600persistence (including tightening a pre-existing0644file), kernel live-swap over the protocol with rollback, full-stack TUI tests for/configand the first-run flow, and component tests for the panel's custom-provider and error paths.0600→ restart boots straight to the composer withbailian/qwen-plus.Summary by CodeRabbit
/configto switch provider/key/model without restarting and update the next prompt immediately./configusage.