Skip to content

Commit 77173c2

Browse files
committed
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 1ee4c39 commit 77173c2

5 files changed

Lines changed: 53 additions & 8 deletions

File tree

‎packages/types/src/providers/kenari.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import type { ModelInfo } from "../model.js"
66
// The full model list (and metadata) is fetched dynamically from
77
// `https://kenari.id/v1/models`, so models can be switched on the fly.
88
// 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+
914
export const kenariDefaultModelId = "glm-5-2"
1015

1116
export const kenariDefaultModelInfo: ModelInfo = {

‎src/api/providers/__tests__/kenari.spec.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,25 @@ describe("KenariHandler", () => {
233233
expect(chunks).toEqual([{ type: "text", text: "Hi" }])
234234
})
235235

236+
it("emits reasoning from the OpenRouter-style `reasoning` field when reasoning_content is absent", async () => {
237+
mockCreate.mockImplementation(async () => ({
238+
[Symbol.asyncIterator]: async function* () {
239+
yield {
240+
choices: [{ delta: { content: "Hi", reasoning: "thinking…" }, index: 0 }],
241+
usage: null,
242+
}
243+
},
244+
}))
245+
246+
const handler = new KenariHandler(mockOptions)
247+
const chunks = []
248+
for await (const chunk of handler.createMessage("sys", [{ role: "user", content: "Hi" }])) {
249+
chunks.push(chunk)
250+
}
251+
252+
expect(chunks).toContainEqual({ type: "reasoning", text: "thinking…" })
253+
})
254+
236255
it("omits temperature for models that do not support it", async () => {
237256
vitest.mocked(getModels).mockResolvedValueOnce({
238257
"openai/o3-mini": {

‎src/api/providers/fetchers/__tests__/kenari.spec.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ describe("Kenari Fetchers", () => {
9090
const models = await getKenariModels("k")
9191

9292
expect(Object.keys(models)).toEqual(["valid-model"])
93+
// The surviving entry keeps its mapped metadata (context_window -> contextWindow),
94+
// so a field-mapping regression in this fallback branch is caught here.
95+
expect(models["valid-model"].contextWindow).toBe(50000)
9396
// Two warns: one for the outer schema mismatch, one for the invalid item
9497
expect(warnSpy).toHaveBeenCalledTimes(2)
9598
expect(warnSpy.mock.calls[0][0]).toContain("did not match expected schema")
@@ -121,6 +124,18 @@ describe("Kenari Fetchers", () => {
121124
expect(info.description).toBe("Model X")
122125
})
123126

127+
it("prefers context_window over context_length and max_output_tokens over max_tokens", () => {
128+
const info = parseKenariModel({
129+
id: "dual",
130+
context_window: 111,
131+
context_length: 222,
132+
max_output_tokens: 33,
133+
max_tokens: 44,
134+
})
135+
expect(info.contextWindow).toBe(111)
136+
expect(info.maxTokens).toBe(33)
137+
})
138+
124139
it("treats a model with no cache pricing as not cache-capable", () => {
125140
const info = parseKenariModel({ id: "x", context_window: 100000, max_tokens: 8000 })
126141
expect(info.supportsPromptCache).toBe(false)

‎src/api/providers/fetchers/kenari.ts‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ import axios from "axios"
22
import { z } from "zod"
33

44
import type { ModelInfo } from "@roo-code/types"
5-
import { kenariDefaultModelInfo } from "@roo-code/types"
6-
7-
const KENARI_BASE_URL = "https://kenari.id/v1"
5+
import { kenariDefaultModelInfo, KENARI_BASE_URL } from "@roo-code/types"
86

97
// The Kenari `/models` endpoint follows the OpenAI `/models` shape and is
108
// public (no key required). The `id` is the only guaranteed field; metadata is

‎src/api/providers/kenari.ts‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { Anthropic } from "@anthropic-ai/sdk"
22
import OpenAI from "openai"
33

4-
import { kenariDefaultModelId, kenariDefaultModelInfo, KENARI_DEFAULT_TEMPERATURE } from "@roo-code/types"
4+
import {
5+
kenariDefaultModelId,
6+
kenariDefaultModelInfo,
7+
KENARI_DEFAULT_TEMPERATURE,
8+
KENARI_BASE_URL,
9+
} from "@roo-code/types"
510

611
import { ApiHandlerOptions } from "../../shared/api"
712

@@ -10,6 +15,7 @@ import { convertToOpenAiMessages } from "../transform/openai-format"
1015

1116
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
1217
import { RouterProvider } from "./router-provider"
18+
import { extractReasoningFromDelta } from "./utils/extract-reasoning"
1319

1420
/**
1521
* API handler for Kenari, an Indonesian OpenAI-compatible AI gateway billed
@@ -32,7 +38,7 @@ export class KenariHandler extends RouterProvider implements SingleCompletionHan
3238
super({
3339
options,
3440
name: "kenari",
35-
baseURL: "https://kenari.id/v1",
41+
baseURL: KENARI_BASE_URL,
3642
apiKey: options.kenariApiKey,
3743
modelId: options.kenariModelId,
3844
defaultModelId: kenariDefaultModelId,
@@ -79,9 +85,11 @@ export class KenariHandler extends RouterProvider implements SingleCompletionHan
7985
yield { type: "text", text: delta.content }
8086
}
8187

82-
// Several Kenari models (GLM, DeepSeek) stream reasoning via this field.
83-
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
84-
yield { type: "reasoning", text: delta.reasoning_content as string }
88+
// Several Kenari models (GLM, DeepSeek) stream reasoning via reasoning_content,
89+
// with an OpenRouter-style `reasoning` fallback; the shared helper handles both.
90+
const reasoningText = extractReasoningFromDelta(delta)
91+
if (reasoningText) {
92+
yield { type: "reasoning", text: reasoningText }
8593
}
8694

8795
// Emit raw tool call chunks - NativeToolCallParser handles state management.

0 commit comments

Comments
 (0)