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
23 changes: 23 additions & 0 deletions cli/src/components/__tests__/choice-ad-banner.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, expect, test } from 'bun:test'

import { getAdDisplayLabel } from '../choice-ad-banner'

describe('choice ad banner display label', () => {
test('uses the display domain when the ad has a URL', () => {
expect(
getAdDisplayLabel({
title: 'Example Sponsor',
url: 'https://www.example.com/path',
}),
).toEqual({ text: 'example.com', variant: 'domain' })
})

test('uses the ad title when the ad has no URL', () => {
expect(
getAdDisplayLabel({
title: 'Example Sponsor',
url: '',
}),
).toEqual({ text: 'Example Sponsor', variant: 'title' })
})
})
37 changes: 32 additions & 5 deletions cli/src/components/choice-ad-banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ function truncateToLines(text: string, lineWidth: number, maxLines: number): str
return text.slice(0, maxChars - 1) + '…'
}

const extractDomain = (url: string): string => {
function truncateToWidth(text: string, width: number): string {
if (width <= 0) return ''
if (text.length <= width) return text
return text.slice(0, width - 1) + '…'
}

export const extractDomain = (url: string): string => {
try {
const parsed = new URL(url)
return parsed.hostname.replace(/^www\./, '')
Expand All @@ -34,6 +40,17 @@ const extractDomain = (url: string): string => {
}
}

export function getAdDisplayLabel(
ad: Pick<AdResponse, 'title' | 'url'>,
): { text: string; variant: 'domain' | 'title' } {
const url = ad.url.trim()
if (url) {
return { text: extractDomain(url), variant: 'domain' }
}

return { text: ad.title.trim() || 'Sponsored', variant: 'title' }
}

/**
* Calculate evenly distributed column widths that sum exactly to availableWidth.
* Distributes remainder pixels across the first N columns so there's no gap.
Expand Down Expand Up @@ -89,8 +106,10 @@ export const ChoiceAdBanner: React.FC<ChoiceAdBannerProps> = ({ ads, onImpressio
>
{visibleAds.map((ad, i) => {
const isHovered = hoveredIndex === i
const domain = extractDomain(ad.url)
const ctaText = ad.cta || ad.title || 'Learn more'
const label = getAdDisplayLabel(ad)
const labelMaxWidth = Math.max(0, widths[i] - ctaText.length - 5)
const labelText = truncateToWidth(label.text, labelMaxWidth)

return (
<Button
Expand Down Expand Up @@ -130,8 +149,16 @@ export const ChoiceAdBanner: React.FC<ChoiceAdBannerProps> = ({ ads, onImpressio
>
{` ${ctaText} `}
</text>
<text style={{ fg: theme.muted, attributes: TextAttributes.UNDERLINE }}>
{domain}
<text
style={{
fg: theme.muted,
attributes:
label.variant === 'domain'
? TextAttributes.UNDERLINE
: TextAttributes.BOLD,
}}
>
{labelText}
</text>

</box>
Expand All @@ -141,6 +168,6 @@ export const ChoiceAdBanner: React.FC<ChoiceAdBannerProps> = ({ ads, onImpressio

</box>

</box>
</box >
)
}
11 changes: 7 additions & 4 deletions common/src/__tests__/freebuff-models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('freebuff model availability', () => {
locale: 'en-US',
timeZone: 'America/Los_Angeles',
}),
).toBe('until 5:00 PM local')
).toBe('until 5:00 PM')
})

test('formats the next open time in the user local timezone while deployment is closed', () => {
Expand All @@ -21,16 +21,16 @@ describe('freebuff model availability', () => {
locale: 'en-US',
timeZone: 'America/Los_Angeles',
}),
).toBe('opens 6:00 AM local')
).toBe('opens 6:00 AM')
})

test('includes the weekday when the next opening is on a later local day', () => {
expect(
getFreebuffDeploymentAvailabilityLabel(new Date('2026-01-10T20:00:00Z'), {
getFreebuffDeploymentAvailabilityLabel(new Date('2026-01-11T03:00:00Z'), {
locale: 'en-US',
timeZone: 'America/Los_Angeles',
}),
).toBe('opens Mon 6:00 AM local')
).toBe('opens Sun 6:00 AM')
})

test('tracks deployment hours correctly across the open and close boundaries', () => {
Expand All @@ -46,5 +46,8 @@ describe('freebuff model availability', () => {
expect(isFreebuffDeploymentHours(new Date('2026-01-06T01:00:00Z'))).toBe(
false,
)
expect(isFreebuffDeploymentHours(new Date('2026-01-10T20:00:00Z'))).toBe(
true,
)
})
})
35 changes: 4 additions & 31 deletions common/src/constants/freebuff-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface FreebuffModelOption {
/** Server-facing fallback copy for APIs and provider errors that can't know
* the caller's local timezone. The CLI should render
* `getFreebuffDeploymentAvailabilityLabel()` instead. */
export const FREEBUFF_DEPLOYMENT_HOURS_LABEL = '9am ET-5pm PT'
export const FREEBUFF_DEPLOYMENT_HOURS_LABEL = '9am ET-5pm PT every day'
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 @@ -30,7 +30,6 @@ interface ZonedDateParts {
year: number
month: number
day: number
weekday: string
hour: number
minute: number
}
Expand Down Expand Up @@ -96,7 +95,6 @@ function getZonedParts(date: Date, timeZone: string): ZonedDateParts {
year: 'numeric',
month: '2-digit',
day: '2-digit',
weekday: 'short',
hour: '2-digit',
minute: '2-digit',
hourCycle: 'h23',
Expand All @@ -112,7 +110,6 @@ function getZonedParts(date: Date, timeZone: string): ZonedDateParts {
year,
month,
day,
weekday: value('weekday') ?? '',
hour,
minute,
}
Expand Down Expand Up @@ -165,34 +162,11 @@ function getUtcForZonedTime(
return guess
}

function isWeekend(
parts: Pick<ZonedDateParts, 'year' | 'month' | 'day'>,
): boolean {
const weekday = getWeekdayIndex(parts)
return weekday === 0 || weekday === 6
}

function getWeekdayIndex(
parts: Pick<ZonedDateParts, 'year' | 'month' | 'day'>,
): number {
return new Date(Date.UTC(parts.year, parts.month - 1, parts.day)).getUTCDay()
}

function getNextFreebuffDeploymentStart(now: Date): Date {
const easternNow = getZonedParts(now, FREEBUFF_EASTERN_TIMEZONE)
const weekday = getWeekdayIndex(easternNow)
const isBeforeTodayOpen = easternNow.hour < 9

const offset =
weekday === 6
? 2
: weekday === 0
? 1
: isBeforeTodayOpen
? 0
: weekday === 5
? 3
: 1
const offset = isBeforeTodayOpen ? 0 : 1

return getUtcForZonedTime(
addDaysToYmd(easternNow.year, easternNow.month, easternNow.day, offset),
Expand Down Expand Up @@ -241,17 +215,16 @@ export function getFreebuffDeploymentAvailabilityLabel(
): string {
if (isFreebuffDeploymentHours(now)) {
const closesAt = getCurrentFreebuffDeploymentEnd(now)
return `until ${formatLocalTime(closesAt, now, options)} local`
return `until ${formatLocalTime(closesAt, now, options)}`
}

const opensAt = getNextFreebuffDeploymentStart(now)
return `opens ${formatLocalTime(opensAt, now, options)} local`
return `opens ${formatLocalTime(opensAt, now, options)}`
}

export function isFreebuffDeploymentHours(now: Date = new Date()): boolean {
const eastern = getZonedParts(now, FREEBUFF_EASTERN_TIMEZONE)
const pacific = getZonedParts(now, FREEBUFF_PACIFIC_TIMEZONE)
if (eastern.weekday === 'Sat' || eastern.weekday === 'Sun') return false
return (
eastern.hour * 60 + eastern.minute >= 9 * 60 &&
pacific.hour * 60 + pacific.minute < 17 * 60
Expand Down
2 changes: 1 addition & 1 deletion freebuff/cli/release/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "freebuff",
"version": "0.0.49",
"version": "0.0.50",
"description": "The world's strongest free coding agent",
"license": "MIT",
"bin": {
Expand Down
64 changes: 53 additions & 11 deletions scripts/test-canopywave-long.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,62 @@
* to measure how well CanopyWave caches the shared prefix across turns.
*
* Usage:
* bun scripts/test-canopywave-long.ts
* bun scripts/test-canopywave-long.ts [model]
*
* Models:
* minimax (default) — minimax/minimax-m2.5
* kimi — moonshotai/kimi-k2.6
*/

export { }

const CANOPYWAVE_BASE_URL = 'https://inference.canopywave.io/v1'
const CANOPYWAVE_MODEL = 'minimax/minimax-m2.5'

// Pricing constants — same model as Fireworks/SiliconFlow
const INPUT_COST_PER_TOKEN = 0.30 / 1_000_000
const CACHED_INPUT_COST_PER_TOKEN = 0.03 / 1_000_000
const OUTPUT_COST_PER_TOKEN = 1.20 / 1_000_000
type ModelConfig = {
id: string
inputCostPerToken: number
cachedInputCostPerToken: number
outputCostPerToken: number
}

const MODEL_CONFIGS: Record<string, ModelConfig> = {
minimax: {
id: 'minimax/minimax-m2.5',
inputCostPerToken: 0.30 / 1_000_000,
cachedInputCostPerToken: 0.03 / 1_000_000,
outputCostPerToken: 1.20 / 1_000_000,
},
kimi: {
// Pricing is approximate — based on public Moonshot k2 rates; CanopyWave may differ.
id: 'moonshotai/kimi-k2.6',
inputCostPerToken: 0.60 / 1_000_000,
cachedInputCostPerToken: 0.15 / 1_000_000,
outputCostPerToken: 2.50 / 1_000_000,
},
}

const MODEL_ALIASES: Record<string, keyof typeof MODEL_CONFIGS> = {
'minimax/minimax-m2.5': 'minimax',
'moonshotai/kimi-k2.6': 'kimi',
'kimi-k2.6': 'kimi',
}

const DEFAULT_MODEL = 'minimax'
const modelArg = process.argv[2]
const modelKey = modelArg ? (MODEL_ALIASES[modelArg] ?? modelArg) : DEFAULT_MODEL
const MODEL = MODEL_CONFIGS[modelKey]
if (!MODEL) {
console.error(`❌ Unknown model: "${modelKey}". Available: ${Object.keys(MODEL_CONFIGS).join(', ')}`)
process.exit(1)
}
const CANOPYWAVE_MODEL = MODEL.id
const INPUT_COST_PER_TOKEN = MODEL.inputCostPerToken
const CACHED_INPUT_COST_PER_TOKEN = MODEL.cachedInputCostPerToken
const OUTPUT_COST_PER_TOKEN = MODEL.outputCostPerToken

const MAX_TOKENS = 100
// Higher cap accounts for reasoning models (e.g. kimi-k2.6) that consume tokens
// on hidden reasoning before producing visible content.
const MAX_TOKENS = 10000

function computeCost(usage: Record<string, unknown>): { cost: number; breakdown: string } {
const inputTokens = typeof usage.prompt_tokens === 'number' ? usage.prompt_tokens : 0
Expand All @@ -35,9 +77,9 @@ function computeCost(usage: Record<string, unknown>): { cost: number; breakdown:
const totalCost = inputCost + cachedCost + outputCost

const breakdown = [
`${nonCachedInput} non-cached input × $0.30/M = $${inputCost.toFixed(8)}`,
`${cachedTokens} cached input × $0.03/M = $${cachedCost.toFixed(8)}`,
`${outputTokens} output × $1.20/M = $${outputCost.toFixed(8)}`,
`${nonCachedInput} non-cached input × $${(INPUT_COST_PER_TOKEN * 1_000_000).toFixed(2)}/M = $${inputCost.toFixed(8)}`,
`${cachedTokens} cached input × $${(CACHED_INPUT_COST_PER_TOKEN * 1_000_000).toFixed(2)}/M = $${cachedCost.toFixed(8)}`,
`${outputTokens} output × $${(OUTPUT_COST_PER_TOKEN * 1_000_000).toFixed(2)}/M = $${outputCost.toFixed(8)}`,
`Total: $${totalCost.toFixed(8)}`,
].join('\n ')

Expand Down Expand Up @@ -275,7 +317,7 @@ async function main() {
console.log(`Base URL: ${CANOPYWAVE_BASE_URL}`)
console.log(`Max tokens: ${MAX_TOKENS} (low output per turn)`)
console.log(`Turns: ${TURN_PROMPTS.length}`)
console.log(`Pricing: $0.30/M input, $0.03/M cached, $1.20/M output`)
console.log(`Pricing: $${(INPUT_COST_PER_TOKEN * 1_000_000).toFixed(2)}/M input, $${(CACHED_INPUT_COST_PER_TOKEN * 1_000_000).toFixed(2)}/M cached, $${(OUTPUT_COST_PER_TOKEN * 1_000_000).toFixed(2)}/M output`)
console.log('='.repeat(60))
console.log()

Expand Down
4 changes: 2 additions & 2 deletions scripts/test-canopywave.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
export {}

const CANOPYWAVE_BASE_URL = 'https://inference.canopywave.io/v1'
const CANOPYWAVE_MODEL = 'minimax/minimax-m2.5'
const OPENROUTER_MODEL = 'minimax/minimax-m2.5'
const CANOPYWAVE_MODEL = 'moonshotai/kimi-k2.6'
const OPENROUTER_MODEL = 'moonshotai/kimi-k2.6'

const testPrompt = 'Say "hello world" and nothing else.'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ describe('POST /api/v1/freebuff/session', () => {
expect(resp.status).toBe(409)
const body = await resp.json()
expect(body.status).toBe('model_unavailable')
expect(body.availableHours).toBe('9am ET-5pm PT')
expect(body.availableHours).toBe('9am ET-5pm PT every day')
expect(sessionDeps.rows.size).toBe(0)
})

Expand Down
6 changes: 3 additions & 3 deletions web/src/llm-api/__tests__/fireworks-deployment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ function createMockLogger(): Logger {

describe('Fireworks deployment routing', () => {
describe('deployment hours', () => {
it('is active from 9am ET until before 5pm PT on weekdays', () => {
it('is active from 9am ET until before 5pm PT every day', () => {
expect(isDeploymentHours(BEFORE_DEPLOYMENT_HOURS)).toBe(false)
expect(isDeploymentHours(IN_DEPLOYMENT_HOURS)).toBe(true)
expect(isDeploymentHours(AFTER_DEPLOYMENT_HOURS)).toBe(false)
expect(isDeploymentHours(WEEKDAY_AFTER_DEPLOYMENT_HOURS)).toBe(false)
})

it('is inactive on weekends', () => {
expect(isDeploymentHours(WEEKEND_DEPLOYMENT_HOURS)).toBe(false)
it('is active on weekends during deployment hours', () => {
expect(isDeploymentHours(WEEKEND_DEPLOYMENT_HOURS)).toBe(true)
})
})

Expand Down
2 changes: 1 addition & 1 deletion web/src/llm-api/fireworks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const FIREWORKS_MODEL_MAP: Record<string, string> = {
/** Flag to enable custom Fireworks deployments (set to false to use global API only) */
const FIREWORKS_USE_CUSTOM_DEPLOYMENT = true

/** Check if current time is within deployment hours: Mon-Fri, 9am ET to 5pm PT. */
/** Check if current time is within deployment hours: daily, 9am ET to 5pm PT. */
export function isDeploymentHours(now: Date = new Date()): boolean {
return isFreebuffDeploymentHours(now)
}
Expand Down
2 changes: 1 addition & 1 deletion web/src/server/free-session/__tests__/public-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ describe('requestSession', () => {
expect(state).toEqual({
status: 'model_unavailable',
requestedModel: 'z-ai/glm-5.1',
availableHours: '9am ET-5pm PT',
availableHours: '9am ET-5pm PT every day',
})
expect(deps.rows.size).toBe(0)
})
Expand Down
Loading