Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ for this exact support, so if you are having problems or if you have question, j
- [简体中文](locales/zh-CN/README.md)
- [繁體中文](locales/zh-TW/README.md)
- ...
</details>
</details>

---

Expand Down
20 changes: 20 additions & 0 deletions packages/types/src/__tests__/opencode-go.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe("opencode-go registry", () => {
"glm-5.2",
"kimi-k2.5",
"kimi-k2.6",
"kimi-k3",
"mimo-v2.5",
"mimo-v2.5-pro",
"deepseek-v4-pro",
Expand Down Expand Up @@ -78,6 +79,25 @@ describe("opencode-go registry", () => {
})

describe("opencodeGoModels registry invariants", () => {
it("configures Kimi K3 with its required max reasoning and automatic caching metadata", () => {
expect(getOpencodeGoModelInfo("kimi-k3")).toMatchObject({
maxTokens: 131_072,
contextWindow: 1_000_000,
supportsImages: true,
supportsPromptCache: true,
supportsMaxTokens: true,
supportsReasoningEffort: ["max"],
requiredReasoningEffort: true,
reasoningEffort: "max",
preserveReasoning: true,
supportsTemperature: false,
inputPrice: 3,
outputPrice: 15,
cacheReadsPrice: 0.3,
})
expect(getOpencodeGoModelInfo("kimi-k3")?.cacheWritesPrice).toBeUndefined()
})

it("every entry has a positive maxTokens and contextWindow", () => {
for (const [id, info] of Object.entries(opencodeGoModels)) {
expect(info.maxTokens).toBeGreaterThan(0)
Expand Down
17 changes: 17 additions & 0 deletions packages/types/src/providers/moonshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,23 @@ export const moonshotModels = {
description:
"Kimi K2.7 Code HighSpeed is the high-speed version of Kimi K2.7 Code with output speed of approximately 180 Tokens/s (up to 260 Tokens/s in short context). Same model architecture, faster output. Context length 256k.",
},
"kimi-k3": {
maxTokens: 131_072,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
supportsMaxTokens: true,
supportsReasoningEffort: ["max"],
requiredReasoningEffort: true,
reasoningEffort: "max",
preserveReasoning: true,
supportsTemperature: false,
inputPrice: 3,
outputPrice: 15,
cacheReadsPrice: 0.3,
description:
"Kimi K3 is Moonshot AI's multimodal reasoning model with a 1M-token context window and up to 128K output tokens.",
},
} as const satisfies Record<string, ModelInfo>

export const MOONSHOT_DEFAULT_TEMPERATURE = 0.6
17 changes: 17 additions & 0 deletions packages/types/src/providers/opencode-go.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,23 @@ export const opencodeGoModels: Record<string, ModelInfo> = {
description:
"Kimi K2.6 is Moonshot AI's native multimodal agentic MoE model with a 256k context window, built for long-horizon coding and tool use. Available via the Opencode Go plan.",
},
"kimi-k3": {
maxTokens: 131_072,
contextWindow: 1_000_000,
supportsImages: true,
supportsPromptCache: true,
supportsMaxTokens: true,
supportsReasoningEffort: ["max"],
requiredReasoningEffort: true,
reasoningEffort: "max",
preserveReasoning: true,
supportsTemperature: false,
inputPrice: 3,
outputPrice: 15,
cacheReadsPrice: 0.3,
description:
"Kimi K3 is Moonshot AI's multimodal reasoning model with a 1M-token context window and up to 128K output tokens. Available via the Opencode Go plan.",
},

// --- Xiaomi MiMo ---
"mimo-v2.5": {
Expand Down
193 changes: 193 additions & 0 deletions src/api/providers/__tests__/moonshot.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,28 @@ describe("MoonshotHandler", () => {
})

describe("getModel", () => {
it("returns Kimi K3 catalog metadata without changing the default model", () => {
const k3Handler = new MoonshotHandler({ ...mockOptions, apiModelId: "kimi-k3" })

expect(moonshotDefaultModelId).not.toBe("kimi-k3")
expect(k3Handler.getModel().info).toMatchObject({
maxTokens: 131_072,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
supportsMaxTokens: true,
supportsReasoningEffort: ["max"],
requiredReasoningEffort: true,
reasoningEffort: "max",
preserveReasoning: true,
supportsTemperature: false,
inputPrice: 3,
outputPrice: 15,
cacheReadsPrice: 0.3,
})
expect("cacheWritesPrice" in k3Handler.getModel().info).toBe(false)
})

it("should return model info for valid model ID", () => {
const model = handler.getModel()
expect(model.id).toBe(mockOptions.apiModelId)
Expand Down Expand Up @@ -144,6 +166,145 @@ describe("MoonshotHandler", () => {
expect(textChunks[0].text).toBe("Test response")
})

it("omits temperature and sends required max reasoning for Kimi K3", async () => {
async function* mockStream() {
yield {
choices: [{ delta: { content: "K3 response" }, finish_reason: null }],
usage: null,
}
}

const mockClient = {
chat: {
completions: {
create: vi.fn().mockResolvedValue(mockStream()),
},
},
}

const k3Handler = new MoonshotHandler({
...mockOptions,
apiModelId: "kimi-k3",
modelTemperature: 0.9,
reasoningEffort: "disable",
enableReasoningEffort: false,
})
;(k3Handler as any).client = mockClient

for await (const _chunk of k3Handler.createMessage(systemPrompt, messages)) {
void _chunk
}

const [requestOptions] = mockClient.chat.completions.create.mock.calls[0]
expect(requestOptions).toMatchObject({
model: "kimi-k3",
reasoning_effort: "max",
max_tokens: 131_072,
})
expect(requestOptions).not.toHaveProperty("temperature")
})

it("sends required max reasoning for Kimi K3 when streaming is disabled", async () => {
const mockClient = {
chat: {
completions: {
create: vi.fn().mockResolvedValue({
choices: [{ message: { content: "K3 response" } }],
usage: { prompt_tokens: 1, completion_tokens: 1 },
}),
},
},
}

const k3Handler = new MoonshotHandler({
...mockOptions,
apiModelId: "kimi-k3",
modelTemperature: 0.9,
reasoningEffort: "disable",
enableReasoningEffort: false,
openAiStreamingEnabled: false,
})
;(k3Handler as any).client = mockClient

for await (const _chunk of k3Handler.createMessage(systemPrompt, messages)) {
void _chunk
}

const [requestOptions] = mockClient.chat.completions.create.mock.calls[0]
expect(requestOptions).toMatchObject({
model: "kimi-k3",
reasoning_effort: "max",
max_tokens: 131_072,
})
expect(requestOptions).not.toHaveProperty("temperature")
})

it("serializes retained Kimi K3 reasoning through the installed OpenAI SDK", async () => {
let requestBody: Record<string, any> | undefined
const fetchMock = vi.fn(async (_input: string | URL | Request, init?: RequestInit) => {
requestBody = JSON.parse(String(init?.body))
return new Response(
'data: {"id":"chatcmpl-test","object":"chat.completion.chunk","created":0,"model":"kimi-k3","choices":[{"index":0,"delta":{"role":"assistant","content":"done"},"finish_reason":null}]}\n\ndata: {"id":"chatcmpl-test","object":"chat.completion.chunk","created":0,"model":"kimi-k3","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":10,"completion_tokens":1,"total_tokens":11}}\n\ndata: [DONE]\n\n',
{ status: 200, headers: { "content-type": "text/event-stream" } },
)
})
vi.stubGlobal("fetch", fetchMock)

try {
const k3Handler = new MoonshotHandler({
apiModelId: "kimi-k3",
moonshotApiKey: "test-key",
moonshotBaseUrl: "https://api.moonshot.ai/v1",
modelTemperature: 0.9,
})
const retainedMessages = [
{ role: "user", content: "Inspect the file" },
{
role: "assistant",
content: [
{ type: "reasoning", text: "I need the file contents first." },
{ type: "tool_use", id: "call_123", name: "read_file", input: { path: "a.ts" } },
],
},
{
role: "user",
content: [{ type: "tool_result", tool_use_id: "call_123", content: "export const a = 1" }],
},
] as Anthropic.Messages.MessageParam[]

for await (const _chunk of k3Handler.createMessage("system", retainedMessages)) {
void _chunk
}

expect(requestBody).toMatchObject({
model: "kimi-k3",
reasoning_effort: "max",
max_tokens: 131_072,
messages: [
{ role: "system", content: "system" },
{ role: "user", content: "Inspect the file" },
{
role: "assistant",
content: "",
reasoning_content: "I need the file contents first.",
tool_calls: [
{
id: "call_123",
type: "function",
function: { name: "read_file", arguments: JSON.stringify({ path: "a.ts" }) },
},
],
},
{ role: "tool", tool_call_id: "call_123", content: "export const a = 1" },
],
})
expect(requestBody).not.toHaveProperty("temperature")
expect(fetchMock).toHaveBeenCalledOnce()
} finally {
vi.unstubAllGlobals()
}
})

it("should include usage information", async () => {
async function* mockStream() {
yield {
Expand Down Expand Up @@ -234,6 +395,38 @@ describe("MoonshotHandler", () => {
{},
)
})

it("omits temperature and sends required max reasoning for Kimi K3", async () => {
const mockClient = {
chat: {
completions: {
create: vi.fn().mockResolvedValue({
choices: [{ message: { content: "K3 completion" } }],
}),
},
},
}

const k3Handler = new MoonshotHandler({
...mockOptions,
apiModelId: "kimi-k3",
modelTemperature: 0.9,
reasoningEffort: "disable",
enableReasoningEffort: false,
})
;(k3Handler as any).client = mockClient

const result = await k3Handler.completePrompt("Test prompt")

expect(result).toBe("K3 completion")
const [requestOptions] = mockClient.chat.completions.create.mock.calls[0]
expect(requestOptions).toMatchObject({
model: "kimi-k3",
reasoning_effort: "max",
max_tokens: 131_072,
})
expect(requestOptions).not.toHaveProperty("temperature")
})
})

describe("processUsageMetrics", () => {
Expand Down
52 changes: 52 additions & 0 deletions src/api/providers/__tests__/opencode-go.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ vitest.mock("../fetchers/modelCache", () => ({
"glm-5.1": { ...opencodeGoModels["glm-5.1"] },
// Anthropic-format model used to exercise the /v1/messages path.
"qwen3.7-max": { ...opencodeGoModels["qwen3.7-max"] },
"kimi-k3": { ...opencodeGoModels["kimi-k3"] },
})
}),
getModelsFromCache: vitest.fn().mockReturnValue(undefined),
Expand Down Expand Up @@ -347,6 +348,39 @@ describe("OpencodeGoHandler", () => {
expect(callArgs.messages.filter((m) => m.role === "user")).toHaveLength(1)
})

it("sends valid Kimi K3 streaming parameters and preserves reasoning history", async () => {
const handler = new OpencodeGoHandler({
...mockOptions,
opencodeGoModelId: "kimi-k3",
modelTemperature: 0.9,
reasoningEffort: "disable",
enableReasoningEffort: false,
})
const messages = [
{
role: "assistant" as const,
content: [
{ type: "reasoning", text: "prior thought" },
{ type: "text" as const, text: "prior answer" },
] as any,
},
{ role: "user" as const, content: "continue" },
]

for await (const _chunk of handler.createMessage("sys", messages)) {
void _chunk
}

const body = mockCreate.mock.calls[0][0] as any
expect(body.model).toBe("kimi-k3")
expect(body.temperature).toBeUndefined()
expect(body.max_completion_tokens).toBe(131_072)
expect(body.reasoning_effort).toBe("max")
expect(body.messages).toContainEqual(
expect.objectContaining({ role: "assistant", reasoning_content: "prior thought" }),
)
})

it("emits a usage chunk with zeroed tokens when the stream reports no usage", async () => {
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
Expand Down Expand Up @@ -382,6 +416,24 @@ describe("OpencodeGoHandler", () => {
})

describe("completePrompt", () => {
it("omits temperature and sends max reasoning for Kimi K3", async () => {
mockCreate.mockResolvedValue({ choices: [{ message: { content: "the answer" } }] })
const handler = new OpencodeGoHandler({
...mockOptions,
opencodeGoModelId: "kimi-k3",
modelTemperature: 0.9,
reasoningEffort: "disable",
enableReasoningEffort: false,
})

await handler.completePrompt("ping")

const body = mockCreate.mock.calls[0][0] as any
expect(body.temperature).toBeUndefined()
expect(body.max_completion_tokens).toBe(131_072)
expect(body.reasoning_effort).toBe("max")
})

it("returns the message content for a non-streaming completion", async () => {
mockCreate.mockResolvedValue({ choices: [{ message: { content: "the answer" } }] })
const handler = new OpencodeGoHandler(mockOptions)
Expand Down
Loading
Loading