Skip to content

feat(kenari): add Kenari as a first-class provider#793

Open
doedja wants to merge 5 commits into
Zoo-Code-Org:mainfrom
doedja:feat/kenari-provider
Open

feat(kenari): add Kenari as a first-class provider#793
doedja wants to merge 5 commits into
Zoo-Code-Org:mainfrom
doedja:feat/kenari-provider

Conversation

@doedja

@doedja doedja commented Jul 3, 2026

Copy link
Copy Markdown

Closes #792

What

Adds Kenari as a first-class dynamic provider. Kenari is an Indonesian OpenAI-compatible AI gateway billed in Rupiah (IDR): one kn- API key covers Claude, GPT, DeepSeek, GLM, Kimi and more. It already works through the generic OpenAI Compatible provider; this gives it a named entry with the live model list auto-populated.

How

Mirrors the Opencode Go provider (#319) file-by-file, adapted to current main:

  • src/api/providers/kenari.ts: handler on RouterProvider (streaming text, reasoning_content, tool-call partials, usage with cache-read tokens).
  • src/api/providers/fetchers/kenari.ts: dynamic model list from the public https://kenari.id/v1/models (no key needed; the key is forwarded when present). Maps context_length and modalities.input (image support). Pricing is intentionally not mapped: Kenari returns IDR (micro_idr_per_1m_tokens), and the ModelInfo price fields are USD, so a converted or raw value would be wrong; cost stays undefined instead.
  • packages/types: kenari.ts (default glm-5-2, 1,048,576 context fallback matching the live default model), settings schema, discriminated union, modelIdKeys, MODELS_BY_PROVIDER, SECRET_STATE_KEYS.
  • Registration: buildApiHandler, modelCache, webviewMessageHandler (fetched unconditionally like the other public-endpoint routers), dynamicProviderExtras.
  • Webview: Kenari.tsx settings component (key field + get-key CTA + ModelPicker), ApiOptions, PROVIDERS constant, providerModelConfig, useSelectedModel, validate.ts, and the kenariApiKey/getKenariApiKey strings across all 18 locales.
  • Tests: handler spec (streaming, request body, completePrompt, error wrapping), fetcher spec (mapping, auth header, error/invalid-entry fallbacks), settings component spec, plus kenari cases in the ClineProvider / webviewMessageHandler / validate suites.

Deliberately skipped from #319: the Anthropic-wire split machinery. Kenari serves every model over OpenAI /chat/completions, so the default "openai" protocol is correct with no extra code.

Testing

  • packages/types suite, src shared+providers sweep (1603 passed / 1 skipped), webview settings+hooks+utils sweep (592 passed), kenari-focused suites 216/216.
  • check-types clean in all three packages; eslint --max-warnings=0 and prettier clean on every touched file.
  • Live verification: https://kenari.id/v1/models public (24 models with context_length and modalities); chat completions verified with a real kn- key against https://kenari.id/v1. Docs: https://kenari.id/docs, OpenAPI: https://kenari.id/openapi.json.

Disclosure: I run kenari.id. Happy to provide a test key for verification.

Summary by CodeRabbit

  • New Features
    • Added Kenari as a supported provider end-to-end, including UI settings, model discovery/selection, routing, and chat handling.
    • Introduced Kenari API key configuration, including the new secret storage key (kenariApiKey).
    • Enabled Kenari streaming and non-streaming responses, including reasoning, tool-call partials, and usage reporting.
    • Added Kenari settings localization across multiple languages.
  • Bug Fixes
    • Improved Kenari configuration validation and model-id fallback behavior.
  • Tests
    • Expanded Kenari test coverage for model fetching/caching, message streaming formatting, and UI interactions.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9f87cded-0e2a-47a1-ad1e-08b6b083d882

📥 Commits

Reviewing files that changed from the base of the PR and between 69e8925 and 77173c2.

📒 Files selected for processing (5)
  • packages/types/src/providers/kenari.ts
  • src/api/providers/__tests__/kenari.spec.ts
  • src/api/providers/fetchers/__tests__/kenari.spec.ts
  • src/api/providers/fetchers/kenari.ts
  • src/api/providers/kenari.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/types/src/providers/kenari.ts
  • src/api/providers/fetchers/tests/kenari.spec.ts
  • src/api/providers/fetchers/kenari.ts
  • src/api/providers/tests/kenari.spec.ts
  • src/api/providers/kenari.ts

📝 Walkthrough

Walkthrough

Kenari is added as a first-class provider across shared types, backend handling, router-model fetching, settings UI wiring, validation, and localized strings. The PR also adds provider-specific model resolution, fetchers, and tests.

Changes

Kenari Provider Integration

Layer / File(s) Summary
Shared types and defaults
packages/types/src/global-settings.ts, packages/types/src/provider-settings.ts, packages/types/src/providers/index.ts, packages/types/src/providers/kenari.ts, src/shared/api.ts
Adds kenari to shared provider metadata, schema validation, secret keys, model-id mappings, default provider lookup, and shared API extras.
Backend fetcher and handler
src/api/index.ts, src/api/providers/kenari.ts, src/api/providers/index.ts, src/api/providers/fetchers/kenari.ts, src/api/providers/fetchers/modelCache.ts, src/api/__tests__/index.spec.ts, src/api/providers/__tests__/kenari.spec.ts, src/api/providers/fetchers/__tests__/kenari.spec.ts, src/api/providers/fetchers/__tests__/modelCache.spec.ts
Implements Kenari model fetching and chat handling, wires both into backend provider dispatch, and adds tests for fetch, stream, and completion behavior.
Webview router-model aggregation
src/core/webview/webviewMessageHandler.ts, src/core/webview/__tests__/ClineProvider.spec.ts, src/core/webview/__tests__/webviewMessageHandler.spec.ts
Adds Kenari as a public model-fetch candidate, initializes its router-model bucket, flushes cached models on new API keys, and updates test expectations.
Settings UI, validation, and localization
webview-ui/src/components/settings/ApiOptions.tsx, webview-ui/src/components/settings/constants.ts, webview-ui/src/components/settings/providers/Kenari.tsx, webview-ui/src/components/settings/providers/index.ts, webview-ui/src/components/settings/ModelPicker.tsx, webview-ui/src/components/settings/utils/providerModelConfig.ts, webview-ui/src/components/ui/hooks/useSelectedModel.ts, webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx, webview-ui/src/components/settings/providers/__tests__/Kenari.spec.tsx, webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts, webview-ui/src/utils/validate.ts, webview-ui/src/utils/__tests__/validate.spec.ts, webview-ui/src/i18n/locales/*/settings.json
Adds the Kenari settings component, registers it in the provider UI, extends model selection and validation wiring, and adds localized settings strings and tests.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • Zoo-Code-Org/Zoo-Code#319: Extends the same dynamic-provider plumbing with a new provider branch across shared types, model fetching, and webview wiring.
  • Zoo-Code-Org/Zoo-Code#344: Adds another provider to the same shared type, default-model, and cache-dispatch surfaces used here.
  • Zoo-Code-Org/Zoo-Code#437: Modifies requestRouterModels in the same way to treat a router provider’s /models endpoint as public and fetch it unconditionally.

Suggested labels: awaiting-review

Suggested reviewers: navedmerchant, JamesRobert20, hannesrudolph, edelauna

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding Kenari as a first-class provider.
Description check ✅ Passed The PR description covers the required what, how, and testing details, with only non-critical template sections omitted.
Linked Issues check ✅ Passed The changes implement the Kenari provider, public model catalog, UI, validation, and tests required by #792.
Out of Scope Changes check ✅ Passed The added tests, locales, UI, and provider wiring all support the Kenari provider work and appear in scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/api/providers/fetchers/kenari.ts (1)

7-7: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Base URL string duplicated with the API handler.

KENARI_BASE_URL here and the hardcoded "https://kenari.id/v1" literal in src/api/providers/kenari.ts (constructor baseURL) are two independent sources of truth. If Kenari's endpoint changes, only one may get updated.

♻️ Proposed fix: export and reuse the constant
-const KENARI_BASE_URL = "https://kenari.id/v1"
+export const KENARI_BASE_URL = "https://kenari.id/v1"

Then in src/api/providers/kenari.ts:

-			baseURL: "https://kenari.id/v1",
+			baseURL: KENARI_BASE_URL,
🤖 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 `@src/api/providers/fetchers/kenari.ts` at line 7, The Kenari base URL is
defined in two places, creating duplicate sources of truth between
KENARI_BASE_URL in the fetcher module and the hardcoded baseURL in the Kenari
provider constructor. Export and reuse the shared KENARI_BASE_URL constant from
the fetcher module inside the Kenari provider so both paths always use the same
endpoint, and keep the provider constructor aligned with that shared symbol.
src/core/webview/webviewMessageHandler.ts (1)

1156-1172: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated "public provider candidate" wiring (same as opencode-go block above).

Lines 1156-1172 mirror lines 1139-1154 almost verbatim (comment text included). Consider extracting a small helper (e.g. pushPublicProviderCandidate(candidates, key, resolvedKey, flushCondition)) since this pattern will likely repeat for future public dynamic providers.

♻️ Sketch of a shared helper
+const pushPublicKeyedCandidate = async (
+	candidates: { key: RouterName; options: GetModelsOptions }[],
+	routerKey: RouterName,
+	apiKey: string | undefined,
+	explicitKeyProvided: boolean,
+) => {
+	if (explicitKeyProvided) {
+		await flushModels({ provider: routerKey, apiKey } as GetModelsOptions, true)
+	}
+	candidates.push({ key: routerKey, options: { provider: routerKey, apiKey } as GetModelsOptions })
+}
🤖 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 `@src/core/webview/webviewMessageHandler.ts` around lines 1156 - 1172, The
Kenari candidate setup is duplicating the same “public provider candidate”
wiring already used in the opencode-go block, so extract the shared logic into a
small helper (for example around the candidate push/flush flow in
`webviewMessageHandler.ts`). Move the repeated key resolution, optional
`flushModels` call, and `candidates.push` behavior into that helper, then reuse
it for both the `kenari` and `opencode-go` paths so future public dynamic
providers can follow the same pattern without copy-paste.
🤖 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.

Nitpick comments:
In `@src/api/providers/fetchers/kenari.ts`:
- Line 7: The Kenari base URL is defined in two places, creating duplicate
sources of truth between KENARI_BASE_URL in the fetcher module and the hardcoded
baseURL in the Kenari provider constructor. Export and reuse the shared
KENARI_BASE_URL constant from the fetcher module inside the Kenari provider so
both paths always use the same endpoint, and keep the provider constructor
aligned with that shared symbol.

In `@src/core/webview/webviewMessageHandler.ts`:
- Around line 1156-1172: The Kenari candidate setup is duplicating the same
“public provider candidate” wiring already used in the opencode-go block, so
extract the shared logic into a small helper (for example around the candidate
push/flush flow in `webviewMessageHandler.ts`). Move the repeated key
resolution, optional `flushModels` call, and `candidates.push` behavior into
that helper, then reuse it for both the `kenari` and `opencode-go` paths so
future public dynamic providers can follow the same pattern without copy-paste.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3570c048-727a-4a48-a722-5c0b66d0506e

📥 Commits

Reviewing files that changed from the base of the PR and between 1c728e7 and eb9af0e.

📒 Files selected for processing (43)
  • packages/types/src/global-settings.ts
  • packages/types/src/provider-settings.ts
  • packages/types/src/providers/index.ts
  • packages/types/src/providers/kenari.ts
  • src/api/index.ts
  • src/api/providers/__tests__/kenari.spec.ts
  • src/api/providers/fetchers/__tests__/kenari.spec.ts
  • src/api/providers/fetchers/kenari.ts
  • src/api/providers/fetchers/modelCache.ts
  • src/api/providers/index.ts
  • src/api/providers/kenari.ts
  • src/core/webview/__tests__/ClineProvider.spec.ts
  • src/core/webview/__tests__/webviewMessageHandler.spec.ts
  • src/core/webview/webviewMessageHandler.ts
  • src/shared/api.ts
  • webview-ui/src/components/settings/ApiOptions.tsx
  • webview-ui/src/components/settings/ModelPicker.tsx
  • webview-ui/src/components/settings/constants.ts
  • webview-ui/src/components/settings/providers/Kenari.tsx
  • webview-ui/src/components/settings/providers/__tests__/Kenari.spec.tsx
  • webview-ui/src/components/settings/providers/index.ts
  • webview-ui/src/components/settings/utils/providerModelConfig.ts
  • webview-ui/src/components/ui/hooks/useSelectedModel.ts
  • webview-ui/src/i18n/locales/ca/settings.json
  • webview-ui/src/i18n/locales/de/settings.json
  • webview-ui/src/i18n/locales/en/settings.json
  • webview-ui/src/i18n/locales/es/settings.json
  • webview-ui/src/i18n/locales/fr/settings.json
  • webview-ui/src/i18n/locales/hi/settings.json
  • webview-ui/src/i18n/locales/id/settings.json
  • webview-ui/src/i18n/locales/it/settings.json
  • webview-ui/src/i18n/locales/ja/settings.json
  • webview-ui/src/i18n/locales/ko/settings.json
  • webview-ui/src/i18n/locales/nl/settings.json
  • webview-ui/src/i18n/locales/pl/settings.json
  • webview-ui/src/i18n/locales/pt-BR/settings.json
  • webview-ui/src/i18n/locales/ru/settings.json
  • webview-ui/src/i18n/locales/tr/settings.json
  • webview-ui/src/i18n/locales/vi/settings.json
  • webview-ui/src/i18n/locales/zh-CN/settings.json
  • webview-ui/src/i18n/locales/zh-TW/settings.json
  • webview-ui/src/utils/__tests__/validate.spec.ts
  • webview-ui/src/utils/validate.ts

@doedja doedja force-pushed the feat/kenari-provider branch from eb9af0e to 90a25b9 Compare July 3, 2026 09:11
@doedja

doedja commented Jul 3, 2026

Copy link
Copy Markdown
Author

The platform-unit-test failure was in SettingsView.change-detection.spec.tsx ("resets cached provider state when a new import timestamp arrives", expected baseten / got deepseek), a file this PR does not touch; it passes locally on this branch (4/4) and the same test failed once on main's own CI when it landed in #726. Pushed a same-content commit to retrigger CI since I lack rerun permissions.

@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Jul 3, 2026

@edelauna edelauna left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good - main comment would be about re-using extract-reasoning logic so we're not duplicating logic.

Before adding this provider can you also create a correspond PR in https://github.com/Zoo-Code-Org/Zoo-Code-Docs for the provider slug documentation.


const models = await getKenariModels("k")

expect(Object.keys(models)).toEqual(["valid-model"])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This only checks that valid-model survives the fallback parse — should it also assert models["valid-model"].contextWindow (the fixture sets context_window: 50000 at line 83)? As written, a mutation that broke field-mapping in this fallback branch specifically wouldn't be caught here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch. Added expect(models["valid-model"].contextWindow).toBe(50000) right after the keys check, so a field-mapping regression in that fallback branch is now caught. (77173c2)

* @returns Normalised model metadata suitable for the model picker.
*/
export const parseKenariModel = (model: KenariModel): ModelInfo => ({
maxTokens: model.max_output_tokens ?? model.max_tokens ?? kenariDefaultModelInfo.maxTokens,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is the max_output_tokens ?? max_tokens (and context_window ?? context_length) precedence intentional? No test currently supplies both fields with different values, so this fallback order isn't actually verified anywhere.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Intentional, and now verified. The precedence matches the house pattern in opencode-go.ts (identical 4-field optional schema) and litellm.ts (max_output_tokens || max_tokens): prefer the explicit context_window/max_output_tokens, fall back to the context_length/max_tokens aliases. Kenari currently returns only context_length and no max-token field, so the first-position fields are forward-compat, but I kept them for consistency and added a parseKenariModel test that supplies both with differing values to lock the order. (77173c2)

Comment thread src/api/providers/kenari.ts Outdated
Comment on lines +83 to +84
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield { type: "reasoning", text: delta.reasoning_content as string }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

src/api/providers/utils/extract-reasoning.ts already has extractReasoningFromDelta(delta) for this — it also falls back to the OpenRouter-style reasoning field and uses a proper typeof === "string" check instead of this cast. Worth reusing here like opencode-go.ts does?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done, switched to extractReasoningFromDelta(delta) exactly like opencode-go.ts. This drops the cast for the proper typeof === "string" check and gains the OpenRouter-style reasoning fallback; added a handler test covering that fallback path. (77173c2)

@github-actions github-actions Bot added awaiting-author PR is waiting for the author to address requested changes and removed awaiting-review PR changes are ready and waiting for maintainer re-review labels Jul 5, 2026
- Reuse the shared extractReasoningFromDelta helper in the stream handler
  instead of a hand-rolled reasoning_content cast, matching opencode-go;
  this also adds the OpenRouter-style `reasoning` fallback. New handler
  test covers the fallback path.
- Fetcher spec: assert the surviving entry's contextWindow in the
  skip-invalid test so a field-mapping regression in that branch is
  caught, and add a parseKenariModel test that locks the
  context_window/max_output_tokens precedence over their aliases.
- Hoist KENARI_BASE_URL into @roo-code/types so the handler baseURL and
  the fetcher endpoint share one source of truth.
@doedja

doedja commented Jul 5, 2026

Copy link
Copy Markdown
Author

Thanks for the review @edelauna. Pushed 77173c2 addressing all three:

  1. Reasoning helper ([kenari.ts:84]) - now uses the shared extractReasoningFromDelta, matching opencode-go.ts; gains the OpenRouter-style reasoning fallback + a test for it.
  2. Fallback-parse coverage ([spec:92]) - asserts contextWindow on the surviving entry.
  3. Precedence ([fetchers:47]) - kept (matches opencode-go/litellm house pattern; Kenari sends only context_length today, so the primary fields are forward-compat) and locked with a parseKenariModel test using both fields.

Gates green locally: check-types (11/11 tasks), the two kenari suites (29 passed), prettier, and eslint --max-warnings=0 on the touched files.

Also opened the companion docs PR as requested: Zoo-Code-Org/Zoo-Code-Docs#7 (provider page + index.json registration).

@doedja doedja requested a review from edelauna July 6, 2026 06:39
@github-actions github-actions Bot added awaiting-review PR changes are ready and waiting for maintainer re-review and removed awaiting-author PR is waiting for the author to address requested changes labels Jul 6, 2026
taltas pushed a commit to Zoo-Code-Org/Zoo-Code-Docs that referenced this pull request Jul 6, 2026
Kenari is an Indonesian OpenAI-compatible gateway billed in Rupiah; one
kn- key reaches Claude, GPT, DeepSeek, GLM, Kimi and more. Adds the
provider page and registers it in index.json so it appears in the
sidebar and the provider comparison table. Companion to the extension
provider PR (Zoo-Code-Org/Zoo-Code#793).
@github-actions github-actions Bot added has-conflicts PR has merge conflicts with the base branch awaiting-review PR changes are ready and waiting for maintainer re-review and removed awaiting-review PR changes are ready and waiting for maintainer re-review has-conflicts PR has merge conflicts with the base branch labels Jul 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-review PR changes are ready and waiting for maintainer re-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Provider request: Kenari (kenari.id), Indonesian OpenAI-compatible gateway

2 participants