Skip to content

Commit b58a4ee

Browse files
authored
Add disabled OpenCode Zen provider scaffold (#623)
1 parent c1f82fa commit b58a4ee

7 files changed

Lines changed: 908 additions & 1 deletion

File tree

.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ ANTHROPIC_API_KEY=dummy_anthropic_key
66
FIREWORKS_API_KEY=dummy_fireworks_key
77
CANOPYWAVE_API_KEY=dummy_canopywave_key
88
SILICONFLOW_API_KEY=dummy_siliconflow_key
9+
OPENCODE_API_KEY=dummy_opencode_key
910

1011
# Database & Server
1112
DATABASE_URL=postgresql://manicode_user_local:secretpassword_local@localhost:5432/manicode_db_local
@@ -43,4 +44,4 @@ NEXT_PUBLIC_POSTHOG_API_KEY=phc_dummy_posthog_key
4344
NEXT_PUBLIC_POSTHOG_HOST_URL=https://us.i.posthog.com
4445
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_dummy_publishable
4546
NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL=https://billing.stripe.com/p/login/test_dummy
46-
NEXT_PUBLIC_WEB_PORT=3000
47+
NEXT_PUBLIC_WEB_PORT=3000

common/src/constants/model-config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ export const openrouterModels = {
5353
export type openrouterModel =
5454
(typeof openrouterModels)[keyof typeof openrouterModels]
5555

56+
export const openCodeZenModels = {
57+
opencode_minimax_m2_7: 'opencode/minimax-m2.7',
58+
opencode_kimi_k2_6: 'opencode/kimi-k2.6',
59+
} as const
60+
export type OpenCodeZenModel =
61+
(typeof openCodeZenModels)[keyof typeof openCodeZenModels]
62+
5663
export const deepseekModels = {
5764
deepseekChat: 'deepseek-chat',
5865
deepseekReasoner: 'deepseek-reasoner',

packages/internal/src/env-schema.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const serverEnvSchema = clientEnvSchema.extend({
1010
CANOPYWAVE_API_KEY: z.string().min(1).optional(),
1111
DEEPSEEK_API_KEY: z.string().min(1).optional(),
1212
SILICONFLOW_API_KEY: z.string().min(1).optional(),
13+
OPENCODE_API_KEY: z.string().min(1).optional(),
1314
LINKUP_API_KEY: z.string().min(1),
1415
CONTEXT7_API_KEY: z.string().optional(),
1516
GRAVITY_API_KEY: z.string().min(1),
@@ -90,6 +91,7 @@ export const serverProcessEnv: ServerInput = {
9091
CANOPYWAVE_API_KEY: process.env.CANOPYWAVE_API_KEY,
9192
DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY,
9293
SILICONFLOW_API_KEY: process.env.SILICONFLOW_API_KEY,
94+
OPENCODE_API_KEY: process.env.OPENCODE_API_KEY,
9395
LINKUP_API_KEY: process.env.LINKUP_API_KEY,
9496
CONTEXT7_API_KEY: process.env.CONTEXT7_API_KEY,
9597
GRAVITY_API_KEY: process.env.GRAVITY_API_KEY,

packages/internal/src/env.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ if (isCI) {
1919
ensureEnvDefault('FIREWORKS_API_KEY', 'test')
2020
ensureEnvDefault('CANOPYWAVE_API_KEY', 'test')
2121
ensureEnvDefault('DEEPSEEK_API_KEY', 'test')
22+
ensureEnvDefault('OPENCODE_API_KEY', 'test')
2223
ensureEnvDefault('LINKUP_API_KEY', 'test')
2324
ensureEnvDefault('GRAVITY_API_KEY', 'test')
2425
ensureEnvDefault('IPINFO_TOKEN', 'test')

web/src/app/api/v1/chat/completions/__tests__/completions.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
FREEBUFF_GLM_MODEL_ID,
88
isFreebuffDeploymentHours,
99
} from '@codebuff/common/constants/freebuff-models'
10+
import { openCodeZenModels } from '@codebuff/common/constants/model-config'
1011
import { postChatCompletions } from '../_post'
1112
import {
1213
checkFreeModeRateLimit,
@@ -852,6 +853,85 @@ describe('/api/v1/chat/completions POST endpoint', () => {
852853
FETCH_PATH_TEST_TIMEOUT_MS,
853854
)
854855

856+
it(
857+
'rejects OpenCode Zen models while the Zen integration is disabled',
858+
async () => {
859+
const fetchViaOpenCodeZen = mock(
860+
async (_url: string | URL | Request, _init?: RequestInit) => {
861+
throw new Error('OpenCode Zen should not be called')
862+
},
863+
) as unknown as typeof globalThis.fetch
864+
865+
for (const codebuffModel of Object.values(openCodeZenModels)) {
866+
const req = new NextRequest(
867+
'http://localhost:3000/api/v1/chat/completions',
868+
{
869+
method: 'POST',
870+
headers: {
871+
Authorization: 'Bearer test-api-key-123',
872+
},
873+
body: JSON.stringify({
874+
model: codebuffModel,
875+
messages: [
876+
{
877+
role: 'system',
878+
content: 'system prompt',
879+
cache_control: { type: 'ephemeral' },
880+
},
881+
{
882+
role: 'user',
883+
content: [
884+
{
885+
type: 'text',
886+
text: 'hello',
887+
cache_control: { type: 'ephemeral' },
888+
},
889+
],
890+
},
891+
],
892+
tools: [
893+
{
894+
id: 'tool_1',
895+
type: 'function',
896+
function: {
897+
name: 'read_files',
898+
parameters: { type: 'object' },
899+
},
900+
},
901+
],
902+
stream: false,
903+
codebuff_metadata: {
904+
run_id: 'run-123',
905+
client_id: 'test-client-id-123',
906+
},
907+
}),
908+
},
909+
)
910+
911+
const response = await postChatCompletions({
912+
req,
913+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
914+
logger: mockLogger,
915+
trackEvent: mockTrackEvent,
916+
getUserUsageData: mockGetUserUsageData,
917+
getAgentRunFromId: mockGetAgentRunFromId,
918+
fetch: fetchViaOpenCodeZen,
919+
insertMessageBigquery: mockInsertMessageBigquery,
920+
loggerWithContext: mockLoggerWithContext,
921+
})
922+
923+
const body = await response.json()
924+
expect(response.status).toBe(400)
925+
expect(body).toEqual({
926+
error: 'opencode_zen_disabled',
927+
message: 'OpenCode Zen models are currently disabled.',
928+
})
929+
}
930+
expect(fetchViaOpenCodeZen).not.toHaveBeenCalled()
931+
},
932+
FETCH_PATH_TEST_TIMEOUT_MS,
933+
)
934+
855935
it('rejects the DeepSeek V4 free agent when it requests another free model', async () => {
856936
const req = new NextRequest(
857937
'http://localhost:3000/api/v1/chat/completions',

web/src/app/api/v1/chat/completions/_post.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import {
5555
handleDeepSeekStream,
5656
isDeepSeekModel,
5757
} from '@/llm-api/deepseek'
58+
import { isOpenCodeZenModel } from '@/llm-api/opencode-zen'
5859
import {
5960
SiliconFlowError,
6061
handleSiliconFlowNonStream,
@@ -377,6 +378,25 @@ export async function postChatCompletions(params: {
377378
)
378379
}
379380

381+
if (isOpenCodeZenModel(typedBody.model)) {
382+
trackEvent({
383+
event: AnalyticsEvent.CHAT_COMPLETIONS_VALIDATION_ERROR,
384+
userId,
385+
properties: {
386+
error: 'opencode_zen_disabled',
387+
model: typedBody.model,
388+
},
389+
logger,
390+
})
391+
return NextResponse.json(
392+
{
393+
error: 'opencode_zen_disabled',
394+
message: 'OpenCode Zen models are currently disabled.',
395+
},
396+
{ status: 400 },
397+
)
398+
}
399+
380400
// Free-mode requests must use an allowlisted agent+model combination.
381401
// Without this gate, an attacker on a brand-new unpaid account can set
382402
// cost_mode='free' to bypass both the paid-account check and the balance

0 commit comments

Comments
 (0)