From 20a3090d959a875650ef00e7a7edc786a61ffbc3 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Fri, 8 May 2026 00:28:55 -0700 Subject: [PATCH 1/4] Enable OpenCode Zen for minimax + kimi; add buffbench agents Wires the OpenCode Zen scaffold into the chat-completions router for opencode/minimax-m2.7 and opencode/kimi-k2.6, replacing the disabled rejection. Adds two buffbench agents (base2-free-opencode-kimi, base2-free-opencode-minimax) and points buffbench main.ts at the minimax variant. Co-Authored-By: Claude Opus 4.7 (1M context) --- agents/base2/base2-free-opencode-kimi.ts | 14 + agents/base2/base2-free-opencode-minimax.ts | 14 + evals/buffbench/main.ts | 2 +- .../completions/__tests__/completions.test.ts | 54 +++- web/src/app/api/v1/chat/completions/_post.ts | 254 +++++++----------- 5 files changed, 169 insertions(+), 169 deletions(-) create mode 100644 agents/base2/base2-free-opencode-kimi.ts create mode 100644 agents/base2/base2-free-opencode-minimax.ts diff --git a/agents/base2/base2-free-opencode-kimi.ts b/agents/base2/base2-free-opencode-kimi.ts new file mode 100644 index 000000000..059dce168 --- /dev/null +++ b/agents/base2/base2-free-opencode-kimi.ts @@ -0,0 +1,14 @@ +import { openCodeZenModels } from '@codebuff/common/constants/model-config' + +import { createBase2 } from './base2' + +const definition = { + ...createBase2('free', { + noAskUser: true, + model: openCodeZenModels.opencode_kimi_k2_6, + }), + id: 'base2-free-opencode-kimi', + displayName: 'Buffy the Kimi (OpenCode) Free Orchestrator', +} + +export default definition diff --git a/agents/base2/base2-free-opencode-minimax.ts b/agents/base2/base2-free-opencode-minimax.ts new file mode 100644 index 000000000..290a47e19 --- /dev/null +++ b/agents/base2/base2-free-opencode-minimax.ts @@ -0,0 +1,14 @@ +import { openCodeZenModels } from '@codebuff/common/constants/model-config' + +import { createBase2 } from './base2' + +const definition = { + ...createBase2('free', { + noAskUser: true, + model: openCodeZenModels.opencode_minimax_m2_7, + }), + id: 'base2-free-opencode-minimax', + displayName: 'Buffy the MiniMax (OpenCode) Free Orchestrator', +} + +export default definition diff --git a/evals/buffbench/main.ts b/evals/buffbench/main.ts index 0173a09fb..515198ec1 100644 --- a/evals/buffbench/main.ts +++ b/evals/buffbench/main.ts @@ -11,7 +11,7 @@ async function main() { // Use 'external:opencode' for OpenCode CLI await runBuffBench({ evalDataPaths: [path.join(__dirname, 'eval-codebuff.json')], - agents: ['base2-free-evals'], + agents: ['base2-free-opencode-minimax'], taskConcurrency: 6, saveTraces, }) diff --git a/web/src/app/api/v1/chat/completions/__tests__/completions.test.ts b/web/src/app/api/v1/chat/completions/__tests__/completions.test.ts index 0fdf0c2e2..596da9313 100644 --- a/web/src/app/api/v1/chat/completions/__tests__/completions.test.ts +++ b/web/src/app/api/v1/chat/completions/__tests__/completions.test.ts @@ -854,15 +854,44 @@ describe('/api/v1/chat/completions POST endpoint', () => { ) it( - 'rejects OpenCode Zen models while the Zen integration is disabled', + 'routes OpenCode Zen models to the direct OpenCode Zen provider', async () => { - const fetchViaOpenCodeZen = mock( - async (_url: string | URL | Request, _init?: RequestInit) => { - throw new Error('OpenCode Zen should not be called') - }, - ) as unknown as typeof globalThis.fetch + const expectedUpstreamModel: Record = { + 'opencode/minimax-m2.7': 'minimax-m2.7', + 'opencode/kimi-k2.6': 'kimi-k2.6', + } for (const codebuffModel of Object.values(openCodeZenModels)) { + const fetchedBodies: Record[] = [] + const fetchedUrls: string[] = [] + const fetchViaOpenCodeZen = mock( + async (url: string | URL | Request, init?: RequestInit) => { + if (String(url).startsWith('https://api.ipinfo.io/lookup/')) { + return Response.json({}) + } + + fetchedUrls.push(String(url)) + fetchedBodies.push(JSON.parse(init?.body as string)) + return new Response( + JSON.stringify({ + id: 'test-id', + model: expectedUpstreamModel[codebuffModel], + choices: [{ message: { content: 'test response' } }], + usage: { + prompt_tokens: 10, + prompt_tokens_details: { cached_tokens: 4 }, + completion_tokens: 20, + total_tokens: 30, + }, + }), + { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }, + ) + }, + ) as unknown as typeof globalThis.fetch + const req = new NextRequest( 'http://localhost:3000/api/v1/chat/completions', { @@ -921,13 +950,14 @@ describe('/api/v1/chat/completions POST endpoint', () => { }) const body = await response.json() - expect(response.status).toBe(400) - expect(body).toEqual({ - error: 'opencode_zen_disabled', - message: 'OpenCode Zen models are currently disabled.', - }) + expect(response.status).toBe(200) + expect(fetchedUrls[0]).toBe( + 'https://opencode.ai/zen/v1/chat/completions', + ) + expect(fetchedBodies[0].model).toBe(expectedUpstreamModel[codebuffModel]) + expect(body.model).toBe(codebuffModel) + expect(body.provider).toBe('OpenCode Zen') } - expect(fetchViaOpenCodeZen).not.toHaveBeenCalled() }, FETCH_PATH_TEST_TIMEOUT_MS, ) diff --git a/web/src/app/api/v1/chat/completions/_post.ts b/web/src/app/api/v1/chat/completions/_post.ts index 317a7d5f4..54a7a0638 100644 --- a/web/src/app/api/v1/chat/completions/_post.ts +++ b/web/src/app/api/v1/chat/completions/_post.ts @@ -55,7 +55,12 @@ import { handleDeepSeekStream, isDeepSeekModel, } from '@/llm-api/deepseek' -import { isOpenCodeZenModel } from '@/llm-api/opencode-zen' +import { + OpenCodeZenError, + handleOpenCodeZenNonStream, + handleOpenCodeZenStream, + isOpenCodeZenModel, +} from '@/llm-api/opencode-zen' import { SiliconFlowError, handleSiliconFlowNonStream, @@ -378,25 +383,6 @@ export async function postChatCompletions(params: { ) } - if (isOpenCodeZenModel(typedBody.model)) { - trackEvent({ - event: AnalyticsEvent.CHAT_COMPLETIONS_VALIDATION_ERROR, - userId, - properties: { - error: 'opencode_zen_disabled', - model: typedBody.model, - }, - logger, - }) - return NextResponse.json( - { - error: 'opencode_zen_disabled', - message: 'OpenCode Zen models are currently disabled.', - }, - { status: 400 }, - ) - } - // Free-mode requests must use an allowlisted agent+model combination. // Without this gate, an attacker on a brand-new unpaid account can set // cost_mode='free' to bypass both the paid-account check and the balance @@ -629,75 +615,49 @@ export async function postChatCompletions(params: { if (bodyStream) { // Streaming request — route supported models to direct providers. const useSiliconFlow = false // isSiliconFlowModel(typedBody.model) - const useCanopyWave = isCanopyWaveModel(typedBody.model) - const useDeepSeek = !useCanopyWave && isDeepSeekModel(typedBody.model) + const useOpenCodeZen = isOpenCodeZenModel(typedBody.model) + const useCanopyWave = + !useOpenCodeZen && isCanopyWaveModel(typedBody.model) + const useDeepSeek = + !useOpenCodeZen && + !useCanopyWave && + isDeepSeekModel(typedBody.model) const useFireworks = - !useCanopyWave && !useDeepSeek && isFireworksModel(typedBody.model) + !useOpenCodeZen && + !useCanopyWave && + !useDeepSeek && + isFireworksModel(typedBody.model) const useOpenAIDirect = + !useOpenCodeZen && !useCanopyWave && !useDeepSeek && !useFireworks && isOpenAIDirectModel(typedBody.model) + const baseArgs = { + body: typedBody, + userId, + stripeCustomerId, + agentId, + fetch, + logger, + insertMessageBigquery, + } const stream = useSiliconFlow - ? await handleSiliconFlowStream({ - body: typedBody, - userId, - stripeCustomerId, - agentId, - fetch, - logger, - insertMessageBigquery, - }) - : useCanopyWave - ? await handleCanopyWaveStream({ - body: typedBody, - userId, - stripeCustomerId, - agentId, - fetch, - logger, - insertMessageBigquery, - }) - : useDeepSeek - ? await handleDeepSeekStream({ - body: typedBody, - userId, - stripeCustomerId, - agentId, - fetch, - logger, - insertMessageBigquery, - }) - : useFireworks - ? await handleFireworksStream({ - body: typedBody, - userId, - stripeCustomerId, - agentId, - fetch, - logger, - insertMessageBigquery, - }) - : useOpenAIDirect - ? await handleOpenAIStream({ - body: typedBody, - userId, - stripeCustomerId, - agentId, - fetch, - logger, - insertMessageBigquery, - }) - : await handleOpenRouterStream({ - body: typedBody, - userId, - stripeCustomerId, - agentId, - openrouterApiKey, - fetch, - logger, - insertMessageBigquery, - }) + ? await handleSiliconFlowStream(baseArgs) + : useOpenCodeZen + ? await handleOpenCodeZenStream(baseArgs) + : useCanopyWave + ? await handleCanopyWaveStream(baseArgs) + : useDeepSeek + ? await handleDeepSeekStream(baseArgs) + : useFireworks + ? await handleFireworksStream(baseArgs) + : useOpenAIDirect + ? await handleOpenAIStream(baseArgs) + : await handleOpenRouterStream({ + ...baseArgs, + openrouterApiKey, + }) trackEvent({ event: AnalyticsEvent.CHAT_COMPLETIONS_STREAM_STARTED, @@ -718,79 +678,50 @@ export async function postChatCompletions(params: { }, }) } else { - // Non-streaming request — route to SiliconFlow/CanopyWave/Fireworks for supported models + // Non-streaming request — route to direct providers for supported models const model = typedBody.model const useSiliconFlow = false // isSiliconFlowModel(model) - const useCanopyWave = isCanopyWaveModel(model) - const useDeepSeek = !useCanopyWave && isDeepSeekModel(model) + const useOpenCodeZen = isOpenCodeZenModel(model) + const useCanopyWave = !useOpenCodeZen && isCanopyWaveModel(model) + const useDeepSeek = + !useOpenCodeZen && !useCanopyWave && isDeepSeekModel(model) const useFireworks = - !useCanopyWave && !useDeepSeek && isFireworksModel(model) + !useOpenCodeZen && + !useCanopyWave && + !useDeepSeek && + isFireworksModel(model) const shouldUseOpenAIEndpoint = + !useOpenCodeZen && !useCanopyWave && !useDeepSeek && !useFireworks && isOpenAIDirectModel(model) + const baseArgs = { + body: typedBody, + userId, + stripeCustomerId, + agentId, + fetch, + logger, + insertMessageBigquery, + } const nonStreamRequest = useSiliconFlow - ? handleSiliconFlowNonStream({ - body: typedBody, - userId, - stripeCustomerId, - agentId, - fetch, - logger, - insertMessageBigquery, - }) - : useCanopyWave - ? handleCanopyWaveNonStream({ - body: typedBody, - userId, - stripeCustomerId, - agentId, - fetch, - logger, - insertMessageBigquery, - }) - : useDeepSeek - ? handleDeepSeekNonStream({ - body: typedBody, - userId, - stripeCustomerId, - agentId, - fetch, - logger, - insertMessageBigquery, - }) - : useFireworks - ? handleFireworksNonStream({ - body: typedBody, - userId, - stripeCustomerId, - agentId, - fetch, - logger, - insertMessageBigquery, - }) - : shouldUseOpenAIEndpoint - ? handleOpenAINonStream({ - body: typedBody, - userId, - stripeCustomerId, - agentId, - fetch, - logger, - insertMessageBigquery, - }) - : handleOpenRouterNonStream({ - body: typedBody, - userId, - stripeCustomerId, - agentId, - openrouterApiKey, - fetch, - logger, - insertMessageBigquery, - }) + ? handleSiliconFlowNonStream(baseArgs) + : useOpenCodeZen + ? handleOpenCodeZenNonStream(baseArgs) + : useCanopyWave + ? handleCanopyWaveNonStream(baseArgs) + : useDeepSeek + ? handleDeepSeekNonStream(baseArgs) + : useFireworks + ? handleFireworksNonStream(baseArgs) + : shouldUseOpenAIEndpoint + ? handleOpenAINonStream(baseArgs) + : handleOpenRouterNonStream({ + ...baseArgs, + openrouterApiKey, + }) const result = await nonStreamRequest trackEvent({ @@ -831,20 +762,26 @@ export async function postChatCompletions(params: { if (error instanceof OpenAIError) { openaiError = error } + let opencodeZenError: OpenCodeZenError | undefined + if (error instanceof OpenCodeZenError) { + opencodeZenError = error + } // Log detailed error information for debugging const errorDetails = openrouterError?.toJSON() const providerLabel = siliconflowError ? 'SiliconFlow' - : canopywaveError - ? 'CanopyWave' - : deepseekError - ? 'DeepSeek' - : fireworksError - ? 'Fireworks' - : openaiError - ? 'OpenAI' - : 'OpenRouter' + : opencodeZenError + ? 'OpenCode Zen' + : canopywaveError + ? 'CanopyWave' + : deepseekError + ? 'DeepSeek' + : fireworksError + ? 'Fireworks' + : openaiError + ? 'OpenAI' + : 'OpenRouter' logger.error( { error: getErrorObject(error), @@ -864,7 +801,8 @@ export async function postChatCompletions(params: { canopywaveError ?? deepseekError ?? siliconflowError ?? - openaiError + openaiError ?? + opencodeZenError )?.statusCode, providerStatusText: ( openrouterError ?? @@ -872,7 +810,8 @@ export async function postChatCompletions(params: { canopywaveError ?? deepseekError ?? siliconflowError ?? - openaiError + openaiError ?? + opencodeZenError )?.statusText, openrouterErrorCode: errorDetails?.error?.code, openrouterErrorType: errorDetails?.error?.type, @@ -913,6 +852,9 @@ export async function postChatCompletions(params: { if (error instanceof OpenAIError) { return NextResponse.json(error.toJSON(), { status: error.statusCode }) } + if (error instanceof OpenCodeZenError) { + return NextResponse.json(error.toJSON(), { status: error.statusCode }) + } return NextResponse.json( { error: 'Failed to process request' }, From 9b2e5f06d5fd12376df0fb913ce5b3e30394982d Mon Sep 17 00:00:00 2001 From: James Grugett Date: Fri, 8 May 2026 00:31:41 -0700 Subject: [PATCH 2/4] buffbench: switch main.ts to base2-free-opencode-kimi Co-Authored-By: Claude Opus 4.7 (1M context) --- evals/buffbench/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evals/buffbench/main.ts b/evals/buffbench/main.ts index 515198ec1..50aeb97ec 100644 --- a/evals/buffbench/main.ts +++ b/evals/buffbench/main.ts @@ -11,7 +11,7 @@ async function main() { // Use 'external:opencode' for OpenCode CLI await runBuffBench({ evalDataPaths: [path.join(__dirname, 'eval-codebuff.json')], - agents: ['base2-free-opencode-minimax'], + agents: ['base2-free-opencode-kimi'], taskConcurrency: 6, saveTraces, }) From 56b80e2bb89971565266f527ee6ee547a3252aa7 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Fri, 8 May 2026 00:44:18 -0700 Subject: [PATCH 3/4] switch to minimax --- evals/buffbench/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evals/buffbench/main.ts b/evals/buffbench/main.ts index 50aeb97ec..515198ec1 100644 --- a/evals/buffbench/main.ts +++ b/evals/buffbench/main.ts @@ -11,7 +11,7 @@ async function main() { // Use 'external:opencode' for OpenCode CLI await runBuffBench({ evalDataPaths: [path.join(__dirname, 'eval-codebuff.json')], - agents: ['base2-free-opencode-kimi'], + agents: ['base2-free-opencode-minimax'], taskConcurrency: 6, saveTraces, }) From f55841f238fc63615ddc69ee3086675fe46eb713 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Fri, 8 May 2026 00:45:14 -0700 Subject: [PATCH 4/4] switch to normal base2-free-kimi --- evals/buffbench/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evals/buffbench/main.ts b/evals/buffbench/main.ts index 515198ec1..ac3f1c956 100644 --- a/evals/buffbench/main.ts +++ b/evals/buffbench/main.ts @@ -11,7 +11,7 @@ async function main() { // Use 'external:opencode' for OpenCode CLI await runBuffBench({ evalDataPaths: [path.join(__dirname, 'eval-codebuff.json')], - agents: ['base2-free-opencode-minimax'], + agents: ['base2-free-kimi'], taskConcurrency: 6, saveTraces, })