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 kimi editor', () => {
Expand All @@ -83,10 +83,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('kimi editor does not include think tags in instructions', () => {
Expand All @@ -110,17 +110,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
3 changes: 1 addition & 2 deletions agents/editor/editor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import { publisher } from '../constants'

import type { AgentDefinition } from '../types/agent-definition'
Expand Down Expand Up @@ -34,7 +33,7 @@ export const createCodeEditor = (options: {
inheritParentSystemPrompt: true,

instructionsPrompt: `You are an expert code editor with deep understanding of software engineering principles. You were spawned to generate an implementation for the user's request. Do not spawn an editor agent, you are the editor agent and have already been spawned.

Your task is to write out ALL the code changes needed to complete the user's request in a single comprehensive response.

Important: You can not make any other tool calls besides editing files. You cannot read more files, write todos, spawn agents, or set output. set_output in particular should not be used. Do not call any of these tools!
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
15 changes: 7 additions & 8 deletions common/src/constants/freebuff-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ interface LocalTimeFormatOptions {
timeZone?: string
}

export const FREEBUFF_MODELS = [
export const FREEBUFF_MODELS: readonly FreebuffModelOption[] = [
{
id: FREEBUFF_MINIMAX_MODEL_ID,
displayName: 'MiniMax M2.7',
Expand All @@ -50,16 +50,15 @@ export const FREEBUFF_MODELS = [
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 (Kimi is closed outside deployment hours); callers that need an
* always-available id for resolution / auto-fallbacks should use
* FALLBACK_FREEBUFF_MODEL_ID instead. */
/** 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
Expand Down
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 @@ -642,15 +641,15 @@
expect(body.countryBlockReason).toBe('anonymized_or_unknown_country')
})

it('lets freebuff use Kimi K2.6 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/fireworks/models/kimi-k2p6',
model: 'moonshotai/kimi-k2.6',
choices: [{ message: { content: 'test response' } }],
usage: {
prompt_tokens: 10,
Expand Down Expand Up @@ -690,26 +689,18 @@
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/fireworks/models/kimi-k2p6',
)
expect(body.model).toBe('moonshotai/kimi-k2.6')
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)

Check failure on line 699 in web/src/app/api/v1/chat/completions/__tests__/completions.test.ts

View workflow job for this annotation

GitHub Actions / test-web

error: expect(received).toBe(expected)

Expected: 200 Received: 503 at <anonymous> (/home/runner/work/***/***/web/src/app/api/v1/chat/completions/__***s__/completions.***.ts:699:31)

Check failure on line 699 in web/src/app/api/v1/chat/completions/__tests__/completions.test.ts

View workflow job for this annotation

GitHub Actions / test-web

error: expect(received).toBe(expected)

Expected: 200 Received: 503 at <anonymous> (/home/runner/work/***/***/web/src/app/api/v1/chat/completions/__***s__/completions.***.ts:699:31)

Check failure on line 699 in web/src/app/api/v1/chat/completions/__tests__/completions.test.ts

View workflow job for this annotation

GitHub Actions / test-web

error: expect(received).toBe(expected)

Expected: 200 Received: 503 at <anonymous> (/home/runner/work/***/***/web/src/app/api/v1/chat/completions/__***s__/completions.***.ts:699:31)
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 @@ -281,19 +281,6 @@ describe('POST /api/v1/freebuff/session', () => {
expect(body.status).toBe('queued')
})

test('returns model_unavailable for Kimi outside deployment hours', async () => {
const sessionDeps = makeSessionDeps()
const resp = await postFreebuffSession(
makeReq('ok', { model: 'moonshotai/kimi-k2.6' }),
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
22 changes: 3 additions & 19 deletions web/src/server/free-session/__tests__/public-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,20 +200,6 @@ describe('requestSession', () => {
expect(state.instanceId).toBe('inst-1')
})

test('deployment-hours-only model is unavailable outside deployment hours', async () => {
const state = await requestSession({
userId: 'u1',
model: 'moonshotai/kimi-k2.6',
deps,
})
expect(state).toEqual({
status: 'model_unavailable',
requestedModel: 'moonshotai/kimi-k2.6',
availableHours: '9am ET-5pm PT every day',
})
expect(deps.rows.size).toBe(0)
})

test('queued response includes a per-model depth snapshot for the selector', async () => {
deps._tick(new Date('2026-04-17T16:00:00Z'))
// Seed 2 users in MiniMax + 1 in Kimi so the returned map captures both.
Expand Down Expand Up @@ -325,9 +311,7 @@ describe('requestSession', () => {

// Per-user rate limit (5 Kimi admissions per 12h) — the wire limit is
// hard-coded in public-api.ts, so tests seed the fake admit log directly
// rather than configuring it. Kimi also has deployment-hours gating, so
// these tests bump `now` into the open window (12pm ET on a weekday)
// before issuing the request.
// rather than configuring it.
const KIMI_MODEL = 'moonshotai/kimi-k2.6'
const KIMI_LIMIT = 5
const KIMI_WINDOW_HOURS = 12
Expand Down Expand Up @@ -636,8 +620,8 @@ describe('getSessionState', () => {
// Regression: the POST response attached rateLimit, but GET polls did
// not — so the "Sessions N/M used" line flashed once then disappeared on
// the next 5s poll. GET must attach the same quota snapshot. Rate
// limits only apply to Kimi, so this test uses Kimi explicitly (inside
// deployment hours) rather than the Minimax DEFAULT_MODEL.
// limits only apply to Kimi, so this test uses Kimi explicitly rather
// than the Minimax DEFAULT_MODEL.
deps._tick(new Date('2026-04-17T16:00:00Z'))
const now = deps._now()
deps.admits.push({
Expand Down
Loading