Skip to content

feat: Alibaba Bailian support, custom OpenAI-compatible providers, and an interactive /config panel - #7

Merged
hutusi merged 12 commits into
mainfrom
feature/provider-config-panel
Jul 11, 2026
Merged

hutusi merged 12 commits into
mainfrom
feature/provider-config-panel

Conversation

@hutusi

@hutusi hutusi commented Jul 10, 2026 •

Copy link
Copy Markdown
Owner

What

  • Alibaba Bailian (DashScope) support: bailian/qwen-plus etc., key via DASHSCOPE_API_KEY, China endpoint by default with the international endpoint one baseUrl override away.
  • Generic custom providers: any OpenAI-compatible endpoint (DeepSeek, Ollama, proxies…) can be defined in settings — name + baseUrl, key env var defaulting to <NAME>_API_KEY.
  • Interactive /config panel 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.
  • Settings: new model and providers fields. API keys are honored from the global ~/.minerva/settings.json only (never the shareable project file), and any file that may hold keys is written with mode 0600 (existing files are chmod-tightened).

How

  • The hardcoded anthropic | openai union becomes a data-driven provider registry (buildProviderRegistry): built-ins + presets + settings-defined entries. Overriding a built-in keeps its kind, so an endpoint override can't change the wire protocol.
  • Compatible endpoints go through @ai-sdk/openai-compatible, not createOpenAI({baseURL}) — @ai-sdk/openai v4 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.
  • Live switching is a new minerva/config/set_model extension method. The kernel stays AI-SDK-free: hosts inject a resolveProvider factory 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 a session.model_changed audit event, which replay ignores.
  • Resolution order: model = --model > MINERVA_MODEL > settings.model > default; key = env var > stored key.
  • The acp path 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 verify green on every commit; 134 tests total, knip clean, per-file coverage above threshold.
  • New coverage: registry resolution/overrides/validation, settings merge + 0600 persistence (including tightening a pre-existing 0644 file), kernel live-swap over the protocol with rollback, full-stack TUI tests for /config and the first-run flow, and component tests for the panel's custom-provider and error paths.
  • PTY e2e (expect) against the real binary: first-run panel → configure bailian → key stored 0600 → restart boots straight to the composer with bailian/qwen-plus.
  • The PTY run caught a real bug pre-merge: rapid ↓+enter arriving as one React batch selected the previously highlighted provider row (stale closure). Fixed with a ref-backed index and pinned with a single-chunk-input regression test.

Summary by CodeRabbit

  • New Features
    • Added Alibaba Bailian (DashScope) provider support.
    • Added OpenAI-compatible custom provider support with provider-specific settings.
    • Added an interactive first-run TUI setup to choose provider/API key/model (masked input).
    • Added /config to switch provider/key/model without restarting and update the next prompt immediately.
  • Security
    • API keys are stored only in global settings and written with secure file permissions (0600).
  • Documentation
    • Updated quick start and configuration/provider docs, including environment-variable precedence and /config usage.

hutusi added 7 commits July 11, 2026 07:20
…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.
@coderabbitai

coderabbitai Bot commented Jul 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@hutusi, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a9784221-7d99-46ba-9dba-666931c1c962

📥 Commits

Reviewing files that changed from the base of the PR and between 2a860ef and bc2cf37.

📒 Files selected for processing (13)
  • .gitignore
  • CHANGELOG.md
  • README.md
  • docs/PROTOCOL.md
  • packages/cli/src/config-panel.tsx
  • packages/cli/src/index.tsx
  • packages/cli/test/config-panel.test.tsx
  • packages/kernel/src/kernel.ts
  • packages/kernel/src/settings.ts
  • packages/kernel/test/config.test.ts
  • packages/protocol/src/types.ts
  • packages/providers/src/registry.ts
  • packages/providers/test/registry.test.ts
📝 Walkthrough

Walkthrough

Adds registry-driven provider configuration, protected settings persistence, the minerva/config/set_model protocol method, live kernel model switching, and an interactive TUI configuration panel with first-run setup and /config support.

Changes

Provider configuration and live model switching

Layer / File(s) Summary
Provider registry and OpenAI-compatible construction
packages/providers/..., packages/providers/package.json, packages/providers/test/registry.test.ts
Provider resolution supports Bailian and custom OpenAI-compatible providers, registry-defined endpoints, and explicit, environment, and stored-key precedence.
Settings persistence and kernel model switching
packages/kernel/..., packages/protocol/src/types.ts, packages/client/src/client.ts, docs/DESIGN.md, docs/PROTOCOL.md
Settings merge model/provider data while excluding project API keys, global writes enforce 0600, and minerva/config/set_model swaps the active provider and records session changes.
Interactive CLI configuration flow
packages/cli/src/*, packages/cli/test/*, README.md, CHANGELOG.md
The CLI resolves settings and provider choices, opens configuration on first run or /config, and applies model changes without restarting. Interactive and persistence behavior is covered by tests.

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
Loading

Possibly related PRs

  • hutusi/minerva#1: Extends the provider registry and provider-ID logic introduced by that PR.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main additions: Bailian support, custom OpenAI-compatible providers, and the new interactive /config flow.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/provider-config-panel

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
packages/providers/src/registry.ts (1)

181-209: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider aligning conditional-check style across provider branches.

The Anthropic branch uses !== undefined checks for apiKey and baseURL (lines 207-208), while the OpenAI and openai-compatible branches use truthy checks (lines 183, 184, 199). Both approaches are safe — empty strings are filtered downstream in createAnthropicProvider — but the inconsistency may cause confusion about intent. Pick one style and apply it uniformly.

♻️ Optional: align on truthy checks (or !== undefined consistently)
     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 win

Log rollback failures in #configSetModel for diagnosability.

The .catch(() => {}) on the rollback updateGlobalSettings call silently swallows write failures. If the rollback fails (e.g., disk full, permissions), the persisted model stays as the rejected modelRef — 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

📥 Commits

Reviewing files that changed from the base of the PR and between bf80162 and dcb1e5e.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (24)
  • CHANGELOG.md
  • README.md
  • docs/DESIGN.md
  • docs/PROTOCOL.md
  • packages/cli/src/app.tsx
  • packages/cli/src/args.ts
  • packages/cli/src/config-panel.tsx
  • packages/cli/src/index.tsx
  • packages/cli/test/app.test.tsx
  • packages/cli/test/args.test.ts
  • packages/cli/test/config-panel.test.tsx
  • packages/cli/test/fixtures/tui-scripted.tsx
  • packages/client/src/client.ts
  • packages/kernel/src/events.ts
  • packages/kernel/src/kernel.ts
  • packages/kernel/src/runtime.ts
  • packages/kernel/src/settings.ts
  • packages/kernel/test/config.test.ts
  • packages/kernel/test/settings.test.ts
  • packages/protocol/src/types.ts
  • packages/providers/package.json
  • packages/providers/src/ai-sdk.ts
  • packages/providers/src/registry.ts
  • packages/providers/test/registry.test.ts

Comment thread packages/cli/src/index.tsx
Comment thread packages/cli/src/index.tsx
hutusi added 5 commits July 11, 2026 08:03
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).
@hutusi
hutusi merged commit 3fb0230 into main Jul 11, 2026
7 checks passed
@hutusi
hutusi deleted the feature/provider-config-panel branch July 11, 2026 01:48
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