Skip to content

feat(cli): add kimi provider subcommand for non-interactive provider management#313

Merged
7Sageer merged 4 commits into
MoonshotAI:mainfrom
7Sageer:feat/cli-provider-subcommand
Jun 2, 2026
Merged

feat(cli): add kimi provider subcommand for non-interactive provider management#313
7Sageer merged 4 commits into
MoonshotAI:mainfrom
7Sageer:feat/cli-provider-subcommand

Conversation

@7Sageer
Copy link
Copy Markdown
Collaborator

@7Sageer 7Sageer commented Jun 2, 2026

Related Issue

No tracking issue — opened for discussion alongside this PR.

Problem

Today, importing providers into Kimi Code requires launching the TUI and walking through the /provider dialog: an entry-source picker, a URL/key form for custom registries, and a model selector. That is great for first-time setup, but rules out a few practical workflows:

  • Scripted machine bootstrap (dotfiles, devcontainer init, postCreate hooks).
  • CI jobs that need a fresh ~/.kimi-code/config.toml to run an LLM-backed task non-interactively.
  • Multi-machine sync where the same registry URL + key needs to land on every box without keystrokes.
  • Quickly inspecting what providers / models are currently configured from a script (e.g. for a smoke check).

The same applies to discovering providers from the public models.dev catalog — currently only reachable from /provider → "Known third-party provider".

What changed

Adds a new kimi provider subcommand group that mirrors the TUI flow non-interactively. All actions reuse existing helpers (fetchCustomRegistry, applyCustomRegistryProvider, fetchCatalog, applyCatalogProvider, KimiHarness.{getConfig,setConfig,removeProvider}); no new business logic is introduced in the SDK / oauth packages.

Five actions, all designed for shell use:

Command Purpose
kimi provider add <url> --api-key <key> Import every provider listed in a custom api.json registry. Persists the same source = { kind = "apiJson", url, api_key } block the TUI writes, so refreshAllProviderModels keeps the model list current on next launch. Falls back to KIMI_REGISTRY_API_KEY.
kimi provider remove <providerId> Drop a provider and every model alias that referenced it.
kimi provider list [--json] One row per provider with type, model count, and source label (apiJson(...) / oauth / inline); --json for piping.
kimi provider catalog list [providerId] [--filter] [--url] [--json] Browse the public models.dev catalog without writing anything. With a providerId, drills into models with context and capabilities.
kimi provider catalog add <providerId> --api-key <key> [--default-model] Import a known provider from the catalog. --default-model is validated against the catalog's model list before writing.

Design notes:

  • Subcommand registration follows the existing apps/kimi-code/src/cli/sub/export.ts pattern (registerXxxCommand(parent, deps?) with dependency injection for tests).
  • catalog add preserves the existing default_model unless --default-model is explicitly supplied — applyCatalogProvider always assigns one, so the handler restores the previous value when none is requested.
  • Errors exit with code 1 and write to stderr; happy paths write to stdout (machine-friendly).
  • catalog add validates --default-model against the catalog before writing, so a typo can't produce a stale config.

Docs are updated to cover both the user-facing reference (docs/{en,zh}/reference/kimi-command.md) and a cross-link from the providers config page (docs/{en,zh}/configuration/providers.md). The changeset is minor (@moonshot-ai/kimi-code) per gen-changesets/SKILL.md — new backwards-compatible feature, no schema or behavior changes.

Tests: 23 new unit tests in apps/kimi-code/test/cli/provider.test.ts cover happy paths, stale-provider replacement, env-var fallback, missing key, HTTP error surfacing, catalog filtering, drill-down rendering, --default-model validation, default-model preservation, and the commander wiring. The existing options.test.ts registered-subcommands assertion is updated to include provider.

Verified manually against https://free-tokens.msh.team/v1/models/api.json (3 providers, 14 models imported into ~/.kimi-code/config.toml) and https://models.dev/api.json (catalog list anthropic returns 24 models with correct capabilities).

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

Add a non-interactive equivalent of the TUI `/provider` command:

- `kimi provider add <url> --api-key <key>` imports every provider in a
  custom api.json registry, persisting `source` so the next TUI launch
  refreshes the model list automatically.
- `kimi provider remove <id>` deletes a provider and its model aliases.
- `kimi provider list [--json]` prints configured providers with model
  counts and source labels.
- `kimi provider catalog list [providerId] [--filter] [--url] [--json]`
  browses the public models.dev catalog.
- `kimi provider catalog add <providerId> --api-key <key> [--default-model]`
  imports a known provider straight from the catalog.

All actions reuse `fetchCustomRegistry`, `applyCustomRegistryProvider`,
`fetchCatalog`, and `applyCatalogProvider` from the existing oauth/SDK
helpers.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jun 2, 2026

🦋 Changeset detected

Latest commit: dfa496a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jun 2, 2026

pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@dfa496a
npx https://pkg.pr.new/@moonshot-ai/kimi-code@dfa496a

commit: dfa496a

- Use `Array#toSorted()` instead of `Array#sort()` to avoid mutating
  arrays returned from `Object.keys()` / `Object.entries()`.
- Drop redundant boolean-literal comparisons on `model.capability.*`
  fields (already typed as `boolean | undefined`).
- Remove the unnecessary `source as Record<string, unknown>` assertion
  in `providerSourceLabel` — `ProviderConfig.source` is already typed
  that way in the schema.
- Drop the empty-object fallback in `{ ...(config.models ?? {}) }`
  inside the test harness.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eace0a6b44

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +116 to +120
for (const entry of entryList) {
if (config.providers[entry.id] !== undefined) {
config = await harness.removeProvider(entry.id);
}
applyCustomRegistryProvider(asManaged(config), entry, source);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve accumulated imports across removals

When the registry contains more than one provider and any later entry already exists locally, this config = await harness.removeProvider(entry.id) reloads the config from disk, discarding providers that were already applied in memory earlier in the loop but not yet persisted. For example, importing a registry with new provider A followed by existing provider B will finish with only B in the final setConfig patch, so A is silently lost. Remove stale aliases without replacing the accumulated in-memory config, or persist/reload after each application.

Useful? React with 👍 / 👎.

apiKey,
models,
selectedModelId: opts.defaultModel ?? '',
thinking: false,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve thinking defaults for catalog defaults

When users pass --default-model, this hard-codes default_thinking = false for the imported model. In the runtime resolver, defaultThinking === false forces thinking off, so kimi provider catalog add anthropic --default-model <thinking-capable-model> silently disables thinking for future sessions even when the existing config would otherwise use the normal/global thinking default. Preserve the previous default, infer from the selected model, or expose a CLI option instead of always writing false.

Useful? React with 👍 / 👎.

Comment thread apps/kimi-code/src/cli/sub/provider.ts Outdated
Comment on lines +333 to +334
const previousDefaultModel = config.defaultModel;
const previousDefaultThinking = config.defaultThinking;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Capture the default before removing the provider

When re-importing an already-configured catalog provider without --default-model, these values are captured after removeProvider has already cleared defaultModel if it pointed at one of that provider's aliases. So a user who runs kimi provider catalog add anthropic --api-key ... to refresh or rotate the key loses an existing default_model = "anthropic/...", despite the command/comment/docs promising to preserve it when no new default is requested. Save the previous default before removal and restore it after the aliases are repopulated.

Useful? React with 👍 / 👎.

P1 — `provider add`: `harness.removeProvider` re-reads the config from
disk (see `agent-core/src/rpc/core-impl.ts removeKimiProvider`), so
calling it mid-loop discarded providers we had already applied in
memory but not yet persisted. Importing a registry that added a new
provider then replaced an existing one silently lost the new one.
Drop every stale id up front in a single batch, then apply each entry
against the resulting fresh config.

P2 — `catalog add`: `applyCatalogProvider` always writes
`defaultThinking`. Hardcoding `false` would silently disable thinking
for thinking-capable models when the user had it on. Thread the prior
`defaultThinking` through.

P2 — `catalog add`: `removeProvider` clears `defaultModel` when it
pointed at one of the provider's aliases, so capturing
`previousDefaultModel` AFTER the removal yielded `undefined`. Capture
both `defaultModel` and `defaultThinking` BEFORE the removal so
re-importing a configured provider (e.g. to rotate the api key)
preserves the user's chosen default.

Tests:

- `makeHarness` now models the on-disk semantics of `removeProvider`
  (clears `defaultModel` when an alias matches, returns fresh disk
  view), so behavior that depended on the buggy in-memory mock is
  exercised honestly.
- Three new regression tests, each verified to fail against the
  pre-fix handler.
@7Sageer
Copy link
Copy Markdown
Collaborator Author

7Sageer commented Jun 2, 2026

Thanks for the careful review — all three findings reproduced and have been fixed in f97ff2b6. Verified each regression test fails against the pre-fix handler before the fix lands.

P1 — accumulated providers lost across removals (provider add)

You're right: harness.removeProvider round-trips through the RPC, which reads the config from disk via readConfigFile in removeKimiProvider (packages/agent-core/src/rpc/core-impl.ts:391) and returns a fresh disk view. Anything the handler accumulated in its in-memory config between iterations was thrown away. Concretely, importing a registry containing [new-provider, stale-provider] would end with only stale-provider in the final setConfig patch.

Fix: drop every stale id up front in a single batch before the apply loop, then apply each entry against the resulting fresh config without further removals.

let config = await harness.getConfig();
const staleIds = entryList
  .filter((entry) => config.providers[entry.id] !== undefined)
  .map((entry) => entry.id);
for (const id of staleIds) {
  config = await harness.removeProvider(id);
}
for (const entry of entryList) {
  applyCustomRegistryProvider(asManaged(config), entry, source);
  ...
}

P2 — default_thinking = false silently overwrites user's thinking preference (catalog add)

Correct. applyCatalogProvider always assigns defaultThinking from options.thinking (packages/node-sdk/src/catalog.ts:123), and the runtime resolver treats false as "force off". The handler now threads the previous defaultThinking through instead of hardcoding false. When --default-model is omitted, we still restore the previous value explicitly via the post-apply assignment that was already there.

P2 — previousDefaultModel captured after removeProvider clears it (catalog add)

Also correct. removeKimiProvider walks the model aliases and clears defaultModel when one matches the removed provider (core-impl.ts:404,409-411), so reading config.defaultModel after the removal yielded undefined. The handler now captures both defaultModel and defaultThinking before any removal call.

Test harness fidelity

The original makeHarness fake didn't model the on-disk reload semantics, so the original tests couldn't have caught these bugs. The fake now:

  • backs by a persisted field instead of a shared config closure
  • clears defaultModel in removeProvider when an alias matches the removed provider
  • returns a fresh view on every call

Three new regression tests cover all three bugs, and I verified each one fails against the pre-fix handler.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f97ff2b6fe

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/kimi-code/src/cli/sub/provider.ts Outdated
apiKey,
models,
selectedModelId: opts.defaultModel ?? '',
thinking: previousDefaultThinking ?? false,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid forcing thinking off for new catalog defaults

When --default-model is supplied and the config has no existing default_thinking, this falls back to false, and resolveThinkingLevel treats defaultThinking === false as an explicit off request before considering the normal thinking defaults. The fresh evidence is that the previous hard-coded value is now a nullish fallback, so a first-time kimi provider catalog add anthropic --default-model claude-opus-4-7 still persists default_thinking = false and disables thinking for future sessions even for thinking-capable models.

Useful? React with 👍 / 👎.

Comment thread apps/kimi-code/src/cli/sub/provider.ts Outdated
});

if (opts.defaultModel === undefined) {
config.defaultModel = previousDefaultModel;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Only restore catalog defaults that still exist

When re-importing a catalog provider without --default-model, this restores the old defaultModel even if the refreshed catalog no longer includes that model. In that case removeProvider has already deleted the old alias and applyCatalogProvider only repopulates the current catalog models, so future sessions using the preserved default_model fail with “Model ... is not configured”; restore it only if the alias exists after the import, otherwise clear it or choose a valid replacement.

Useful? React with 👍 / 👎.

Two more findings on `catalog add`:

P2 — `default_thinking` fallback to `false` was wrong even after the
previous fix. `resolveThinkingLevel` (agent-core/.../thinking.ts:23)
treats `defaultThinking === false` as an explicit "off" request and
silently disables thinking before per-model defaults kick in. A
first-time `kimi provider catalog add anthropic --default-model
claude-opus-4-7` was therefore still persisting `default_thinking =
false` for thinking-capable models. The handler now always restores
the previous `defaultThinking` (including `undefined`) — the only
way to let the runtime resolver pick the per-model default.

P2 — Restoring `default_model` was unconditional, even when the
refreshed catalog no longer ships that model. `applyCatalogProvider`
drops the old aliases and only populates the current catalog, so
restoring an alias the catalog no longer contains would point
`default_model` at a non-existent entry and break the next session.
The handler now checks whether the alias still resolves and clears
it otherwise.

Test harness:

- The fake `setConfig` now mirrors the real `mergeConfigPatch`
  semantics (deep-merge with `undefined` keys skipped), so tests can
  honestly assert that `setConfig({defaultModel: undefined})` does
  NOT wipe a key from disk — only `removeProvider` can.

Two new regression tests, each verified to fail against the pre-fix
handler.
@7Sageer
Copy link
Copy Markdown
Collaborator Author

7Sageer commented Jun 2, 2026

Thanks — both follow-up P2s landed. Fix in dfa496a2.

P2 — default_thinking fallback to false is still wrong

You're right; the previous ?? false was a bandage. resolveThinkingLevel treats defaultThinking === false as an explicit "off" request before considering per-model defaults (packages/agent-core/src/agent/config/thinking.ts:23), so a first-time kimi provider catalog add anthropic --default-model claude-opus-4-7 was still persisting default_thinking = false and silently disabling thinking for thinking-capable models.

Fix: always restore the previous defaultThinking (including undefined). undefined is the only value that lets the resolver pick the per-model default — true is "force on", false is "force off".

// Always restore — including `undefined`. Anything else would override
// the resolver's per-model default with an explicit on/off request.
config.defaultThinking = previousDefaultThinking;

To make this honestly testable, the fake setConfig now mirrors mergeConfigPatch semantics (deep-merge with undefined keys skipped). Without that, the test assertion setConfig({defaultThinking: undefined}) would have looked like it cleared the key from disk, when the real RPC just no-ops on undefined.

P2 — Only restore catalog defaults that still resolve

Also correct. applyCatalogProvider (packages/node-sdk/src/catalog.ts:113-115) drops every old alias for the provider before populating from the catalog. If the user previously chose anthropic/legacy-claude and the catalog dropped that model, restoring the old default would point default_model at a non-existent alias and the next session would fail with "Model ... is not configured".

Fix: gate the restore on whether the alias still resolves after the import.

const stillResolves =
  previousDefaultModel !== undefined &&
  config.models?.[previousDefaultModel] !== undefined;
config.defaultModel = stillResolves ? previousDefaultModel : undefined;

Cross-provider defaults are also correctly handled: an unrelated default_model = "other/main" survives the import because removeProvider only touches the provider being re-imported, leaving "other/main" in config.models and therefore in the restore path.

Two new regression tests, both verified to fail against the pre-fix handler.

@7Sageer 7Sageer merged commit 3c5dee8 into MoonshotAI:main Jun 2, 2026
8 checks passed
@github-actions github-actions Bot mentioned this pull request Jun 2, 2026
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