@@ -7,30 +7,51 @@ import type { CostMode } from './model-config'
77 */
88export const FREE_COST_MODE = 'free' as const
99
10+ /**
11+ * Agents that are allowed to run in FREE mode.
12+ * Only these specific agents (and their expected models) get 0 credits in FREE mode.
13+ * This prevents abuse by users trying to use arbitrary agents for free.
14+ *
15+ * The mapping also specifies which models each agent is allowed to use in free mode.
16+ * If an agent uses a different model, it will be charged full credits.
17+ */
18+ export const FREE_MODE_AGENT_MODELS: Record<string, Set<string>> = {
19+ // Root orchestrator
20+ 'base2-free': new Set(['x-ai/grok-4.1-fast']),
21+
22+ // File exploration agents
23+ 'file-picker': new Set(['google/gemini-2.5-flash-lite']),
24+ 'file-picker-max': new Set(['x-ai/grok-4.1-fast']),
25+ 'file-lister': new Set(['x-ai/grok-4.1-fast']),
26+
27+ // Research agents
28+ 'researcher-web': new Set(['x-ai/grok-4.1-fast']),
29+ 'researcher-docs': new Set(['x-ai/grok-4.1-fast']),
30+
31+ // Command execution
32+ 'commander-lite': new Set(['x-ai/grok-4.1-fast']),
33+
34+ // Editor for free mode
35+ 'editor-glm': new Set(['z-ai/glm-4.7', 'z-ai/glm-4.6']),
36+ }
37+
38+ /**
39+ * Set of all agent IDs allowed in FREE mode.
40+ * Derived from FREE_MODE_AGENT_MODELS for quick lookups.
41+ */
42+ export const FREE_MODE_ALLOWED_AGENTS = new Set(Object.keys(FREE_MODE_AGENT_MODELS))
43+
1044/**
1145 * Models that are allowed in FREE mode.
12- * Only these cheap/fast models get 0 credits in FREE mode .
46+ * Derived from FREE_MODE_AGENT_MODELS - this is the union of all allowed models .
1347 * This prevents abuse by users trying to use expensive models for free.
1448 */
15- export const FREE_MODE_ALLOWED_MODELS = new Set([
16- // Grok models used by base2-free, commander-lite, file-lister, file-picker-max
17- 'x-ai/grok-4.1-fast',
18- 'x-ai/grok-4-fast', // researcher agents
19-
20- // Gemini flash models used by file-picker and other subagents
21- 'google/gemini-2.5-flash',
22- 'google/gemini-2.5-flash-lite',
23- 'google/gemini-2.5-flash-preview-09-2025',
24- 'google/gemini-2.5-flash-lite-preview-09-2025',
25-
26- // GPT models used by editor-gpt-5, thinker, context-pruner
27- 'openai/gpt-5.1',
28- 'openai/gpt-5.1-chat',
29- 'openai/gpt-5-mini',
30- ])
49+ export const FREE_MODE_ALLOWED_MODELS = new Set(
50+ Object.values(FREE_MODE_AGENT_MODELS).flatMap((models) => Array.from(models)),
51+ )
3152
3253/**
33- * Agents that don't charge credits.
54+ * Agents that don't charge credits when credits would be very small (<5) .
3455 *
3556 * These are typically lightweight utility agents that:
3657 * - Use cheap models (e.g., Gemini Flash)
@@ -39,6 +60,10 @@ export const FREE_MODE_ALLOWED_MODELS = new Set([
3960 *
4061 * Making them free avoids user confusion when they connect their own
4162 * Claude subscription (BYOK) but still see credit charges for non-Claude models.
63+ *
64+ * NOTE: This is separate from FREE_MODE_ALLOWED_AGENTS which is for the
65+ * explicit "free" cost mode. These agents get free credits only when
66+ * the cost would be trivial (<5 credits).
4267 */
4368export const FREE_TIER_AGENTS = new Set([
4469 'file-picker',
@@ -65,13 +90,83 @@ export function isFreeModeAllowedModel(model: string): boolean {
6590}
6691
6792/**
68- * Check if an agent should be free (no credit charge).
93+ * Check if an agent is allowed to run in FREE mode.
94+ * Validates both the agent ID and optionally the publisher.
95+ *
96+ * For security, we only allow:
97+ * - Internal agents (no publisher, e.g., 'base2-free')
98+ * - Codebuff-published agents (publisher === 'codebuff')
99+ *
100+ * This prevents attackers from creating agents with matching names
101+ * under different publishers to abuse free mode.
102+ */
103+ export function isFreeModeAllowedAgent(fullAgentId: string): boolean {
104+ const { publisherId, agentId } = parseAgentId(fullAgentId)
105+
106+ // Must have a valid agent ID
107+ if (!agentId) return false
108+
109+ // Must be in the allowed agents list
110+ if (!FREE_MODE_ALLOWED_AGENTS.has(agentId)) return false
111+
112+ // Must be either internal (no publisher) or from codebuff
113+ if (publisherId && publisherId !== 'codebuff') return false
114+
115+ return true
116+ }
117+
118+ /**
119+ * Check if a specific agent is allowed to use a specific model in FREE mode.
120+ * This is the strictest check - validates both the agent AND model combination.
121+ *
122+ * Returns true only if:
123+ * 1. The agent is allowed in free mode (isFreeModeAllowedAgent)
124+ * 2. The model is in that agent's allowed model set
125+ */
126+ export function isFreeModeAllowedAgentModel(
127+ fullAgentId: string,
128+ model: string,
129+ ): boolean {
130+ // First check if agent is allowed in free mode (includes publisher validation)
131+ if (!isFreeModeAllowedAgent(fullAgentId)) return false
132+
133+ // Parse to get the base agent ID for model lookup
134+ const { agentId } = parseAgentId(fullAgentId)
135+ if (!agentId) return false
136+
137+ // Get the allowed models for this agent
138+ const allowedModels = FREE_MODE_AGENT_MODELS[agentId]
139+ if (!allowedModels) return false
140+
141+ // Empty set means programmatic agent (no LLM calls expected)
142+ // For these, any model check should fail (they shouldn't be making LLM calls)
143+ if (allowedModels.size === 0) return false
144+
145+ return allowedModels.has(model)
146+ }
147+
148+ /**
149+ * Check if an agent should be free (no credit charge) for small requests.
150+ * This is separate from FREE mode - these agents get free credits only
151+ * when the cost would be trivial (<5 credits).
152+ *
69153 * Handles all agent ID formats:
70154 * - 'file-picker'
71155 * - 'file-picker@1.0.0'
72156 * - 'codebuff/file-picker@0.0.2'
73157 */
74158export function isFreeAgent(fullAgentId: string): boolean {
75- const { agentId } = parseAgentId(fullAgentId)
76- return agentId ? FREE_TIER_AGENTS.has(agentId) : false
159+ const { publisherId, agentId } = parseAgentId(fullAgentId)
160+
161+ // Must have a valid agent ID
162+ if (!agentId) return false
163+
164+ // Must be in the free tier agents list
165+ if (!FREE_TIER_AGENTS.has(agentId)) return false
166+
167+ // Must be either internal (no publisher) or from codebuff
168+ // This prevents publisher spoofing attacks
169+ if (publisherId && publisherId !== 'codebuff') return false
170+
171+ return true
77172}
0 commit comments