From 5414a55155f58903ae9933c626ab40df580e7fcd Mon Sep 17 00:00:00 2001 From: James Grugett Date: Wed, 6 May 2026 17:13:49 -0700 Subject: [PATCH 1/3] Configure freebuff agents by model --- agents/base2/base2-free-deepseek-v4.ts | 7 +- agents/base2/base2-free-deepseek.ts | 15 +++ agents/base2/base2-free-kimi.ts | 14 +++ agents/base2/base2-free.ts | 4 +- agents/base2/base2.ts | 38 ++++--- agents/reviewer/code-reviewer-deepseek.ts | 11 +++ agents/reviewer/code-reviewer-kimi.ts | 11 +++ agents/reviewer/code-reviewer-lite.ts | 2 +- .../integration/local-agents.test.ts | 93 ----------------- cli/src/hooks/use-send-message.ts | 9 +- cli/src/utils/constants.ts | 10 +- cli/src/utils/freebuff-agent-selection.ts | 12 +++ cli/src/utils/local-agent-registry.ts | 99 +------------------ common/src/__tests__/free-agents.test.ts | 59 ++++++++++- common/src/__tests__/freebuff-models.test.ts | 8 +- common/src/constants/free-agents.ts | 32 +++++- common/src/constants/freebuff-models.ts | 7 +- docs/freebuff-waiting-room.md | 2 +- 18 files changed, 204 insertions(+), 229 deletions(-) create mode 100644 agents/base2/base2-free-deepseek.ts create mode 100644 agents/base2/base2-free-kimi.ts create mode 100644 agents/reviewer/code-reviewer-deepseek.ts create mode 100644 agents/reviewer/code-reviewer-kimi.ts create mode 100644 cli/src/utils/freebuff-agent-selection.ts diff --git a/agents/base2/base2-free-deepseek-v4.ts b/agents/base2/base2-free-deepseek-v4.ts index 19ca5a8912..4f6da0dc82 100644 --- a/agents/base2/base2-free-deepseek-v4.ts +++ b/agents/base2/base2-free-deepseek-v4.ts @@ -1,10 +1,7 @@ -import { createBase2 } from './base2' +import base2FreeDeepseek from './base2-free-deepseek' const definition = { - ...createBase2('free', { - noAskUser: true, - model: 'deepseek/deepseek-v4-pro', - }), + ...base2FreeDeepseek, id: 'base2-free-deepseek-v4', displayName: 'Buffy the DeepSeek V4 Free Orchestrator', } diff --git a/agents/base2/base2-free-deepseek.ts b/agents/base2/base2-free-deepseek.ts new file mode 100644 index 0000000000..c62aa2a8d5 --- /dev/null +++ b/agents/base2/base2-free-deepseek.ts @@ -0,0 +1,15 @@ +import { FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID } from '@codebuff/common/constants/freebuff-models' + +import { createBase2 } from './base2' + +const definition = { + ...createBase2('free', { + noAskUser: true, + model: FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID, + freeCodeReviewerAgentId: 'code-reviewer-deepseek', + }), + id: 'base2-free-deepseek', + displayName: 'Buffy the DeepSeek Free Orchestrator', +} + +export default definition diff --git a/agents/base2/base2-free-kimi.ts b/agents/base2/base2-free-kimi.ts new file mode 100644 index 0000000000..a769b81c47 --- /dev/null +++ b/agents/base2/base2-free-kimi.ts @@ -0,0 +1,14 @@ +import { FREEBUFF_KIMI_MODEL_ID } from '@codebuff/common/constants/freebuff-models' + +import { createBase2 } from './base2' + +const definition = { + ...createBase2('free', { + model: FREEBUFF_KIMI_MODEL_ID, + freeCodeReviewerAgentId: 'code-reviewer-kimi', + }), + id: 'base2-free-kimi', + displayName: 'Buffy the Kimi Free Orchestrator', +} + +export default definition diff --git a/agents/base2/base2-free.ts b/agents/base2/base2-free.ts index 464defff24..98b3dbd84e 100644 --- a/agents/base2/base2-free.ts +++ b/agents/base2/base2-free.ts @@ -1,7 +1,9 @@ import { createBase2 } from './base2' const definition = { - ...createBase2('free'), + ...createBase2('free', { + freeCodeReviewerAgentId: 'code-reviewer-lite', + }), id: 'base2-free', displayName: 'Buffy the Free Orchestrator', } diff --git a/agents/base2/base2.ts b/agents/base2/base2.ts index 4e2a06ecd6..18e216ebd7 100644 --- a/agents/base2/base2.ts +++ b/agents/base2/base2.ts @@ -5,6 +5,10 @@ import { FREEBUFF_GEMINI_THINKER_STEP_PROMPT, FREEBUFF_GEMINI_THINKER_SYSTEM_INSTRUCTION, } from '@codebuff/common/constants/freebuff-gemini-thinker' +import { + canFreebuffModelSpawnGeminiThinker, + FREEBUFF_MINIMAX_MODEL_ID, +} from '@codebuff/common/constants/freebuff-models' import { publisher } from '../constants' import { @@ -20,6 +24,7 @@ export function createBase2( noAskUser?: boolean model?: SecretAgentDefinition['model'] providerOptions?: SecretAgentDefinition['providerOptions'] + freeCodeReviewerAgentId?: string }, ): Omit { const { @@ -28,6 +33,7 @@ export function createBase2( noAskUser = false, model: modelOverride, providerOptions, + freeCodeReviewerAgentId = 'code-reviewer-lite', } = options ?? {} const isDefault = mode === 'default' const isFast = mode === 'fast' @@ -38,20 +44,18 @@ export function createBase2( // Lite (paid Codebuff) defaults to Kimi: no data-retention surface in the // CLI today, so we don't want to silently route Codebuff prompts through a // model whose provider trains on user data. Free (freebuff) defaults to - // DeepSeek and surfaces the data-collection caveat in the picker; the CLI - // overrides the model anyway based on the user's freebuff selection. + // MiniMax M2.7; Kimi and DeepSeek are separate free agent variants. const model = modelOverride ?? (mode === 'lite' ? 'moonshotai/kimi-k2.6' : mode === 'free' - ? 'deepseek/deepseek-v4-pro' + ? FREEBUFF_MINIMAX_MODEL_ID : 'anthropic/claude-opus-4.7') - // Bundled free-mode definitions ship with the gemini-thinker spawnable + - // prompts; the CLI strips them at runtime if the user picks a fast model - // that doesn't benefit (e.g. MiniMax). Smart freebuff models (Kimi, - // DeepSeek) keep it so they can offload deeper reasoning. - const hasFreeGeminiThinker = isFree + // Smart freebuff model variants (Kimi, DeepSeek) can offload deeper + // reasoning. Fast MiniMax omits the extra round trip by construction. + const hasFreeGeminiThinker = + isFree && canFreebuffModelSpawnGeminiThinker(model) const defaultProviderOptions = isFree ? { data_collection: 'deny' as const, @@ -114,7 +118,7 @@ export function createBase2( isMax && 'editor-multi-prompt', 'tmux-cli', 'browser-use', - isFree && 'code-reviewer-lite', + isFree && freeCodeReviewerAgentId, isDefault && 'code-reviewer', isMax && 'code-reviewer-multi-prompt', hasFreeGeminiThinker && FREEBUFF_GEMINI_THINKER_AGENT_ID, @@ -183,7 +187,7 @@ Use the spawn_agents tool to spawn specialized agents to help you complete the u isMax && `- IMPORTANT: You must spawn the editor-multi-prompt agent to implement the changes after you have gathered all the context you need. You must spawn this agent for non-trivial changes, since it writes much better code than you would with the str_replace or write_file tools. Don't spawn the editor in parallel with context-gathering agents.`, isFree && - '- Spawn a code-reviewer-lite to review the changes after you have implemented the changes.', + `- Spawn a ${freeCodeReviewerAgentId} to review the changes after you have implemented the changes.`, '- Spawn bashers sequentially if the second command depends on the the first.', isDefault && '- Spawn a code-reviewer to review the changes after you have implemented the changes.', @@ -252,7 +256,7 @@ ${ isDefault ? `[ You spawn a code-reviewer, a basher to typecheck the changes, and another basher to run tests, all in parallel ]` : isFree - ? `[ You spawn a code-reviewer-lite to review the changes, a basher to typecheck the local changes, a basher to typecheck the whole project, and another basher to run tests, all in parallel ]` + ? `[ You spawn a ${freeCodeReviewerAgentId} to review the changes, a basher to typecheck the local changes, a basher to typecheck the whole project, and another basher to run tests, all in parallel ]` : isMax ? `[ You spawn a basher to typecheck the changes, and another basher to run tests, in parallel. Then, you spawn a code-reviewer-multi-prompt to review the changes. ]` : '[ You spawn a basher to typecheck the changes and another basher to run tests, all in parallel ]' @@ -262,7 +266,7 @@ ${ isDefault ? `[ You fix the issues found by the code-reviewer and type/test errors ]` : isFree - ? `[ You fix the issues found by the code-reviewer-lite and type/test errors ]` + ? `[ You fix the issues found by the ${freeCodeReviewerAgentId} and type/test errors ]` : isMax ? `[ You fix the issues found by the code-reviewer-multi-prompt and type/test errors ]` : '[ You fix the issues found by the type/test errors and spawn more bashers to confirm ]' @@ -305,6 +309,7 @@ ${PLACEHOLDER.GIT_CHANGES_PROMPT} hasFreeGeminiThinker, hasNoValidation, noAskUser, + freeCodeReviewerAgentId, }), stepPrompt: planOnly ? buildPlanOnlyStepPrompt({}) @@ -317,6 +322,7 @@ ${PLACEHOLDER.GIT_CHANGES_PROMPT} isFree, hasFreeGeminiThinker, noAskUser, + freeCodeReviewerAgentId, }), // handleSteps is serialized via .toString() and re-eval'd, so closure @@ -367,6 +373,7 @@ function buildImplementationInstructionsPrompt({ hasFreeGeminiThinker, hasNoValidation, noAskUser, + freeCodeReviewerAgentId, }: { isSonnet: boolean isFast: boolean @@ -376,6 +383,7 @@ function buildImplementationInstructionsPrompt({ hasFreeGeminiThinker: boolean hasNoValidation: boolean noAskUser: boolean + freeCodeReviewerAgentId: string }) { return `Act as a helpful assistant and freely respond to the user's request however would be most helpful to the user. Use your judgement to orchestrate the completion of the user's request using your specialized sub-agents and tools as needed. Take your time and be comprehensive. Don't surprise the user. For example, don't modify files if the user has not asked you to do so at least implicitly. @@ -407,7 +415,7 @@ ${buildArray( (isDefault || isMax) && `- Spawn a ${isDefault ? 'code-reviewer' : 'code-reviewer-multi-prompt'} to review the changes after you have implemented changes. (Skip this step only if the change is extremely straightforward and obvious.)`, isFree && - `- Spawn a code-reviewer-lite to review the changes after you have implemented changes. (Skip this step only if the change is extremely straightforward and obvious.)`, + `- Spawn a ${freeCodeReviewerAgentId} to review the changes after you have implemented changes. (Skip this step only if the change is extremely straightforward and obvious.)`, `- Inform the user that you have completed the task in one sentence or a few short bullet points.${isSonnet ? " Don't create any markdown summary files or example documentation files, unless asked by the user." : ''}`, !isFast && !noAskUser && @@ -424,6 +432,7 @@ function buildImplementationStepPrompt({ isFree, hasFreeGeminiThinker, noAskUser, + freeCodeReviewerAgentId, }: { isDefault: boolean isFast: boolean @@ -433,6 +442,7 @@ function buildImplementationStepPrompt({ isFree: boolean hasFreeGeminiThinker: boolean noAskUser: boolean + freeCodeReviewerAgentId: string }) { return buildArray( isMax && @@ -444,7 +454,7 @@ function buildImplementationStepPrompt({ (isDefault || isMax) && `You must spawn a ${isDefault ? 'code-reviewer' : 'code-reviewer-multi-prompt'} to review the changes after you have implemented the changes and in parallel with typechecking or testing.`, isFree && - `You must spawn a code-reviewer-lite to review the changes after you have implemented the changes and in parallel with typechecking or testing.`, + `You must spawn a ${freeCodeReviewerAgentId} to review the changes after you have implemented the changes and in parallel with typechecking or testing.`, `After completing the user request, summarize your changes in a sentence${isFast ? '' : ' or a few short bullet points'}.${isSonnet ? " Don't create any summary markdown files or example documentation files, unless asked by the user." : ''}.`, !isFast && !noAskUser && diff --git a/agents/reviewer/code-reviewer-deepseek.ts b/agents/reviewer/code-reviewer-deepseek.ts new file mode 100644 index 0000000000..451f2e6bb3 --- /dev/null +++ b/agents/reviewer/code-reviewer-deepseek.ts @@ -0,0 +1,11 @@ +import { publisher } from '../constants' +import type { SecretAgentDefinition } from '../types/secret-agent-definition' +import { createReviewer } from './code-reviewer' + +const definition: SecretAgentDefinition = { + id: 'code-reviewer-deepseek', + publisher, + ...createReviewer('deepseek/deepseek-v4-pro'), +} + +export default definition diff --git a/agents/reviewer/code-reviewer-kimi.ts b/agents/reviewer/code-reviewer-kimi.ts new file mode 100644 index 0000000000..c6eb10c600 --- /dev/null +++ b/agents/reviewer/code-reviewer-kimi.ts @@ -0,0 +1,11 @@ +import { publisher } from '../constants' +import type { SecretAgentDefinition } from '../types/secret-agent-definition' +import { createReviewer } from './code-reviewer' + +const definition: SecretAgentDefinition = { + id: 'code-reviewer-kimi', + publisher, + ...createReviewer('moonshotai/kimi-k2.6'), +} + +export default definition diff --git a/agents/reviewer/code-reviewer-lite.ts b/agents/reviewer/code-reviewer-lite.ts index 888cadf4f7..ee017c24e6 100644 --- a/agents/reviewer/code-reviewer-lite.ts +++ b/agents/reviewer/code-reviewer-lite.ts @@ -5,7 +5,7 @@ import { createReviewer } from './code-reviewer' const definition: SecretAgentDefinition = { id: 'code-reviewer-lite', publisher, - ...createReviewer('moonshotai/kimi-k2.6'), + ...createReviewer('minimax/minimax-m2.7'), } export default definition diff --git a/cli/src/__tests__/integration/local-agents.test.ts b/cli/src/__tests__/integration/local-agents.test.ts index e023a1dff8..b7444a87b3 100644 --- a/cli/src/__tests__/integration/local-agents.test.ts +++ b/cli/src/__tests__/integration/local-agents.test.ts @@ -3,17 +3,6 @@ import os from 'os' import path from 'path' import { validateAgents } from '@codebuff/sdk' -import { - FREEBUFF_GEMINI_THINKER_AGENT_ID, - FREEBUFF_GEMINI_THINKER_INSTRUCTIONS_PROMPT, - FREEBUFF_GEMINI_THINKER_STEP_PROMPT, - FREEBUFF_GEMINI_THINKER_SYSTEM_INSTRUCTION, -} from '@codebuff/common/constants/freebuff-gemini-thinker' -import { - FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID, - FREEBUFF_KIMI_MODEL_ID, - FREEBUFF_MINIMAX_MODEL_ID, -} from '@codebuff/common/constants/freebuff-models' import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test' // Mock the logger to prevent analytics initialization errors in tests @@ -31,7 +20,6 @@ import { setProjectRoot, getProjectRoot } from '../../project-files' import { loadAgentDefinitions, loadLocalAgents, - configureFreebuffBaseAgentForModel, initializeAgentRegistry, findAgentsDirectory, getLoadedAgentsData, @@ -42,87 +30,6 @@ import { const MODEL_NAME = 'anthropic/claude-sonnet-4' -describe('configureFreebuffBaseAgentForModel', () => { - const makeBase2Free = () => ({ - id: 'base2-free', - spawnableAgents: ['file-picker', FREEBUFF_GEMINI_THINKER_AGENT_ID], - systemPrompt: [ - 'before', - FREEBUFF_GEMINI_THINKER_SYSTEM_INSTRUCTION, - 'after', - ].join('\n'), - instructionsPrompt: [ - 'before', - FREEBUFF_GEMINI_THINKER_INSTRUCTIONS_PROMPT, - 'after', - ].join('\n'), - stepPrompt: ['before', FREEBUFF_GEMINI_THINKER_STEP_PROMPT, 'after'].join( - '\n', - ), - }) - - test('keeps the Gemini thinker and prompt guidance for Kimi', () => { - const definition = makeBase2Free() - - configureFreebuffBaseAgentForModel(definition, FREEBUFF_KIMI_MODEL_ID) - - expect(definition.spawnableAgents).toContain( - FREEBUFF_GEMINI_THINKER_AGENT_ID, - ) - expect(definition.systemPrompt).toContain( - FREEBUFF_GEMINI_THINKER_SYSTEM_INSTRUCTION, - ) - expect(definition.instructionsPrompt).toContain( - FREEBUFF_GEMINI_THINKER_INSTRUCTIONS_PROMPT, - ) - expect(definition.stepPrompt).toContain(FREEBUFF_GEMINI_THINKER_STEP_PROMPT) - }) - - test('keeps the Gemini thinker and prompt guidance for DeepSeek', () => { - const definition = makeBase2Free() - - configureFreebuffBaseAgentForModel( - definition, - FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID, - ) - - expect(definition.spawnableAgents).toContain( - FREEBUFF_GEMINI_THINKER_AGENT_ID, - ) - expect(definition.systemPrompt).toContain( - FREEBUFF_GEMINI_THINKER_SYSTEM_INSTRUCTION, - ) - expect(definition.instructionsPrompt).toContain( - FREEBUFF_GEMINI_THINKER_INSTRUCTIONS_PROMPT, - ) - expect(definition.stepPrompt).toContain(FREEBUFF_GEMINI_THINKER_STEP_PROMPT) - }) - - test('removes only exact Gemini thinker prompt guidance for MiniMax', () => { - const definition = makeBase2Free() - definition.systemPrompt += - '\nUser text mentioning thinker-with-files-gemini should stay.' - - configureFreebuffBaseAgentForModel(definition, FREEBUFF_MINIMAX_MODEL_ID) - - expect(definition.spawnableAgents).not.toContain( - FREEBUFF_GEMINI_THINKER_AGENT_ID, - ) - expect(definition.systemPrompt).not.toContain( - FREEBUFF_GEMINI_THINKER_SYSTEM_INSTRUCTION, - ) - expect(definition.instructionsPrompt).not.toContain( - FREEBUFF_GEMINI_THINKER_INSTRUCTIONS_PROMPT, - ) - expect(definition.stepPrompt).not.toContain( - FREEBUFF_GEMINI_THINKER_STEP_PROMPT, - ) - expect(definition.systemPrompt).toContain( - 'User text mentioning thinker-with-files-gemini should stay.', - ) - }) -}) - const writeAgentFile = ( agentsDir: string, fileName: string, diff --git a/cli/src/hooks/use-send-message.ts b/cli/src/hooks/use-send-message.ts index cd66a8234d..b66e046fa0 100644 --- a/cli/src/hooks/use-send-message.ts +++ b/cli/src/hooks/use-send-message.ts @@ -5,13 +5,10 @@ import { createStreamController } from './stream-state' import { useChatStore } from '../state/chat-store' import { getFreebuffInstanceId } from './use-freebuff-session' import { getCodebuffClient } from '../utils/codebuff-client' -import { - AGENT_MODE_TO_ID, - AGENT_MODE_TO_COST_MODE, - IS_FREEBUFF, -} from '../utils/constants' +import { AGENT_MODE_TO_COST_MODE, IS_FREEBUFF } from '../utils/constants' import { createEventHandlerState } from '../utils/create-event-handler-state' import { createRunConfig } from '../utils/create-run-config' +import { getAgentIdForMode } from '../utils/freebuff-agent-selection' import { loadAgentDefinitions } from '../utils/local-agent-registry' import { logger } from '../utils/logger' import { @@ -81,7 +78,7 @@ const resolveAgent = ( ? agentDefinitions.find((definition) => definition.id === agentId) : undefined - return selectedAgentDefinition ?? agentId ?? AGENT_MODE_TO_ID[agentMode] + return selectedAgentDefinition ?? agentId ?? getAgentIdForMode(agentMode) } // Respect bash context, but avoid sending empty prompts when only images are attached. diff --git a/cli/src/utils/constants.ts b/cli/src/utils/constants.ts index 0b9cabed72..bc1d2e59ab 100644 --- a/cli/src/utils/constants.ts +++ b/cli/src/utils/constants.ts @@ -127,8 +127,9 @@ export const MAIN_AGENT_ID = 'main-agent' * Mapping from agent mode to agent ID. * Single source of truth for all agent modes (order = cycling order). * - * Freebuff maps LITE to the free-tier agent (base2-free) so it stays fully free; - * regular Codebuff maps LITE to base2-lite which charges credits normally. + * Freebuff resolves LITE through the selected freebuff model at send time; + * this fallback stays on base2-free for non-runtime callers. Regular + * Codebuff maps LITE to base2-lite which charges credits normally. */ export const AGENT_MODE_TO_ID = { DEFAULT: 'base2', @@ -152,4 +153,7 @@ export const AGENT_MODE_TO_COST_MODE = { LITE: IS_FREEBUFF ? 'free' : 'lite', MAX: 'max', PLAN: 'normal', -} as const satisfies Record +} as const satisfies Record< + AgentMode, + 'free' | 'lite' | 'normal' | 'max' | 'experimental' | 'ask' +> diff --git a/cli/src/utils/freebuff-agent-selection.ts b/cli/src/utils/freebuff-agent-selection.ts new file mode 100644 index 0000000000..094f0de0f1 --- /dev/null +++ b/cli/src/utils/freebuff-agent-selection.ts @@ -0,0 +1,12 @@ +import { getFreebuffRootAgentIdForModel } from '@codebuff/common/constants/free-agents' + +import { getSelectedFreebuffModel } from '../state/freebuff-model-store' +import { AGENT_MODE_TO_ID, IS_FREEBUFF, type AgentMode } from './constants' + +export function getAgentIdForMode(agentMode: AgentMode): string { + if (IS_FREEBUFF && agentMode === 'LITE') { + return getFreebuffRootAgentIdForModel(getSelectedFreebuffModel()) + } + + return AGENT_MODE_TO_ID[agentMode] +} diff --git a/cli/src/utils/local-agent-registry.ts b/cli/src/utils/local-agent-registry.ts index 9bc45c084f..1781e50db3 100644 --- a/cli/src/utils/local-agent-registry.ts +++ b/cli/src/utils/local-agent-registry.ts @@ -10,83 +10,15 @@ import { import type { MCPConfig } from '@codebuff/common/types/mcp' -import { FREE_MODE_AGENT_MODELS } from '@codebuff/common/constants/free-agents' -import { - FREEBUFF_GEMINI_THINKER_AGENT_ID, - FREEBUFF_GEMINI_THINKER_PROMPT_LINES, -} from '@codebuff/common/constants/freebuff-gemini-thinker' -import { - canFreebuffModelSpawnGeminiThinker, - FREEBUFF_MODELS, -} from '@codebuff/common/constants/freebuff-models' - import { getSelectedFreebuffModel } from '../state/freebuff-model-store' import { getProjectRoot } from '../project-files' -import { AGENT_MODE_TO_ID, IS_FREEBUFF, type AgentMode } from './constants' +import { IS_FREEBUFF, type AgentMode } from './constants' +import { getAgentIdForMode } from './freebuff-agent-selection' import { logger } from './logger' import * as bundledAgentsModule from '../agents/bundled-agents.generated' import type { AgentDefinition } from '@codebuff/common/templates/initial-agents-dir/types/agent-definition' -/** Agents whose hardcoded model gets swapped out for the user's currently - * selected freebuff model. Derived from the server's - * `FREE_MODE_AGENT_MODELS` — any agent whose allowlist contains every - * freebuff model is safe to retarget client-side without tripping the - * server's `free_mode_invalid_agent_model` rejection. */ -const FREEBUFF_MODEL_OVERRIDABLE_AGENT_IDS: ReadonlySet = new Set( - Object.entries(FREE_MODE_AGENT_MODELS) - .filter(([, allowed]) => FREEBUFF_MODELS.every((m) => allowed.has(m.id))) - .map(([agentId]) => agentId), -) -const FREEBUFF_GEMINI_THINKER_PROMPT_LINE_SET = new Set( - FREEBUFF_GEMINI_THINKER_PROMPT_LINES, -) - -type ConfigurableFreebuffBaseAgent = { - id: string - spawnableAgents?: string[] - systemPrompt?: string - instructionsPrompt?: string - stepPrompt?: string -} - -function stripFreebuffGeminiThinkerPrompt(prompt: string): string { - return prompt - .split('\n') - .filter((line) => !FREEBUFF_GEMINI_THINKER_PROMPT_LINE_SET.has(line.trim())) - .join('\n') -} - -/** The bundled `base2-free` ships with the gemini-thinker spawnable + prompts - * so the smart freebuff models (Kimi, DeepSeek) can offload deeper reasoning. - * When the user picks a model that doesn't support gemini-thinker (e.g. - * MiniMax — fastest tier, extra round-trip would defeat that), strip the - * spawnable and the inlined prompt guidance so the agent doesn't try to call - * a tool we just removed. */ -export function configureFreebuffBaseAgentForModel( - def: ConfigurableFreebuffBaseAgent, - selectedModel: string, -): void { - if (def.id !== 'base2-free') return - if (canFreebuffModelSpawnGeminiThinker(selectedModel)) return - - const spawnableAgents = def.spawnableAgents ?? [] - def.spawnableAgents = spawnableAgents.filter( - (agentId) => agentId !== FREEBUFF_GEMINI_THINKER_AGENT_ID, - ) - - for (const key of [ - 'systemPrompt', - 'instructionsPrompt', - 'stepPrompt', - ] as const) { - const prompt = def[key] - if (typeof prompt === 'string') { - def[key] = stripFreebuffGeminiThinkerPrompt(prompt) - } - } -} - // ============================================================================ // Constants and types // ============================================================================ @@ -329,18 +261,10 @@ export const loadLocalAgents = ( // Filter bundled agents to only include subagents of the current mode's agent let filteredBundledAgents: LocalAgentInfo[] if (currentAgentMode) { - const currentAgentId = AGENT_MODE_TO_ID[currentAgentMode] + const currentAgentId = getAgentIdForMode(currentAgentMode) const currentAgentDef = bundledAgents[currentAgentId] - ? { - ...bundledAgents[currentAgentId], - spawnableAgents: [ - ...(bundledAgents[currentAgentId].spawnableAgents ?? []), - ], - } + ? bundledAgents[currentAgentId] : undefined - if (selectedFreebuffModel && currentAgentDef) { - configureFreebuffBaseAgentForModel(currentAgentDef, selectedFreebuffModel) - } const spawnableAgentIds = new Set(currentAgentDef?.spawnableAgents ?? []) // Only include bundled agents that are in the spawnableAgents list @@ -455,21 +379,6 @@ export const loadAgentDefinitions = (): AgentDefinition[] => { } } - // Override the model of free-mode agents to match the user's pick from the - // freebuff waiting room. Bundled definitions hardcode a free model; we swap in - // whatever the user chose so the chat-completions request body carries the - // matching model and the server-side session gate doesn't reject it as a - // model mismatch. - if (IS_FREEBUFF) { - const selectedModel = getSelectedFreebuffModel() - for (const def of definitions) { - if (FREEBUFF_MODEL_OVERRIDABLE_AGENT_IDS.has(def.id)) { - def.model = selectedModel - } - configureFreebuffBaseAgentForModel(def, selectedModel) - } - } - return definitions } diff --git a/common/src/__tests__/free-agents.test.ts b/common/src/__tests__/free-agents.test.ts index 6913f4834e..b59ff9d3a7 100644 --- a/common/src/__tests__/free-agents.test.ts +++ b/common/src/__tests__/free-agents.test.ts @@ -1,13 +1,70 @@ import { describe, expect, test } from 'bun:test' -import { FREEBUFF_GEMINI_PRO_MODEL_ID } from '../constants/freebuff-models' +import { + FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID, + FREEBUFF_GEMINI_PRO_MODEL_ID, + FREEBUFF_KIMI_MODEL_ID, + FREEBUFF_MINIMAX_MODEL_ID, +} from '../constants/freebuff-models' import { FREEBUFF_GEMINI_THINKER_AGENT_ID } from '../constants/freebuff-gemini-thinker' import { + getFreebuffRootAgentIdForModel, isFreebuffGeminiThinkerAgent, isFreeModeAllowedAgentModel, } from '../constants/free-agents' describe('free mode agent model allowlist', () => { + test('maps selectable freebuff models to concrete root agents', () => { + expect(getFreebuffRootAgentIdForModel(FREEBUFF_MINIMAX_MODEL_ID)).toBe( + 'base2-free', + ) + expect(getFreebuffRootAgentIdForModel(FREEBUFF_KIMI_MODEL_ID)).toBe( + 'base2-free-kimi', + ) + expect( + getFreebuffRootAgentIdForModel(FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID), + ).toBe('base2-free-deepseek') + }) + + test('allows each freebuff root agent only with its configured model', () => { + expect( + isFreeModeAllowedAgentModel('base2-free', FREEBUFF_MINIMAX_MODEL_ID), + ).toBe(true) + expect( + isFreeModeAllowedAgentModel('base2-free', FREEBUFF_KIMI_MODEL_ID), + ).toBe(false) + expect( + isFreeModeAllowedAgentModel('base2-free-kimi', FREEBUFF_KIMI_MODEL_ID), + ).toBe(true) + expect( + isFreeModeAllowedAgentModel( + 'base2-free-deepseek', + FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID, + ), + ).toBe(true) + }) + + test('allows each freebuff reviewer agent only with its configured model', () => { + expect( + isFreeModeAllowedAgentModel( + 'code-reviewer-lite', + FREEBUFF_MINIMAX_MODEL_ID, + ), + ).toBe(true) + expect( + isFreeModeAllowedAgentModel('code-reviewer-lite', FREEBUFF_KIMI_MODEL_ID), + ).toBe(false) + expect( + isFreeModeAllowedAgentModel('code-reviewer-kimi', FREEBUFF_KIMI_MODEL_ID), + ).toBe(true) + expect( + isFreeModeAllowedAgentModel( + 'code-reviewer-deepseek', + FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID, + ), + ).toBe(true) + }) + test('allows the browser-use subagent with its bundled model', () => { expect( isFreeModeAllowedAgentModel( diff --git a/common/src/__tests__/freebuff-models.test.ts b/common/src/__tests__/freebuff-models.test.ts index c8a6dcba67..87ba034773 100644 --- a/common/src/__tests__/freebuff-models.test.ts +++ b/common/src/__tests__/freebuff-models.test.ts @@ -16,8 +16,8 @@ import { } from '../constants/freebuff-models' describe('freebuff model availability', () => { - test('defaults to DeepSeek V4 Pro (the smartest free model)', () => { - expect(DEFAULT_FREEBUFF_MODEL_ID).toBe(FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID) + test('defaults to MiniMax M2.7 for base2-free', () => { + expect(DEFAULT_FREEBUFF_MODEL_ID).toBe(FREEBUFF_MINIMAX_MODEL_ID) }) test('DeepSeek carries the data-collection warning so users see it before picking', () => { @@ -28,7 +28,9 @@ describe('freebuff model availability', () => { }) test('only smart freebuff models can spawn the gemini-thinker subagent', () => { - expect(canFreebuffModelSpawnGeminiThinker(FREEBUFF_KIMI_MODEL_ID)).toBe(true) + expect(canFreebuffModelSpawnGeminiThinker(FREEBUFF_KIMI_MODEL_ID)).toBe( + true, + ) expect( canFreebuffModelSpawnGeminiThinker(FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID), ).toBe(true) diff --git a/common/src/constants/free-agents.ts b/common/src/constants/free-agents.ts index 9d41abd899..8a299ccbd6 100644 --- a/common/src/constants/free-agents.ts +++ b/common/src/constants/free-agents.ts @@ -4,6 +4,9 @@ import { FREEBUFF_GEMINI_THINKER_AGENT_ID } from './freebuff-gemini-thinker' import { FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID, FREEBUFF_GEMINI_PRO_MODEL_ID, + FREEBUFF_GLM_MODEL_ID, + FREEBUFF_KIMI_MODEL_ID, + FREEBUFF_MINIMAX_MODEL_ID, SUPPORTED_FREEBUFF_MODELS, } from './freebuff-models' @@ -23,6 +26,8 @@ export const FREE_COST_MODE = 'free' as const */ export const FREEBUFF_ROOT_AGENT_IDS = [ 'base2-free', + 'base2-free-kimi', + 'base2-free-deepseek', 'base2-free-deepseek-v4', ] as const const FREEBUFF_ROOT_AGENT_ID_SET: ReadonlySet = new Set( @@ -32,6 +37,22 @@ const FREEBUFF_ALLOWED_MODEL_IDS = SUPPORTED_FREEBUFF_MODELS.map( (model) => model.id, ) +export const FREEBUFF_ROOT_AGENT_ID_BY_MODEL: Record = { + [FREEBUFF_MINIMAX_MODEL_ID]: 'base2-free', + [FREEBUFF_KIMI_MODEL_ID]: 'base2-free-kimi', + [FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID]: 'base2-free-deepseek', +} + +export const FREEBUFF_REVIEWER_AGENT_ID_BY_MODEL: Record = { + [FREEBUFF_MINIMAX_MODEL_ID]: 'code-reviewer-lite', + [FREEBUFF_KIMI_MODEL_ID]: 'code-reviewer-kimi', + [FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID]: 'code-reviewer-deepseek', +} + +export function getFreebuffRootAgentIdForModel(model: string): string { + return FREEBUFF_ROOT_AGENT_ID_BY_MODEL[model] ?? 'base2-free' +} + /** * Agents that are allowed to run in FREE mode. * Only these specific agents (and their expected models) get 0 credits in FREE mode. @@ -42,7 +63,9 @@ const FREEBUFF_ALLOWED_MODEL_IDS = SUPPORTED_FREEBUFF_MODELS.map( */ export const FREE_MODE_AGENT_MODELS: Record> = { // Root orchestrator - 'base2-free': new Set(FREEBUFF_ALLOWED_MODEL_IDS), + 'base2-free': new Set([FREEBUFF_MINIMAX_MODEL_ID, FREEBUFF_GLM_MODEL_ID]), + 'base2-free-kimi': new Set([FREEBUFF_KIMI_MODEL_ID]), + 'base2-free-deepseek': new Set([FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID]), 'base2-free-deepseek-v4': new Set([FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID]), // File exploration agents @@ -64,7 +87,12 @@ export const FREE_MODE_AGENT_MODELS: Record> = { 'editor-lite': new Set(FREEBUFF_ALLOWED_MODEL_IDS), // Code reviewer for free mode - 'code-reviewer-lite': new Set(FREEBUFF_ALLOWED_MODEL_IDS), + 'code-reviewer-lite': new Set([ + FREEBUFF_MINIMAX_MODEL_ID, + FREEBUFF_GLM_MODEL_ID, + ]), + 'code-reviewer-kimi': new Set([FREEBUFF_KIMI_MODEL_ID]), + 'code-reviewer-deepseek': new Set([FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID]), // Legacy: kept for the standalone gemini thinker agent if invoked directly. [FREEBUFF_GEMINI_THINKER_AGENT_ID]: new Set([FREEBUFF_GEMINI_PRO_MODEL_ID]), diff --git a/common/src/constants/freebuff-models.ts b/common/src/constants/freebuff-models.ts index 8bfaf7b767..434ed35f45 100644 --- a/common/src/constants/freebuff-models.ts +++ b/common/src/constants/freebuff-models.ts @@ -113,13 +113,12 @@ export type SupportedFreebuffModelId = (typeof SUPPORTED_FREEBUFF_MODELS)[number]['id'] export type FreebuffPremiumModelId = (typeof FREEBUFF_PREMIUM_MODEL_IDS)[number] -/** What new freebuff users see selected in the picker. DeepSeek is the - * smartest of the free options; the picker surfaces its data-collection - * caveat (`warning`) so users can opt out to Kimi if that's a concern. +/** What new freebuff users see selected in the picker. MiniMax is the + * fastest always-available option and backs the default base2-free agent. * Callers that need a guaranteed-available id for resolution / auto-fallbacks * should use FALLBACK_FREEBUFF_MODEL_ID instead. */ export const DEFAULT_FREEBUFF_MODEL_ID: FreebuffModelId = - FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID + FREEBUFF_MINIMAX_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 diff --git a/docs/freebuff-waiting-room.md b/docs/freebuff-waiting-room.md index a4a74468b6..9713538810 100644 --- a/docs/freebuff-waiting-room.md +++ b/docs/freebuff-waiting-room.md @@ -156,7 +156,7 @@ The final tick result carries a `queueDepthByModel` map and a single `skipped` r | Constant | Location | Default | Purpose | |---|---|---|---| | `ADMISSION_TICK_MS` | `config.ts` | 15000 | How often the ticker fires. Up to one user is admitted per model per tick. | -| `FREEBUFF_MODELS` | `common/src/constants/freebuff-models.ts` | `minimax-m2.7`, `glm-5.1` | Selectable models; each gets its own queue and admission slot. | +| `FREEBUFF_MODELS` | `common/src/constants/freebuff-models.ts` | `deepseek-v4-pro`, `kimi-k2.6`, `minimax-m2.7` | Selectable models; each gets its own queue and admission slot. | | `FIREWORKS_DEPLOYMENT_MAP` | `web/src/llm-api/fireworks-config.ts` | `glm-5.1` | Models with dedicated Fireworks deployments. Models not listed are treated as `healthy` (serverless fallback) — drop this default when they migrate to their own deployments. | | `HEALTH_CACHE_TTL_MS` | `fireworks-health.ts` | 25000 | Fleet probe cache TTL. Sits just under the Fireworks 30s exporter cadence and 6 req/min rate limit. | | `FREEBUFF_SESSION_LENGTH_MS` | env | 3_600_000 | Session lifetime | From 379244f826cd79931d136943175b10b4a4b370ae Mon Sep 17 00:00:00 2001 From: James Grugett Date: Wed, 6 May 2026 21:55:04 -0700 Subject: [PATCH 2/3] Remove legacy DeepSeek free agent alias --- agents/base2/base2-free-deepseek-v4.ts | 8 -------- agents/base2/base2-free.ts | 2 +- agents/reviewer/code-reviewer-lite.ts | 2 +- agents/reviewer/code-reviewer-minimax.ts | 11 +++++++++++ common/src/__tests__/free-agents.test.ts | 7 +++++-- common/src/constants/free-agents.ts | 6 ++---- .../chat/completions/__tests__/completions.test.ts | 12 ++++++------ 7 files changed, 26 insertions(+), 22 deletions(-) delete mode 100644 agents/base2/base2-free-deepseek-v4.ts create mode 100644 agents/reviewer/code-reviewer-minimax.ts diff --git a/agents/base2/base2-free-deepseek-v4.ts b/agents/base2/base2-free-deepseek-v4.ts deleted file mode 100644 index 4f6da0dc82..0000000000 --- a/agents/base2/base2-free-deepseek-v4.ts +++ /dev/null @@ -1,8 +0,0 @@ -import base2FreeDeepseek from './base2-free-deepseek' - -const definition = { - ...base2FreeDeepseek, - id: 'base2-free-deepseek-v4', - displayName: 'Buffy the DeepSeek V4 Free Orchestrator', -} -export default definition diff --git a/agents/base2/base2-free.ts b/agents/base2/base2-free.ts index 98b3dbd84e..ee3a4cca05 100644 --- a/agents/base2/base2-free.ts +++ b/agents/base2/base2-free.ts @@ -2,7 +2,7 @@ import { createBase2 } from './base2' const definition = { ...createBase2('free', { - freeCodeReviewerAgentId: 'code-reviewer-lite', + freeCodeReviewerAgentId: 'code-reviewer-minimax', }), id: 'base2-free', displayName: 'Buffy the Free Orchestrator', diff --git a/agents/reviewer/code-reviewer-lite.ts b/agents/reviewer/code-reviewer-lite.ts index ee017c24e6..888cadf4f7 100644 --- a/agents/reviewer/code-reviewer-lite.ts +++ b/agents/reviewer/code-reviewer-lite.ts @@ -5,7 +5,7 @@ import { createReviewer } from './code-reviewer' const definition: SecretAgentDefinition = { id: 'code-reviewer-lite', publisher, - ...createReviewer('minimax/minimax-m2.7'), + ...createReviewer('moonshotai/kimi-k2.6'), } export default definition diff --git a/agents/reviewer/code-reviewer-minimax.ts b/agents/reviewer/code-reviewer-minimax.ts new file mode 100644 index 0000000000..e962623e40 --- /dev/null +++ b/agents/reviewer/code-reviewer-minimax.ts @@ -0,0 +1,11 @@ +import { publisher } from '../constants' +import type { SecretAgentDefinition } from '../types/secret-agent-definition' +import { createReviewer } from './code-reviewer' + +const definition: SecretAgentDefinition = { + id: 'code-reviewer-minimax', + publisher, + ...createReviewer('minimax/minimax-m2.7'), +} + +export default definition diff --git a/common/src/__tests__/free-agents.test.ts b/common/src/__tests__/free-agents.test.ts index b59ff9d3a7..fc2cf2963b 100644 --- a/common/src/__tests__/free-agents.test.ts +++ b/common/src/__tests__/free-agents.test.ts @@ -47,12 +47,15 @@ describe('free mode agent model allowlist', () => { test('allows each freebuff reviewer agent only with its configured model', () => { expect( isFreeModeAllowedAgentModel( - 'code-reviewer-lite', + 'code-reviewer-minimax', FREEBUFF_MINIMAX_MODEL_ID, ), ).toBe(true) expect( - isFreeModeAllowedAgentModel('code-reviewer-lite', FREEBUFF_KIMI_MODEL_ID), + isFreeModeAllowedAgentModel( + 'code-reviewer-minimax', + FREEBUFF_KIMI_MODEL_ID, + ), ).toBe(false) expect( isFreeModeAllowedAgentModel('code-reviewer-kimi', FREEBUFF_KIMI_MODEL_ID), diff --git a/common/src/constants/free-agents.ts b/common/src/constants/free-agents.ts index 8a299ccbd6..eeb1a17e82 100644 --- a/common/src/constants/free-agents.ts +++ b/common/src/constants/free-agents.ts @@ -28,7 +28,6 @@ export const FREEBUFF_ROOT_AGENT_IDS = [ 'base2-free', 'base2-free-kimi', 'base2-free-deepseek', - 'base2-free-deepseek-v4', ] as const const FREEBUFF_ROOT_AGENT_ID_SET: ReadonlySet = new Set( FREEBUFF_ROOT_AGENT_IDS, @@ -44,7 +43,7 @@ export const FREEBUFF_ROOT_AGENT_ID_BY_MODEL: Record = { } export const FREEBUFF_REVIEWER_AGENT_ID_BY_MODEL: Record = { - [FREEBUFF_MINIMAX_MODEL_ID]: 'code-reviewer-lite', + [FREEBUFF_MINIMAX_MODEL_ID]: 'code-reviewer-minimax', [FREEBUFF_KIMI_MODEL_ID]: 'code-reviewer-kimi', [FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID]: 'code-reviewer-deepseek', } @@ -66,7 +65,6 @@ export const FREE_MODE_AGENT_MODELS: Record> = { 'base2-free': new Set([FREEBUFF_MINIMAX_MODEL_ID, FREEBUFF_GLM_MODEL_ID]), 'base2-free-kimi': new Set([FREEBUFF_KIMI_MODEL_ID]), 'base2-free-deepseek': new Set([FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID]), - 'base2-free-deepseek-v4': new Set([FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID]), // File exploration agents 'file-picker': new Set(['google/gemini-2.5-flash-lite']), @@ -87,7 +85,7 @@ export const FREE_MODE_AGENT_MODELS: Record> = { 'editor-lite': new Set(FREEBUFF_ALLOWED_MODEL_IDS), // Code reviewer for free mode - 'code-reviewer-lite': new Set([ + 'code-reviewer-minimax': new Set([ FREEBUFF_MINIMAX_MODEL_ID, FREEBUFF_GLM_MODEL_ID, ]), 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 d2c84fb6b9..360f9945c3 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 @@ -156,23 +156,23 @@ describe('/api/v1/chat/completions POST endpoint', () => { status: 'running', } } - if (runId === 'run-free-deepseek-v4') { + if (runId === 'run-free-deepseek') { return { - agent_id: 'base2-free-deepseek-v4', + agent_id: 'base2-free-deepseek', ancestor_run_ids: [], status: 'running', } } if (runId === 'run-reviewer-direct') { return { - agent_id: 'code-reviewer-lite', + agent_id: 'code-reviewer-minimax', ancestor_run_ids: [], status: 'running', } } if (runId === 'run-reviewer-child') { return { - agent_id: 'code-reviewer-lite', + agent_id: 'code-reviewer-minimax', ancestor_run_ids: ['run-free'], status: 'running', } @@ -821,7 +821,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { model: FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID, stream: false, codebuff_metadata: { - run_id: 'run-free-deepseek-v4', + run_id: 'run-free-deepseek', client_id: 'test-client-id-123', cost_mode: 'free', }, @@ -862,7 +862,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { model: FREEBUFF_GEMINI_PRO_MODEL_ID, stream: false, codebuff_metadata: { - run_id: 'run-free-deepseek-v4', + run_id: 'run-free-deepseek', client_id: 'test-client-id-123', cost_mode: 'free', }, From a7433bf58d1908ac5f5b21f4bec624f9a015a90e Mon Sep 17 00:00:00 2001 From: James Grugett Date: Thu, 7 May 2026 11:05:07 -0700 Subject: [PATCH 3/3] Remove unused lite editor agent --- agents/editor/editor-lite.ts | 9 --------- common/src/constants/free-agents.ts | 3 --- 2 files changed, 12 deletions(-) delete mode 100644 agents/editor/editor-lite.ts diff --git a/agents/editor/editor-lite.ts b/agents/editor/editor-lite.ts deleted file mode 100644 index 6dbb4bb3c6..0000000000 --- a/agents/editor/editor-lite.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createCodeEditor } from './editor' - -import type { AgentDefinition } from '../types/agent-definition' - -const definition: AgentDefinition = { - ...createCodeEditor({ model: 'kimi' }), - id: 'editor-lite', -} -export default definition diff --git a/common/src/constants/free-agents.ts b/common/src/constants/free-agents.ts index eeb1a17e82..e5b2fb0d1c 100644 --- a/common/src/constants/free-agents.ts +++ b/common/src/constants/free-agents.ts @@ -81,9 +81,6 @@ export const FREE_MODE_AGENT_MODELS: Record> = { // Command execution basher: new Set(['google/gemini-3.1-flash-lite-preview']), - // Editor for free mode - 'editor-lite': new Set(FREEBUFF_ALLOWED_MODEL_IDS), - // Code reviewer for free mode 'code-reviewer-minimax': new Set([ FREEBUFF_MINIMAX_MODEL_ID,