Skip to content

Commit e482d11

Browse files
authored
feat(kenari): add Kenari as a first-class provider (#793)
* feat(kenari): add Kenari as a first-class provider * test(kenari): cover provider registration arms and stream branches * test(kenari): flip remaining partial branches, drop dead reasoning fallback * refactor(kenari): address review feedback - 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.
1 parent 7d0879b commit e482d11

47 files changed

Lines changed: 1340 additions & 3 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/types/src/global-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ export const SECRET_STATE_KEYS = [
319319
"friendliApiKey",
320320
"vercelAiGatewayApiKey",
321321
"opencodeGoApiKey",
322+
"kenariApiKey",
322323
"basetenApiKey",
323324
] as const
324325

packages/types/src/provider-settings.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export const dynamicProviders = [
4747
"poe",
4848
"deepseek",
4949
"opencode-go",
50+
"kenari",
5051
] as const
5152

5253
export type DynamicProvider = (typeof dynamicProviders)[number]
@@ -413,6 +414,11 @@ const opencodeGoSchema = baseProviderSettingsSchema.extend({
413414
opencodeGoModelId: z.string().optional(),
414415
})
415416

417+
const kenariSchema = baseProviderSettingsSchema.extend({
418+
kenariApiKey: z.string().optional(),
419+
kenariModelId: z.string().optional(),
420+
})
421+
416422
const zooGatewaySchema = baseProviderSettingsSchema.extend({
417423
zooSessionToken: z.string().optional(),
418424
zooGatewayModelId: z.string().optional(),
@@ -459,6 +465,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
459465
qwenCodeSchema.merge(z.object({ apiProvider: z.literal("qwen-code") })),
460466
vercelAiGatewaySchema.merge(z.object({ apiProvider: z.literal("vercel-ai-gateway") })),
461467
opencodeGoSchema.merge(z.object({ apiProvider: z.literal("opencode-go") })),
468+
kenariSchema.merge(z.object({ apiProvider: z.literal("kenari") })),
462469
zooGatewaySchema.merge(z.object({ apiProvider: z.literal("zoo-gateway") })),
463470
defaultSchema,
464471
])
@@ -496,6 +503,7 @@ export const providerSettingsSchema = z.object({
496503
...qwenCodeSchema.shape,
497504
...vercelAiGatewaySchema.shape,
498505
...opencodeGoSchema.shape,
506+
...kenariSchema.shape,
499507
...zooGatewaySchema.shape,
500508
...codebaseIndexProviderSchema.shape,
501509
})
@@ -528,6 +536,7 @@ export const modelIdKeys = [
528536
"litellmModelId",
529537
"vercelAiGatewayModelId",
530538
"opencodeGoModelId",
539+
"kenariModelId",
531540
"zooGatewayModelId",
532541
] as const satisfies readonly (keyof ProviderSettings)[]
533542

@@ -576,6 +585,7 @@ export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
576585
friendli: "apiModelId",
577586
"vercel-ai-gateway": "vercelAiGatewayModelId",
578587
"opencode-go": "opencodeGoModelId",
588+
kenari: "kenariModelId",
579589
"zoo-gateway": "zooGatewayModelId",
580590
}
581591

@@ -715,6 +725,7 @@ export const MODELS_BY_PROVIDER: Record<
715725
unbound: { id: "unbound", label: "Unbound", models: [] },
716726
"vercel-ai-gateway": { id: "vercel-ai-gateway", label: "Vercel AI Gateway", models: [] },
717727
"opencode-go": { id: "opencode-go", label: "Opencode Go", models: [] },
728+
kenari: { id: "kenari", label: "Kenari", models: [] },
718729
"zoo-gateway": { id: "zoo-gateway", label: "Zoo Gateway", models: [] },
719730

720731
// Local providers; models discovered from localhost endpoints.

packages/types/src/providers/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export * from "./vscode-llm.js"
2424
export * from "./xai.js"
2525
export * from "./vercel-ai-gateway.js"
2626
export * from "./opencode-go.js"
27+
export * from "./kenari.js"
2728
export * from "./zai.js"
2829
export * from "./minimax.js"
2930
export * from "./mimo.js"
@@ -51,6 +52,7 @@ import { vscodeLlmDefaultModelId } from "./vscode-llm.js"
5152
import { xaiDefaultModelId } from "./xai.js"
5253
import { vercelAiGatewayDefaultModelId } from "./vercel-ai-gateway.js"
5354
import { opencodeGoDefaultModelId } from "./opencode-go.js"
55+
import { kenariDefaultModelId } from "./kenari.js"
5456
import { internationalZAiDefaultModelId, mainlandZAiDefaultModelId } from "./zai.js"
5557
import { minimaxDefaultModelId } from "./minimax.js"
5658
import { mimoDefaultModelId } from "./mimo.js"
@@ -125,6 +127,8 @@ export function getProviderDefaultModelId(
125127
return vercelAiGatewayDefaultModelId
126128
case "opencode-go":
127129
return opencodeGoDefaultModelId
130+
case "kenari":
131+
return kenariDefaultModelId
128132
case "zoo-gateway":
129133
return zooGatewayDefaultModelId
130134
case "anthropic":
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { ModelInfo } from "../model.js"
2+
3+
// Kenari: Indonesian OpenAI-compatible AI gateway billed in Rupiah (IDR).
4+
// https://kenari.id/docs · base URL: https://kenari.id/v1
5+
//
6+
// The full model list (and metadata) is fetched dynamically from
7+
// `https://kenari.id/v1/models`, so models can be switched on the fly.
8+
// The values below are only a fallback used before the live list resolves.
9+
10+
// Single source of truth for the gateway base URL, shared by the handler and
11+
// the model fetcher so the endpoint is defined in exactly one place.
12+
export const KENARI_BASE_URL = "https://kenari.id/v1"
13+
14+
export const kenariDefaultModelId = "glm-5-2"
15+
16+
export const kenariDefaultModelInfo: ModelInfo = {
17+
maxTokens: 32_768,
18+
contextWindow: 1_048_576,
19+
supportsImages: false,
20+
supportsPromptCache: false,
21+
// Pricing is intentionally omitted: Kenari bills in IDR (micro-rupiah per 1M tokens),
22+
// which cannot be rendered in the USD price fields without a misleading conversion.
23+
description: "Kenari model. Available models and metadata are resolved dynamically from /v1/models.",
24+
}
25+
26+
export const KENARI_DEFAULT_TEMPERATURE = 0

src/api/__tests__/index.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// npx vitest run src/api/__tests__/index.spec.ts
2+
3+
// Mock vscode first to avoid import errors
4+
vitest.mock("vscode", () => ({
5+
workspace: {
6+
getConfiguration: () => ({
7+
get: (_key: string, defaultValue?: unknown) => defaultValue,
8+
}),
9+
},
10+
}))
11+
12+
import type { ProviderSettings } from "@roo-code/types"
13+
14+
import { buildApiHandler } from "../index"
15+
import { KenariHandler } from "../providers/kenari"
16+
17+
describe("buildApiHandler", () => {
18+
it("returns a KenariHandler for the kenari provider", () => {
19+
const configuration: ProviderSettings = {
20+
apiProvider: "kenari",
21+
kenariApiKey: "test-key",
22+
kenariModelId: "glm-5-2",
23+
}
24+
25+
const handler = buildApiHandler(configuration)
26+
27+
expect(handler).toBeInstanceOf(KenariHandler)
28+
})
29+
})

src/api/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
FriendliHandler,
3535
VercelAiGatewayHandler,
3636
OpencodeGoHandler,
37+
KenariHandler,
3738
ZooGatewayHandler,
3839
MiniMaxHandler,
3940
MimoHandler,
@@ -196,6 +197,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
196197
return new VercelAiGatewayHandler(options)
197198
case "opencode-go":
198199
return new OpencodeGoHandler(options)
200+
case "kenari":
201+
return new KenariHandler(options)
199202
case "zoo-gateway":
200203
return new ZooGatewayHandler(options)
201204
case "minimax":

0 commit comments

Comments
 (0)