Skip to content

Commit beecd7c

Browse files
jahoomaclaude
andcommitted
Default freebuff to DeepSeek V4 Pro, switch lite to Kimi
- Freebuff picker now offers DeepSeek (default, smartest, with "Collects data for training" warning), Kimi K2.6, and MiniMax. Both Kimi and DeepSeek run 24/7 with 5/18h rate limits and 1000-slot instant-admit capacity. - Codebuff Lite (paid) defaults to Kimi instead of DeepSeek to avoid silently routing user prompts through a model whose provider trains on them. - Generalised the gemini-thinker session bypass from Kimi-only to any smart parent model, with `canFreebuffModelSpawnGeminiThinker` as the helper. - Editor variant table replaces the chained ternary; only Opus retains <think>-tag scaffolding. - Disabled INCLUDE_REASONING_IN_MESSAGE_HISTORY pending broader testing; the new stream-parser-reasoning tests skip while it's off. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2a2037f commit beecd7c

28 files changed

Lines changed: 464 additions & 246 deletions

File tree

agents/__tests__/editor.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ describe('editor agent', () => {
7070
expect(kimiEditor.model).toBe('moonshotai/kimi-k2.6')
7171
})
7272

73+
test('creates deepseek editor', () => {
74+
const deepseekEditor = createCodeEditor({ model: 'deepseek' })
75+
expect(deepseekEditor.model).toBe('deepseek/deepseek-v4-pro')
76+
})
77+
7378
test('creates minimax editor', () => {
7479
const minimaxEditor = createCodeEditor({ model: 'minimax' })
7580
expect(minimaxEditor.model).toBe('minimax/minimax-m2.7')
@@ -93,6 +98,12 @@ describe('editor agent', () => {
9398
expect(kimiEditor.instructionsPrompt).not.toContain('</think>')
9499
})
95100

101+
test('deepseek editor does not include think tags in instructions', () => {
102+
const deepseekEditor = createCodeEditor({ model: 'deepseek' })
103+
expect(deepseekEditor.instructionsPrompt).not.toContain('<think>')
104+
expect(deepseekEditor.instructionsPrompt).not.toContain('</think>')
105+
})
106+
96107
test('minimax editor does not include think tags in instructions', () => {
97108
const minimaxEditor = createCodeEditor({ model: 'minimax' })
98109
expect(minimaxEditor.instructionsPrompt).not.toContain('<think>')

agents/base2/base2.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { buildArray } from '@codebuff/common/util/array'
2-
import { FREEBUFF_KIMI_MODEL_ID } from '@codebuff/common/constants/freebuff-models'
32
import {
43
FREEBUFF_GEMINI_THINKER_AGENT_ID,
54
FREEBUFF_GEMINI_THINKER_INSTRUCTIONS_PROMPT,
@@ -36,10 +35,23 @@ export function createBase2(
3635
const isFree = mode === 'free' || mode === 'lite'
3736

3837
const isSonnet = false
38+
// Lite (paid Codebuff) defaults to Kimi: no data-retention surface in the
39+
// CLI today, so we don't want to silently route Codebuff prompts through a
40+
// model whose provider trains on user data. Free (freebuff) defaults to
41+
// DeepSeek and surfaces the data-collection caveat in the picker; the CLI
42+
// overrides the model anyway based on the user's freebuff selection.
3943
const model =
4044
modelOverride ??
41-
(isFree ? 'moonshotai/kimi-k2.6' : 'anthropic/claude-opus-4.7')
42-
const hasFreeGeminiThinker = isFree && model === FREEBUFF_KIMI_MODEL_ID
45+
(mode === 'lite'
46+
? 'moonshotai/kimi-k2.6'
47+
: mode === 'free'
48+
? 'deepseek/deepseek-v4-pro'
49+
: 'anthropic/claude-opus-4.7')
50+
// Bundled free-mode definitions ship with the gemini-thinker spawnable +
51+
// prompts; the CLI strips them at runtime if the user picks a fast model
52+
// that doesn't benefit (e.g. MiniMax). Smart freebuff models (Kimi,
53+
// DeepSeek) keep it so they can offload deeper reasoning.
54+
const hasFreeGeminiThinker = isFree
4355
const defaultProviderOptions = isFree
4456
? {
4557
data_collection: 'deny' as const,

agents/editor/editor.ts

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,37 @@ import { publisher } from '../constants'
22

33
import type { AgentDefinition } from '../types/agent-definition'
44

5+
type CodeEditorVariant =
6+
| 'gpt-5'
7+
| 'opus'
8+
| 'glm'
9+
| 'kimi'
10+
| 'deepseek'
11+
| 'minimax'
12+
13+
const EDITOR_MODEL_BY_VARIANT: Record<CodeEditorVariant, string> = {
14+
'gpt-5': 'openai/gpt-5.1',
15+
opus: 'anthropic/claude-opus-4.7',
16+
glm: 'z-ai/glm-5.1',
17+
kimi: 'moonshotai/kimi-k2.6',
18+
deepseek: 'deepseek/deepseek-v4-pro',
19+
minimax: 'minimax/minimax-m2.7',
20+
}
21+
22+
// Only Opus gets <think>-tag scaffolding in its instructions; the other
23+
// variants either have native reasoning (deepseek) or are non-reasoning
24+
// models where the extra prose just bloats the prompt without helping.
25+
const EDITOR_VARIANTS_WITH_THINK_TAGS: ReadonlySet<CodeEditorVariant> = new Set(
26+
['opus'],
27+
)
28+
529
export const createCodeEditor = (options: {
6-
model: 'gpt-5' | 'opus' | 'glm' | 'kimi' | 'minimax'
30+
model: CodeEditorVariant
731
}): Omit<AgentDefinition, 'id'> => {
832
const { model } = options
933
return {
1034
publisher,
11-
model:
12-
options.model === 'gpt-5'
13-
? 'openai/gpt-5.1'
14-
: options.model === 'minimax'
15-
? 'minimax/minimax-m2.7'
16-
: options.model === 'kimi'
17-
? 'moonshotai/kimi-k2.6'
18-
: options.model === 'glm'
19-
? 'z-ai/glm-5.1'
20-
: 'anthropic/claude-opus-4.7',
35+
model: EDITOR_MODEL_BY_VARIANT[options.model],
2136
...(options.model === 'opus' && {
2237
providerOptions: {
2338
only: ['amazon-bedrock'],
@@ -69,12 +84,8 @@ OR for new files or major rewrites:
6984
</codebuff_tool_call>
7085
7186
${
72-
model === 'gpt-5' ||
73-
model === 'glm' ||
74-
model === 'kimi' ||
75-
model === 'minimax'
76-
? ''
77-
: `Before you start writing your implementation, you should use <think> tags to think about the best way to implement the changes.
87+
EDITOR_VARIANTS_WITH_THINK_TAGS.has(model)
88+
? `Before you start writing your implementation, you should use <think> tags to think about the best way to implement the changes.
7889
7990
You can also use <think> tags interspersed between tool calls to think about the best way to implement the changes.
8091
@@ -101,6 +112,7 @@ You can also use <think> tags interspersed between tool calls to think about the
101112
</codebuff_tool_call>
102113
103114
</example>`
115+
: ''
104116
}
105117
106118
Your implementation should:

cli/src/__tests__/integration/local-agents.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
FREEBUFF_GEMINI_THINKER_SYSTEM_INSTRUCTION,
1111
} from '@codebuff/common/constants/freebuff-gemini-thinker'
1212
import {
13+
FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
1314
FREEBUFF_KIMI_MODEL_ID,
1415
FREEBUFF_MINIMAX_MODEL_ID,
1516
} from '@codebuff/common/constants/freebuff-models'
@@ -77,6 +78,26 @@ describe('configureFreebuffBaseAgentForModel', () => {
7778
expect(definition.stepPrompt).toContain(FREEBUFF_GEMINI_THINKER_STEP_PROMPT)
7879
})
7980

81+
test('keeps the Gemini thinker and prompt guidance for DeepSeek', () => {
82+
const definition = makeBase2Free()
83+
84+
configureFreebuffBaseAgentForModel(
85+
definition,
86+
FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
87+
)
88+
89+
expect(definition.spawnableAgents).toContain(
90+
FREEBUFF_GEMINI_THINKER_AGENT_ID,
91+
)
92+
expect(definition.systemPrompt).toContain(
93+
FREEBUFF_GEMINI_THINKER_SYSTEM_INSTRUCTION,
94+
)
95+
expect(definition.instructionsPrompt).toContain(
96+
FREEBUFF_GEMINI_THINKER_INSTRUCTIONS_PROMPT,
97+
)
98+
expect(definition.stepPrompt).toContain(FREEBUFF_GEMINI_THINKER_STEP_PROMPT)
99+
})
100+
80101
test('removes only exact Gemini thinker prompt guidance for MiniMax', () => {
81102
const definition = makeBase2Free()
82103
definition.systemPrompt +=

cli/src/components/freebuff-model-selector.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
44

55
import { Button } from './button'
66
import {
7+
DEFAULT_FREEBUFF_MODEL_ID,
78
FALLBACK_FREEBUFF_MODEL_ID,
8-
FREEBUFF_KIMI_MODEL_ID,
99
FREEBUFF_MODELS,
1010
getFreebuffDeploymentAvailabilityLabel,
1111
isFreebuffModelAvailable,
@@ -19,11 +19,16 @@ import { useTerminalDimensions } from '../hooks/use-terminal-dimensions'
1919
import { useTheme } from '../hooks/use-theme'
2020
import { nextFreebuffModelId } from '../utils/freebuff-model-navigation'
2121

22+
import type { FreebuffModelOption } from '@codebuff/common/constants/freebuff-models'
2223
import type { KeyEvent } from '@opentui/core'
2324

24-
const FREEBUFF_MODEL_SELECTOR_MODELS = [
25-
...FREEBUFF_MODELS.filter((model) => model.id === FREEBUFF_KIMI_MODEL_ID),
26-
...FREEBUFF_MODELS.filter((model) => model.id !== FREEBUFF_KIMI_MODEL_ID),
25+
// Widen the readonly tuple from FREEBUFF_MODELS to FreebuffModelOption[] so
26+
// the selector can branch on optional fields (e.g. `warning`) and on
27+
// availability values that aren't present in today's set but might be added
28+
// later, without TS narrowing the literal types away.
29+
const FREEBUFF_MODEL_SELECTOR_MODELS: readonly FreebuffModelOption[] = [
30+
...FREEBUFF_MODELS.filter((model) => model.id === DEFAULT_FREEBUFF_MODEL_ID),
31+
...FREEBUFF_MODELS.filter((model) => model.id !== DEFAULT_FREEBUFF_MODEL_ID),
2732
]
2833

2934
/**
@@ -69,7 +74,7 @@ export const FreebuffModelSelector: React.FC = () => {
6974
// unavailable (e.g. deployment hours close while the picker is open),
7075
// swap to the always-available fallback so Enter doesn't POST a model
7176
// the server will immediately reject. In-memory only — the user's saved
72-
// preference (e.g. Kimi) is preserved for the next launch.
77+
// preference (e.g. Kimi or DeepSeek) is preserved for the next launch.
7378
if (
7479
(session?.status === 'none' || !session) &&
7580
!isFreebuffModelAvailable(selectedModel, new Date(now))
@@ -119,7 +124,7 @@ export const FreebuffModelSelector: React.FC = () => {
119124

120125
// Decide row vs column layout based on whether the buttons actually fit
121126
// side-by-side. Each button's inner text is
122-
// "● {displayName} · {tagline} · {hours} {hint}",
127+
// "● {displayName} · {tagline} · {hours/warning} {hint}",
123128
// plus 2 cols of border and 2 cols of padding. Buttons are separated by a
124129
// gap of 2. If the total exceeds the terminal width, stack vertically.
125130
const stackVertically = useMemo(() => {
@@ -134,6 +139,7 @@ export const FreebuffModelSelector: React.FC = () => {
134139
(model.availability === 'deployment_hours'
135140
? 3 + deploymentAvailabilityLabel.length
136141
: 0) +
142+
(model.warning ? 3 + model.warning.length : 0) +
137143
2 /* " " */ +
138144
hintWidth
139145
return sum + inner + BUTTON_CHROME + (idx > 0 ? GAP : 0)
@@ -302,6 +308,9 @@ export const FreebuffModelSelector: React.FC = () => {
302308
{model.availability === 'deployment_hours' && (
303309
<span fg={theme.muted}> · {deploymentAvailabilityLabel}</span>
304310
)}
311+
{model.warning && (
312+
<span fg={theme.secondary}> · {model.warning}</span>
313+
)}
305314
<span fg={hintColor}> {hint.padEnd(hintWidth)}</span>
306315
</text>
307316
</Button>

cli/src/components/waiting-room-screen.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -260,9 +260,9 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
260260
<span>Elapsed </span>
261261
{formatElapsed(elapsedMs)}
262262
</text>
263-
{/* Per-model session quota (e.g. Kimi K2.6 caps at 5/12h). Only
264-
rendered for rate-limited models so the Minimax queue stays
265-
clutter-free. */}
263+
{/* Per-model session quota (e.g. DeepSeek V4 Pro caps at 5/12h).
264+
Only rendered for rate-limited models so the Minimax queue
265+
stays clutter-free. */}
266266
{session.rateLimit && (
267267
<text style={{ fg: theme.muted, alignSelf: 'flex-start' }}>
268268
<span>Sessions </span>
@@ -343,8 +343,8 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
343343
</>
344344
)}
345345

346-
{/* Per-model session quota exhausted (e.g. 5+ Kimi sessions in the
347-
last 12h). Terminal for this run — the user can exit and come
346+
{/* Per-model session quota exhausted (e.g. 5+ DeepSeek sessions in
347+
the last 12h). Terminal for this run — the user can exit and come
348348
back once the oldest session in the window rolls off. */}
349349
{session?.status === 'rate_limited' && (
350350
<>

cli/src/hooks/use-freebuff-session.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ async function callSession(
104104
return body
105105
}
106106
}
107-
// 429 from POST is the per-model session-quota reject (e.g. too many Kimi
107+
// 429 from POST is the per-model session-quota reject (e.g. too many DeepSeek
108108
// sessions in the last 12h). Terminal for the current poll — the CLI shows
109109
// a screen explaining the limit and when the user can try again. The 429
110110
// status (rather than 200) keeps older CLIs in their error path so they
@@ -442,10 +442,10 @@ export function useFreebuffSession(): UseFreebuffSessionResult {
442442
}
443443
if (next.status === 'model_unavailable') {
444444
// Server says the requested model isn't available right now (e.g.
445-
// Kimi outside deployment hours). Flip to the always-available
446-
// fallback for this run. In-memory only — `setSelectedModel`
447-
// doesn't persist, so the user's saved preference (e.g. Kimi)
448-
// is preserved for their next launch during deployment hours.
445+
// legacy GLM 5.1 outside deployment hours). Flip to the
446+
// always-available fallback for this run. In-memory only —
447+
// `setSelectedModel` doesn't persist, so the user's saved preference
448+
// is preserved for their next launch.
449449
useFreebuffModelStore
450450
.getState()
451451
.setSelectedModel(FALLBACK_FREEBUFF_MODEL_ID)

cli/src/utils/local-agent-registry.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
FREEBUFF_GEMINI_THINKER_PROMPT_LINES,
1717
} from '@codebuff/common/constants/freebuff-gemini-thinker'
1818
import {
19-
FREEBUFF_KIMI_MODEL_ID,
19+
canFreebuffModelSpawnGeminiThinker,
2020
FREEBUFF_MODELS,
2121
} from '@codebuff/common/constants/freebuff-models'
2222

@@ -57,24 +57,23 @@ function stripFreebuffGeminiThinkerPrompt(prompt: string): string {
5757
.join('\n')
5858
}
5959

60+
/** The bundled `base2-free` ships with the gemini-thinker spawnable + prompts
61+
* so the smart freebuff models (Kimi, DeepSeek) can offload deeper reasoning.
62+
* When the user picks a model that doesn't support gemini-thinker (e.g.
63+
* MiniMax — fastest tier, extra round-trip would defeat that), strip the
64+
* spawnable and the inlined prompt guidance so the agent doesn't try to call
65+
* a tool we just removed. */
6066
export function configureFreebuffBaseAgentForModel(
6167
def: ConfigurableFreebuffBaseAgent,
6268
selectedModel: string,
6369
): void {
6470
if (def.id !== 'base2-free') return
71+
if (canFreebuffModelSpawnGeminiThinker(selectedModel)) return
6572

66-
const hasGeminiThinker = selectedModel === FREEBUFF_KIMI_MODEL_ID
6773
const spawnableAgents = def.spawnableAgents ?? []
68-
69-
def.spawnableAgents = hasGeminiThinker
70-
? Array.from(
71-
new Set([...spawnableAgents, FREEBUFF_GEMINI_THINKER_AGENT_ID]),
72-
)
73-
: spawnableAgents.filter(
74-
(agentId) => agentId !== FREEBUFF_GEMINI_THINKER_AGENT_ID,
75-
)
76-
77-
if (hasGeminiThinker) return
74+
def.spawnableAgents = spawnableAgents.filter(
75+
(agentId) => agentId !== FREEBUFF_GEMINI_THINKER_AGENT_ID,
76+
)
7877

7978
for (const key of [
8079
'systemPrompt',

common/src/__tests__/freebuff-models.test.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { describe, expect, test } from 'bun:test'
22

33
import {
4+
canFreebuffModelSpawnGeminiThinker,
45
DEFAULT_FREEBUFF_MODEL_ID,
6+
FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
57
FREEBUFF_GLM_MODEL_ID,
68
FREEBUFF_KIMI_MODEL_ID,
9+
FREEBUFF_MINIMAX_MODEL_ID,
710
FREEBUFF_MODELS,
811
SUPPORTED_FREEBUFF_MODELS,
912
getFreebuffDeploymentAvailabilityLabel,
@@ -13,8 +16,25 @@ import {
1316
} from '../constants/freebuff-models'
1417

1518
describe('freebuff model availability', () => {
16-
test('defaults to Kimi K2.6', () => {
17-
expect(DEFAULT_FREEBUFF_MODEL_ID).toBe(FREEBUFF_KIMI_MODEL_ID)
19+
test('defaults to DeepSeek V4 Pro (the smartest free model)', () => {
20+
expect(DEFAULT_FREEBUFF_MODEL_ID).toBe(FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID)
21+
})
22+
23+
test('DeepSeek carries the data-collection warning so users see it before picking', () => {
24+
const deepseek = FREEBUFF_MODELS.find(
25+
(m) => m.id === FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
26+
)
27+
expect(deepseek?.warning).toBe('Collects data for training')
28+
})
29+
30+
test('only smart freebuff models can spawn the gemini-thinker subagent', () => {
31+
expect(canFreebuffModelSpawnGeminiThinker(FREEBUFF_KIMI_MODEL_ID)).toBe(true)
32+
expect(
33+
canFreebuffModelSpawnGeminiThinker(FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID),
34+
).toBe(true)
35+
expect(canFreebuffModelSpawnGeminiThinker(FREEBUFF_MINIMAX_MODEL_ID)).toBe(
36+
false,
37+
)
1838
})
1939

2040
test('supports GLM 5.1 as a legacy server-side model without selecting it for new clients', () => {

common/src/constants/free-agents.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export const FREE_MODE_AGENT_MODELS: Record<string, Set<string>> = {
6363
// Code reviewer for free mode
6464
'code-reviewer-lite': new Set(FREEBUFF_ALLOWED_MODEL_IDS),
6565

66-
// Kimi freebuff root may spawn Gemini Pro for deeper thinking.
66+
// Legacy: kept for the standalone gemini thinker agent if invoked directly.
6767
[FREEBUFF_GEMINI_THINKER_AGENT_ID]: new Set([FREEBUFF_GEMINI_PRO_MODEL_ID]),
6868
}
6969

0 commit comments

Comments
 (0)