Skip to content
Merged
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
13 changes: 13 additions & 0 deletions agents/base2/base2-gemini-no-editor-evals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createBase2 } from './base2'

const definition = {
...createBase2('free', {
noAskUser: true,
model: 'google/gemini-3.1-pro-preview',
providerOptions: {},
}),
id: 'base2-gemini-no-editor-evals',
displayName: 'Buffy the Gemini Evals Orchestrator',
}

export default definition
22 changes: 14 additions & 8 deletions agents/base2/base2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,37 @@ export function createBase2(
hasNoValidation?: boolean
planOnly?: boolean
noAskUser?: boolean
model?: SecretAgentDefinition['model']
providerOptions?: SecretAgentDefinition['providerOptions']
},
): Omit<SecretAgentDefinition, 'id'> {
const {
hasNoValidation = mode === 'fast',
planOnly = false,
noAskUser = false,
model: modelOverride,
providerOptions,
} = options ?? {}
const isDefault = mode === 'default'
const isFast = mode === 'fast'
const isMax = mode === 'max'
const isFree = mode === 'free' || mode === 'lite'

const isSonnet = false
const model = isFree ? 'z-ai/glm-5.1' : 'anthropic/claude-opus-4.7'
const model =
modelOverride ?? (isFree ? 'z-ai/glm-5.1' : 'anthropic/claude-opus-4.7')
const defaultProviderOptions = isFree
? {
data_collection: 'deny' as const,
}
: {
only: ['amazon-bedrock'],
}

return {
publisher,
model,
providerOptions: isFree ? {
data_collection: 'deny',
} : {
only: ['amazon-bedrock'],
},
providerOptions: providerOptions ?? defaultProviderOptions,
displayName: 'Buffy the Orchestrator',
spawnerPrompt:
'Advanced base agent that orchestrates planning, editing, and reviewing for complex coding tasks',
Expand Down Expand Up @@ -150,8 +158,6 @@ 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 &&
'- Implement code changes using the str_replace or write_file tools directly.',
isFree &&
'- Spawn a code-reviewer-lite to review the changes after you have implemented the changes.',
'- Spawn bashers sequentially if the second command depends on the the first.',
isDefault &&
Expand Down
10 changes: 9 additions & 1 deletion cli/src/components/freebuff-model-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { Button } from './button'
import {
FALLBACK_FREEBUFF_MODEL_ID,
FREEBUFF_GEMINI_PRO_MODEL_ID,
FREEBUFF_GLM_MODEL_ID,
FREEBUFF_MODELS,
getFreebuffDeploymentAvailabilityLabel,
Expand All @@ -25,8 +26,15 @@ import {
import type { KeyEvent } from '@opentui/core'

const FREEBUFF_MODEL_SELECTOR_MODELS = [
...FREEBUFF_MODELS.filter(
(model) => model.id === FREEBUFF_GEMINI_PRO_MODEL_ID,
),
...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_GEMINI_PRO_MODEL_ID &&
model.id !== FREEBUFF_GLM_MODEL_ID,
),
]

/**
Expand Down
21 changes: 21 additions & 0 deletions common/src/__tests__/freebuff-models.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
import { describe, expect, test } from 'bun:test'

import {
FREEBUFF_GEMINI_PRO_MODEL_ID,
FREEBUFF_MODELS,
getFreebuffDeploymentAvailabilityLabel,
isFreebuffDeploymentHours,
isFreebuffModelAvailable,
} from '../constants/freebuff-models'

describe('freebuff model availability', () => {
test('includes Gemini 3.1 Pro as an always-available option', () => {
expect(FREEBUFF_MODELS.map((model) => model.id)).toContain(
FREEBUFF_GEMINI_PRO_MODEL_ID,
)
expect(
isFreebuffModelAvailable(
FREEBUFF_GEMINI_PRO_MODEL_ID,
new Date('2026-01-05T18:00:00Z'),
),
).toBe(true)
expect(
isFreebuffModelAvailable(
FREEBUFF_GEMINI_PRO_MODEL_ID,
new Date('2026-01-05T12:00:00Z'),
),
).toBe(true)
})

test('formats the close time in the user local timezone while deployment is open', () => {
expect(
getFreebuffDeploymentAvailabilityLabel(new Date('2026-01-05T18:00:00Z'), {
Expand Down
28 changes: 16 additions & 12 deletions common/src/constants/free-agents.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { parseAgentId } from '../util/agent-id-parsing'

import { FREEBUFF_MODELS } from './freebuff-models'

import type { CostMode } from './model-config'

/**
Expand All @@ -15,6 +17,10 @@ export const FREE_COST_MODE = 'free' as const
* every user's apparent activity.
*/
export const FREEBUFF_ROOT_AGENT_IDS = ['base2-free'] as const
const FREEBUFF_ROOT_AGENT_ID_SET: ReadonlySet<string> = new Set(
FREEBUFF_ROOT_AGENT_IDS,
)
const FREEBUFF_SELECTABLE_MODEL_IDS = FREEBUFF_MODELS.map((model) => model.id)

/**
* Agents that are allowed to run in FREE mode.
Expand All @@ -26,10 +32,7 @@ export const FREEBUFF_ROOT_AGENT_IDS = ['base2-free'] as const
*/
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',
]),
'base2-free': new Set(FREEBUFF_SELECTABLE_MODEL_IDS),

// File exploration agents
'file-picker': new Set(['google/gemini-2.5-flash-lite']),
Expand All @@ -44,16 +47,10 @@ export const FREE_MODE_AGENT_MODELS: Record<string, Set<string>> = {
'basher': new Set(['google/gemini-3.1-flash-lite-preview']),

// Editor for free mode
'editor-lite': new Set([
'minimax/minimax-m2.7',
'z-ai/glm-5.1',
]),
'editor-lite': new Set(FREEBUFF_SELECTABLE_MODEL_IDS),

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

/**
Expand Down Expand Up @@ -87,6 +84,13 @@ export function isFreeMode(costMode: CostMode | string | undefined): boolean {
return costMode === FREE_COST_MODE
}

export function isFreebuffRootAgent(fullAgentId: string): boolean {
const { publisherId, agentId } = parseAgentId(fullAgentId)
if (!agentId) return false
if (publisherId && publisherId !== 'codebuff') return false
return FREEBUFF_ROOT_AGENT_ID_SET.has(agentId)
}

/**
* Check if a specific agent is allowed to use a specific model in FREE mode.
* This is the strictest check - validates both the agent AND model combination.
Expand Down
7 changes: 7 additions & 0 deletions common/src/constants/freebuff-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +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_GEMINI_PRO_MODEL_ID = 'google/gemini-3.1-pro-preview'
export const FREEBUFF_GLM_MODEL_ID = 'z-ai/glm-5.1'
export const FREEBUFF_MINIMAX_MODEL_ID = 'minimax/minimax-m2.7'
const FREEBUFF_EASTERN_TIMEZONE = 'America/New_York'
Expand All @@ -40,6 +41,12 @@ interface LocalTimeFormatOptions {
}

export const FREEBUFF_MODELS = [
{
id: FREEBUFF_GEMINI_PRO_MODEL_ID,
displayName: 'Gemini 3.1 Pro',
tagline: 'Deepest, 1/day',
availability: 'always',
},
{
id: FREEBUFF_MINIMAX_MODEL_ID,
displayName: 'MiniMax M2.7',
Expand Down
1 change: 1 addition & 0 deletions common/src/types/contracts/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type GetUserInfoFromApiKeyFn = <T extends UserColumn>(

type AgentRun = {
agent_id: string
ancestor_run_ids: string[]
status: 'running' | 'completed' | 'failed' | 'cancelled'
}
export type AgentRunColumn = keyof AgentRun
Expand Down
13 changes: 6 additions & 7 deletions common/src/types/freebuff-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@
/**
* Per-model usage counter surfaced to the CLI so the waiting-room UI can
* render "N of M sessions used" alongside queue/active state. Present when
* the joined model has a rate limit applied (today: GLM 5.1 with 5 admits
* per 12-hour window). `recentCount` is the number of admissions inside
* `windowHours` at the time the response was produced — see also the
* standalone `rate_limited` status for the reject path.
* the joined model has a rate limit applied. `recentCount` is the number of
* admissions inside `windowHours` at the time the response was produced —
* see also the standalone `rate_limited` status for the reject path.
*/
export interface FreebuffSessionRateLimit {
model: string
Expand Down Expand Up @@ -72,7 +71,7 @@ export type FreebuffSessionServerResponse =
queueDepthByModel: Record<string, number>
estimatedWaitMs: number
queuedAt: string
/** Rate-limit quota for rate-limited models (GLM 5.1 today). Absent
/** Rate-limit quota for rate-limited models. Absent
* for unlimited models or when the status was produced outside the
* rate-limit check path (e.g. pure read via GET). */
rateLimit?: FreebuffSessionRateLimit
Expand All @@ -85,7 +84,7 @@ export type FreebuffSessionServerResponse =
admittedAt: string
expiresAt: string
remainingMs: number
/** Rate-limit quota for rate-limited models (GLM 5.1 today). Absent
/** Rate-limit quota for rate-limited models. Absent
* for unlimited models or when the status was produced outside the
* rate-limit check path (e.g. pure read via GET). */
rateLimit?: FreebuffSessionRateLimit
Expand Down Expand Up @@ -152,7 +151,7 @@ export type FreebuffSessionServerResponse =
}
| {
/** User has used up their per-model admission quota in the rolling
* window (GLM 5.1: 5 one-hour sessions per 12h). Returned from POST
* window. Returned from POST
* /session before the user is placed in the queue. `retryAfterMs` is
* the time until the oldest admission inside the window falls off
* and one quota slot opens up — clients should show the user when
Expand Down
Loading
Loading