Skip to content
Open
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
22 changes: 11 additions & 11 deletions agents/__tests__/editor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ describe('editor agent', () => {
expect(gpt5Editor.model).toBe('openai/gpt-5.1')
})

test('creates glm editor', () => {
const glmEditor = createCodeEditor({ model: 'glm' })
expect(glmEditor.model).toBe('z-ai/glm-5.1')
test('creates kimi editor', () => {
const kimiEditor = createCodeEditor({ model: 'kimi' })
expect(kimiEditor.model).toBe('moonshotai/kimi-k2.6')
})

test('creates minimax editor', () => {
Expand All @@ -78,10 +78,10 @@ describe('editor agent', () => {
expect(gpt5Editor.instructionsPrompt).not.toContain('</think>')
})

test('glm editor does not include think tags in instructions', () => {
const glmEditor = createCodeEditor({ model: 'glm' })
expect(glmEditor.instructionsPrompt).not.toContain('<think>')
expect(glmEditor.instructionsPrompt).not.toContain('</think>')
test('kimi editor does not include think tags in instructions', () => {
const kimiEditor = createCodeEditor({ model: 'kimi' })
expect(kimiEditor.instructionsPrompt).not.toContain('<think>')
expect(kimiEditor.instructionsPrompt).not.toContain('</think>')
})

test('minimax editor does not include think tags in instructions', () => {
Expand All @@ -99,17 +99,17 @@ describe('editor agent', () => {
test('all variants have same base properties', () => {
const opusEditor = createCodeEditor({ model: 'opus' })
const gpt5Editor = createCodeEditor({ model: 'gpt-5' })
const glmEditor = createCodeEditor({ model: 'glm' })
const kimiEditor = createCodeEditor({ model: 'kimi' })

// All should have same basic structure
expect(opusEditor.displayName).toBe(gpt5Editor.displayName)
expect(gpt5Editor.displayName).toBe(glmEditor.displayName)
expect(gpt5Editor.displayName).toBe(kimiEditor.displayName)

expect(opusEditor.outputMode).toBe(gpt5Editor.outputMode)
expect(gpt5Editor.outputMode).toBe(glmEditor.outputMode)
expect(gpt5Editor.outputMode).toBe(kimiEditor.outputMode)

expect(opusEditor.toolNames).toEqual(gpt5Editor.toolNames)
expect(gpt5Editor.toolNames).toEqual(glmEditor.toolNames)
expect(gpt5Editor.toolNames).toEqual(kimiEditor.toolNames)
})
})

Expand Down
2 changes: 1 addition & 1 deletion agents/base2/base2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function createBase2(
const isFree = mode === 'free' || mode === 'lite'

const isSonnet = false
const model = isFree ? 'z-ai/glm-5.1' : 'anthropic/claude-opus-4.7'
const model = isFree ? 'moonshotai/kimi-k2.6' : 'anthropic/claude-opus-4.7'

return {
publisher,
Expand Down
2 changes: 1 addition & 1 deletion agents/editor/editor-lite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createCodeEditor } from './editor'
import type { AgentDefinition } from '../types/agent-definition'

const definition: AgentDefinition = {
...createCodeEditor({ model: 'glm' }),
...createCodeEditor({ model: 'kimi' }),
id: 'editor-lite',
}
export default definition
8 changes: 4 additions & 4 deletions agents/editor/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { publisher } from '../constants'
import type { AgentDefinition } from '../types/agent-definition'

export const createCodeEditor = (options: {
model: 'gpt-5' | 'opus' | 'glm' | 'minimax'
model: 'gpt-5' | 'opus' | 'kimi' | 'minimax'
}): Omit<AgentDefinition, 'id'> => {
const { model } = options
return {
Expand All @@ -14,8 +14,8 @@ export const createCodeEditor = (options: {
? 'openai/gpt-5.1'
: options.model === 'minimax'
? 'minimax/minimax-m2.7'
: options.model === 'glm'
? 'z-ai/glm-5.1'
: options.model === 'kimi'
? 'moonshotai/kimi-k2.6'
: 'anthropic/claude-opus-4.7',
...(options.model === 'opus' && {
providerOptions: {
Expand Down Expand Up @@ -67,7 +67,7 @@ OR for new files or major rewrites:
}
</codebuff_tool_call>

${model === 'gpt-5' || model === 'glm' || model === 'minimax'
${model === 'gpt-5' || model === 'kimi' || model === 'minimax'
? ''
: `Before you start writing your implementation, you should use <think> tags to think about the best way to implement the changes.

Expand Down
2 changes: 1 addition & 1 deletion agents/reviewer/code-reviewer-lite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { createReviewer } from './code-reviewer'
const definition: SecretAgentDefinition = {
id: 'code-reviewer-lite',
publisher,
...createReviewer('z-ai/glm-5.1'),
...createReviewer('moonshotai/kimi-k2.6'),
}

export default definition
6 changes: 3 additions & 3 deletions cli/src/components/freebuff-model-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { Button } from './button'
import {
FALLBACK_FREEBUFF_MODEL_ID,
FREEBUFF_GLM_MODEL_ID,
FREEBUFF_KIMI_MODEL_ID,
FREEBUFF_MODELS,
getFreebuffDeploymentAvailabilityLabel,
isFreebuffModelAvailable,
Expand All @@ -25,8 +25,8 @@ import {
import type { KeyEvent } from '@opentui/core'

const FREEBUFF_MODEL_SELECTOR_MODELS = [
...FREEBUFF_MODELS.filter((model) => model.id === FREEBUFF_GLM_MODEL_ID),
...FREEBUFF_MODELS.filter((model) => model.id !== FREEBUFF_GLM_MODEL_ID),
...FREEBUFF_MODELS.filter((model) => model.id === FREEBUFF_KIMI_MODEL_ID),
...FREEBUFF_MODELS.filter((model) => model.id !== FREEBUFF_KIMI_MODEL_ID),
]

/**
Expand Down
22 changes: 11 additions & 11 deletions cli/src/utils/__tests__/freebuff-model-navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,49 +7,49 @@ import {

describe('nextSelectableFreebuffModelId', () => {
test('skips unavailable models when moving forward', () => {
const modelIds = ['glm', 'minimax']
const modelIds = ['kimi', 'minimax']

expect(
nextSelectableFreebuffModelId({
modelIds,
focusedId: 'minimax',
direction: 'forward',
isSelectable: (id) => id !== 'glm',
isSelectable: (id) => id !== 'kimi',
}),
).toBe('minimax')
})

test('skips unavailable models when moving backward', () => {
const modelIds = ['glm', 'minimax']
const modelIds = ['kimi', 'minimax']

expect(
nextSelectableFreebuffModelId({
modelIds,
focusedId: 'minimax',
direction: 'backward',
isSelectable: (id) => id !== 'glm',
isSelectable: (id) => id !== 'kimi',
}),
).toBe('minimax')
})

test('moves to the next available model when more than one is selectable', () => {
const modelIds = ['glm', 'minimax', 'other']
const modelIds = ['kimi', 'minimax', 'other']

expect(
nextSelectableFreebuffModelId({
modelIds,
focusedId: 'minimax',
direction: 'forward',
isSelectable: (id) => id !== 'glm',
isSelectable: (id) => id !== 'kimi',
}),
).toBe('other')
})

test('returns null when no selectable model exists', () => {
expect(
nextSelectableFreebuffModelId({
modelIds: ['glm'],
focusedId: 'glm',
modelIds: ['kimi'],
focusedId: 'kimi',
direction: 'forward',
isSelectable: () => false,
}),
Expand All @@ -61,10 +61,10 @@ describe('resolveFreebuffModelCommitTarget', () => {
test('falls back to the selected model when focus is on a closed model', () => {
expect(
resolveFreebuffModelCommitTarget({
focusedId: 'glm',
focusedId: 'kimi',
selectedId: 'minimax',
committedId: null,
isSelectable: (id) => id !== 'glm',
isSelectable: (id) => id !== 'kimi',
}),
).toBe('minimax')
})
Expand All @@ -73,7 +73,7 @@ describe('resolveFreebuffModelCommitTarget', () => {
expect(
resolveFreebuffModelCommitTarget({
focusedId: 'minimax',
selectedId: 'glm',
selectedId: 'kimi',
committedId: null,
isSelectable: (id) => id === 'minimax',
}),
Expand Down
6 changes: 3 additions & 3 deletions common/src/constants/free-agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const FREE_MODE_AGENT_MODELS: Record<string, Set<string>> = {
// Root orchestrator
'base2-free': new Set([
'minimax/minimax-m2.7',
'z-ai/glm-5.1',
'moonshotai/kimi-k2.6',
]),

// File exploration agents
Expand All @@ -46,13 +46,13 @@ export const FREE_MODE_AGENT_MODELS: Record<string, Set<string>> = {
// Editor for free mode
'editor-lite': new Set([
'minimax/minimax-m2.7',
'z-ai/glm-5.1',
'moonshotai/kimi-k2.6',
]),

// Code reviewer for free mode
'code-reviewer-lite': new Set([
'minimax/minimax-m2.7',
'z-ai/glm-5.1',
'moonshotai/kimi-k2.6',
]),
}

Expand Down
23 changes: 11 additions & 12 deletions common/src/constants/freebuff-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface FreebuffModelOption {
* the caller's local timezone. The CLI should render
* `getFreebuffDeploymentAvailabilityLabel()` instead. */
export const FREEBUFF_DEPLOYMENT_HOURS_LABEL = '9am ET-5pm PT every day'
export const FREEBUFF_GLM_MODEL_ID = 'z-ai/glm-5.1'
export const FREEBUFF_KIMI_MODEL_ID = 'moonshotai/kimi-k2.6'
export const FREEBUFF_MINIMAX_MODEL_ID = 'minimax/minimax-m2.7'
const FREEBUFF_EASTERN_TIMEZONE = 'America/New_York'
const FREEBUFF_PACIFIC_TIMEZONE = 'America/Los_Angeles'
Expand All @@ -39,28 +39,27 @@ interface LocalTimeFormatOptions {
timeZone?: string
}

export const FREEBUFF_MODELS = [
export const FREEBUFF_MODELS: readonly FreebuffModelOption[] = [
{
id: FREEBUFF_MINIMAX_MODEL_ID,
displayName: 'MiniMax M2.7',
tagline: 'Fastest',
availability: 'always',
},
{
id: FREEBUFF_GLM_MODEL_ID,
displayName: 'GLM 5.1',
id: FREEBUFF_KIMI_MODEL_ID,
displayName: 'Kimi K2.6',
tagline: 'Smartest',
availability: 'deployment_hours',
availability: 'always',
},
] as const satisfies readonly FreebuffModelOption[]
]

export type FreebuffModelId = (typeof FREEBUFF_MODELS)[number]['id']
export type FreebuffModelId =
| typeof FREEBUFF_MINIMAX_MODEL_ID
| typeof FREEBUFF_KIMI_MODEL_ID
Comment on lines +42 to +59
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 as const satisfies removal makes FreebuffModelId drift-prone

Switching from as const satisfies readonly FreebuffModelOption[] to an explicit readonly FreebuffModelOption[] annotation widens the inferred element types to string, which is why FreebuffModelId had to be redefined as a manual union. The old pattern was simpler: a new entry in FREEBUFF_MODELS automatically expanded the union type with no extra edit required.

export const FREEBUFF_MODELS = [
  {
    id: FREEBUFF_MINIMAX_MODEL_ID,
    displayName: 'MiniMax M2.7',
    tagline: 'Fastest',
    availability: 'always',
  },
  {
    id: FREEBUFF_KIMI_MODEL_ID,
    displayName: 'Kimi K2.6',
    tagline: 'Smartest',
    availability: 'always',
  },
] as const satisfies readonly FreebuffModelOption[]

export type FreebuffModelId = (typeof FREEBUFF_MODELS)[number]['id']

This restores the original self-maintaining type while keeping the readonly FreebuffModelOption[] constraint via satisfies.

Prompt To Fix With AI
This is a comment left during a code review.
Path: common/src/constants/freebuff-models.ts
Line: 42-59

Comment:
**`as const satisfies` removal makes `FreebuffModelId` drift-prone**

Switching from `as const satisfies readonly FreebuffModelOption[]` to an explicit `readonly FreebuffModelOption[]` annotation widens the inferred element types to `string`, which is why `FreebuffModelId` had to be redefined as a manual union. The old pattern was simpler: a new entry in `FREEBUFF_MODELS` automatically expanded the union type with no extra edit required.

```ts
export const FREEBUFF_MODELS = [
  {
    id: FREEBUFF_MINIMAX_MODEL_ID,
    displayName: 'MiniMax M2.7',
    tagline: 'Fastest',
    availability: 'always',
  },
  {
    id: FREEBUFF_KIMI_MODEL_ID,
    displayName: 'Kimi K2.6',
    tagline: 'Smartest',
    availability: 'always',
  },
] as const satisfies readonly FreebuffModelOption[]

export type FreebuffModelId = (typeof FREEBUFF_MODELS)[number]['id']
```

This restores the original self-maintaining type while keeping the `readonly FreebuffModelOption[]` constraint via `satisfies`.

How can I resolve this? If you propose a fix, please make it concise.


/** What new freebuff users see selected in the picker. May not be currently
* available (GLM is closed outside deployment hours); callers that need an
* always-available id for resolution / auto-fallbacks should use
* FALLBACK_FREEBUFF_MODEL_ID instead. */
export const DEFAULT_FREEBUFF_MODEL_ID: FreebuffModelId = FREEBUFF_GLM_MODEL_ID
/** What new freebuff users see selected in the picker. */
export const DEFAULT_FREEBUFF_MODEL_ID: FreebuffModelId = FREEBUFF_KIMI_MODEL_ID

/** Always-available fallback used when the requested model can't be served
* right now (unknown id, deployment hours closed, etc.). Kept distinct from
Expand Down
29 changes: 10 additions & 19 deletions web/src/app/api/v1/chat/completions/__tests__/completions.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { afterEach, beforeEach, describe, expect, mock, it } from 'bun:test'
import { NextRequest } from 'next/server'

import { isFreebuffDeploymentHours } from '@codebuff/common/constants/freebuff-models'
import { formatQuotaResetCountdown, postChatCompletions } from '../_post'

import type { TrackEventFn } from '@codebuff/common/types/contracts/analytics'
Expand Down Expand Up @@ -556,15 +555,15 @@ describe('/api/v1/chat/completions POST endpoint', () => {
expect(response.status).toBe(200)
})

it('lets freebuff use GLM 5.1 through Fireworks availability rules', async () => {
it('lets freebuff use Kimi K2.6 through CanopyWave', async () => {
const fetchedBodies: Record<string, unknown>[] = []
const fetchViaFireworks = mock(
const fetchViaCanopyWave = mock(
async (_url: string | URL | Request, init?: RequestInit) => {
fetchedBodies.push(JSON.parse(init?.body as string))
return new Response(
JSON.stringify({
id: 'test-id',
model: 'accounts/james-65d217/deployments/mjb4i7ea',
model: 'moonshotai/kimi-k2.6',
choices: [{ message: { content: 'test response' } }],
usage: {
prompt_tokens: 10,
Expand All @@ -586,7 +585,7 @@ describe('/api/v1/chat/completions POST endpoint', () => {
method: 'POST',
headers: { Authorization: 'Bearer test-api-key-new-free' },
body: JSON.stringify({
model: 'z-ai/glm-5.1',
model: 'moonshotai/kimi-k2.6',
stream: false,
codebuff_metadata: {
run_id: 'run-free',
Expand All @@ -604,26 +603,18 @@ describe('/api/v1/chat/completions POST endpoint', () => {
trackEvent: mockTrackEvent,
getUserUsageData: mockGetUserUsageData,
getAgentRunFromId: mockGetAgentRunFromId,
fetch: fetchViaFireworks,
fetch: fetchViaCanopyWave,
insertMessageBigquery: mockInsertMessageBigquery,
loggerWithContext: mockLoggerWithContext,
checkSessionAdmissible: mockCheckSessionAdmissibleAllow,
})

const body = await response.json()
if (isFreebuffDeploymentHours()) {
expect(response.status).toBe(200)
expect(fetchedBodies).toHaveLength(1)
expect(fetchedBodies[0].model).toBe(
'accounts/james-65d217/deployments/mjb4i7ea',
)
expect(body.model).toBe('z-ai/glm-5.1')
expect(body.provider).toBe('Fireworks')
} else {
expect(response.status).toBe(503)
expect(fetchedBodies).toHaveLength(0)
expect(body.error.code).toBe('DEPLOYMENT_OUTSIDE_HOURS')
}
expect(response.status).toBe(200)
expect(fetchedBodies).toHaveLength(1)
expect(fetchedBodies[0].model).toBe('moonshotai/kimi-k2.6')
expect(body.model).toBe('moonshotai/kimi-k2.6')
expect(body.provider).toBe('CanopyWave')
})

it('skips credit check when in FREE mode even with 0 credits', async () => {
Expand Down
13 changes: 0 additions & 13 deletions web/src/app/api/v1/freebuff/session/__tests__/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,19 +158,6 @@ describe('POST /api/v1/freebuff/session', () => {
expect(body.status).toBe('queued')
})

test('returns model_unavailable for GLM outside deployment hours', async () => {
const sessionDeps = makeSessionDeps()
const resp = await postFreebuffSession(
makeReq('ok', { model: 'z-ai/glm-5.1' }),
makeDeps(sessionDeps, 'u1'),
)
expect(resp.status).toBe(409)
const body = await resp.json()
expect(body.status).toBe('model_unavailable')
expect(body.availableHours).toBe('9am ET-5pm PT every day')
expect(sessionDeps.rows.size).toBe(0)
})

// Banned bots with valid API keys were POSTing every few seconds and
// inflating queueDepth between the 15s admission-tick sweeps. Rejecting at
// the HTTP layer with 403 (terminal, like country_blocked) keeps them out
Expand Down
Loading
Loading