Skip to content

Commit 701e755

Browse files
Sync public snapshot from freebuff-private
Source: CodebuffAI/freebuff-private@eb39d80fc066af3e1b1f4e5ab600b0d1b482fe53
1 parent bcaabbd commit 701e755

9 files changed

Lines changed: 168 additions & 39 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Freebuff includes a curated model catalog. The regular picker currently offers:
3737
| **MiMo 2.6 Flash** | Full and limited access | The default everywhere; balanced performance with image support |
3838
| **GLM 5.3 Flash** | Full and limited access | Deepest reasoning, unmetered |
3939
| **DeepSeek V4.1 Flash** | Full and limited access | Fast coding and tool use, unmetered |
40+
| **DeepSeek V4.1 Flash Fast** | Full access | Fast mode: the same model on DeepSeek's own API, splitting work across parallel subagents and verifying the result; priced on DeepSeek's peak and off-peak hours |
4041
| **GPT-6 Luna** | Full access | Strong all-around with native images; runs on OpenAI flex capacity |
4142
| **MiMo 2.6 Pro** | Full access | Xiaomi's stronger reasoning model, with image support |
4243
| **Solar Mini 4** | Full and limited access | Upstage's fast, compact model; 524K context, text only |

‎README.zh-CN.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Freebuff 提供经过筛选的模型目录。常规模型选择器目前包括
3737
| **MiMo 2.6 Flash** | 完整和受限访问 | 所有模式下的默认模型;均衡性能并支持图像 |
3838
| **GLM 5.3 Flash** | 完整和受限访问 | 推理最深入,不消耗会话 |
3939
| **DeepSeek V4.1 Flash** | 完整和受限访问 | 快速编程和工具调用,不消耗会话 |
40+
| **DeepSeek V4.1 Flash Fast** | 完整访问 | 快速模式:同一模型运行于 DeepSeek 官方 API,将工作拆分给并行子智能体并自行验证结果;按 DeepSeek 的高峰和非高峰时段定价 |
4041
| **GPT-6 Luna** | 完整访问 | 综合能力强,原生支持图像;运行于 OpenAI flex 容量 |
4142
| **MiMo 2.6 Pro** | 完整访问 | 小米更强的推理模型,支持图像 |
4243
| **Solar Mini 4** | 完整和受限访问 | Upstage 的快速轻量模型;52.4 万上下文,仅支持文本 |

‎cli/src/components/__tests__/freebuff-model-selector.test.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ test.each([
141141
useFreebuffModelStore.getState().setSelectedModel(id)
142142
const setup = await renderSelector(40, undefined, 100, now)
143143
expect(setup.captureCharFrame()).toMatch(new RegExp(`│ +${before} Freebucks/hr`))
144+
// At peak, line 2 says when the off-peak price is back.
145+
expect(setup.captureCharFrame().includes('10 Freebucks/hr from')).toBe(before === 15)
144146
expect(setup.captureCharFrame()).not.toContain('Off-peak')
145147
expect(setup.captureCharFrame()).not.toContain('normally 15/hr')
146148
expect(setup.captureCharFrame()).not.toContain('Server fallback price notice')
@@ -151,6 +153,7 @@ test.each([
151153
})
152154
await setup.renderOnce()
153155
expect(setup.captureCharFrame()).toMatch(new RegExp(`│ +${after} Freebucks/hr`))
156+
expect(setup.captureCharFrame().includes('10 Freebucks/hr from')).toBe(after === 15)
154157
expect(setup.captureCharFrame()).not.toContain('Off-peak')
155158
expect(setup.captureCharFrame()).not.toContain('normally 15/hr')
156159
} finally {

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { freebucksOffPeakCopy } from '@codebuff/common/util/freebuff-off-peak-price'
12
import { isFreebucksPeakModel } from '@codebuff/common/util/freebuff-peak-price'
23
import { watchFreebucksPriceChanges } from '@codebuff/common/util/freebuff-price-changes'
34
import { freebucksListPriceFor } from '@codebuff/common/util/freebuff-price-order'
@@ -516,6 +517,15 @@ export const FreebuffModelSelector: React.FC<FreebuffModelSelectorProps> = ({
516517
warn: (freebucks?.balance ?? 0) < rowPrice,
517518
highlight: freebucksListPriceFor(freebucks, model.id) !== undefined,
518519
})
520+
// At peak, when the off-peak price is back, so a user can wait for it.
521+
// On the clock `freebucks` was resolved with, not `now`, which can lag.
522+
const resumes = freebucksOffPeakCopy(freebucks, model.id)?.resumes
523+
if (resumes) {
524+
details.push({
525+
text: `${freebucksPriceLabel(resumes.price)} from ${resumes.at}`,
526+
warn: false,
527+
})
528+
}
519529
}
520530
// Beside the price it qualifies, in the warning colour. The terminal has
521531
// no tooltip to hold the catalog's full sentence, so the row carries its

‎common/src/constants/__tests__/freebuff-limited-subscriber.test.ts‎

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import {
1717
resolveFreebuffWebModelForLimitedTier,
1818
FREEBUFF_GEMINI_38_FLASH_MODEL_ID,
1919
FREEBUFF_MIMO_V26_PRO_MODEL_ID,
20-
FREEBUFF_ENABLE_FAST_MODE_IN_UI,
20+
FREEBUFF_FULL_ACCESS_ONLY_MODEL_IDS,
21+
isFreebuffModelAllowedForAccessTier,
2122
} from '../freebuff-models'
2223
import {
2324
FREEBUFF_SUBSCRIPTION_MODEL_IDS,
@@ -160,15 +161,12 @@ describe('paid plans at limited access', () => {
160161
// Pinned so that adding a row to a plan is a decision somebody states
161162
// here, not a diff that passes quietly.
162163
// MiMo 2.6 Pro joined on 2026-09-21, on Luna's terms (plan-only at
163-
// limited access, Freebucks at full). DeepSeek Flash fast mode joined on
164-
// 2026-09-26 on the same terms; Muse Spark 1.3 joined on 2026-09-28,
164+
// limited access, Freebucks at full). Muse Spark 1.3 joined on 2026-09-28,
165165
// paid-only on every surface, and GPT-6.1 Sol on 2026-09-29 on the same
166166
// terms. Kimi K3 (CrofAI) left on 2026-10-01: its provider is retired and
167-
// no picker lists it.
168-
// The fast row joins the plan with its picker row, behind the UI switch.
169-
expect(FREEBUFF_SUBSCRIPTION_MODEL_IDS).toHaveLength(
170-
FREEBUFF_ENABLE_FAST_MODE_IN_UI ? 8 : 7,
171-
)
167+
// no picker lists it. DeepSeek Flash fast mode is NOT a plan model: it is
168+
// full access only (see the test below).
169+
expect(FREEBUFF_SUBSCRIPTION_MODEL_IDS).toHaveLength(7)
172170
})
173171

174172
test('a limited-tier plan-only row is never free at limited access', () => {
@@ -188,6 +186,42 @@ describe('paid plans at limited access', () => {
188186
expect(FREEBUFF_LIMITED_TIER_PLAN_ONLY_MODEL_IDS.length).toBeGreaterThan(0)
189187
})
190188

189+
test('a full-access-only row is open at full access and absent at limited access, plan or not', () => {
190+
expect(FREEBUFF_FULL_ACCESS_ONLY_MODEL_IDS.length).toBeGreaterThan(0)
191+
for (const model of FREEBUFF_FULL_ACCESS_ONLY_MODEL_IDS) {
192+
for (const paid of [false, true]) {
193+
expect(
194+
isFreebuffSessionModelAllowedForAccessTier(model, 'full', paid),
195+
).toBe(true)
196+
expect(isFreebuffModelAllowedForAccessTier(model, 'full', paid)).toBe(
197+
true,
198+
)
199+
expect(
200+
getFreebuffModelsForAccessTier('full', paid).map((m) => m.id),
201+
).toContain(model)
202+
203+
expect(
204+
isFreebuffSessionModelAllowedForAccessTier(model, 'limited', paid),
205+
).toBe(false)
206+
expect(
207+
isFreebuffModelAllowedForAccessTier(model, 'limited', paid),
208+
).toBe(false)
209+
expect(isFreebuffWebModelAllowedForLimitedTier(model, paid)).toBe(false)
210+
expect(
211+
resolveFreebuffSessionModelForAccessTier(model, 'limited', {
212+
hasPaidSubscription: paid,
213+
}),
214+
).toBe(LIMITED_FREEBUFF_MODEL_ID)
215+
expect(
216+
getFreebuffModelsForAccessTier('limited', paid).map((m) => m.id),
217+
).not.toContain(model)
218+
}
219+
// Not listed locked at limited access either: a plan cannot unlock it.
220+
expect(FREEBUFF_LIMITED_TIER_PLAN_ONLY_MODEL_IDS).not.toContain(model)
221+
expect(FREEBUFF_SUBSCRIPTION_MODEL_IDS).not.toContain(model)
222+
}
223+
})
224+
191225
test('every plan model resolves in the Web catalog', () => {
192226
// The plans page renders the plan lineup via getFreebuffWebModel, which
193227
// FALLS BACK to MiMo 2.5 for an id the Web catalog lacks — it would

‎common/src/constants/freebuff-models.ts‎

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,14 +1138,14 @@ export const MUSE_SPARK_FALLBACK_NOTICE =
11381138
* wired even when these models are hidden from the Freebuff picker. */
11391139
export const FREEBUFF_ENABLE_MIMO_MODELS_IN_UI = true
11401140
/** UI-only rollout switch for the DeepSeek Flash FAST MODE row
1141-
* (`deepseek/deepseek-v4-flash-fast`). Off: no picker on any surface lists
1142-
* the row (the CLI, Web and Desktop pickers read FREEBUFF_MODELS; the iOS and
1143-
* Android catalogs carry the same switch by hand), while everything behind it
1144-
* stays wired — the id is SUPPORTED and admitted, priced on DeepSeek's clock, routed
1145-
* to DeepSeek direct alone, and runs the base3-fast harness — so a client
1146-
* that sends the id gets the real thing. Flip to true to ship the row; the
1147-
* tests that count picker rows follow this switch. */
1148-
export const FREEBUFF_ENABLE_FAST_MODE_IN_UI = false
1141+
* (`deepseek/deepseek-v4-flash-fast`). On since 2026-10-04, for full access
1142+
* only (FREEBUFF_FULL_ACCESS_ONLY_MODEL_IDS). Off: no picker on any surface
1143+
* lists the row (the CLI, Web and Desktop pickers read FREEBUFF_MODELS; the
1144+
* iOS and Android catalogs list it by hand), while everything behind it stays
1145+
* wired — the id is SUPPORTED and admitted at full access, priced on
1146+
* DeepSeek's clock, routed to DeepSeek direct alone, and runs the base3-fast
1147+
* harness. The tests that count picker rows follow this switch. */
1148+
export const FREEBUFF_ENABLE_FAST_MODE_IN_UI = true
11491149
/** UI-only rollout switch for the streak indicator in the waiting room. */
11501150
export const FREEBUFF_ENABLE_STREAK_IN_UI = true
11511151
/** Local/debug switch: force the localhost free-mode country bypass into
@@ -1747,16 +1747,17 @@ const DEEPSEEK_V4_FLASH_MODEL = {
17471747
* from that grant at admission and runs a single direct lane in the router
17481748
* (web/src/llm-api/deepseek-router.ts).
17491749
*
1750-
* Priced on DeepSeek's own clock (freebuff-freebucks.ts): 45 Freebucks in
1750+
* Priced on DeepSeek's own clock (freebuff-freebucks.ts): 50 Freebucks in
17511751
* the weekday window where DeepSeek direct doubles, 25 the rest of the time.
17521752
* The harness spends about 1.3x base3 per task, and DeepSeek direct bills
17531753
* 1.7x Luminal off-peak and 3.4x at peak on the same tokens.
17541754
*
17551755
* Everything else tracks the Flash row on purpose — the same DeepSeek data-use
17561756
* terms, the same effort ladder, and `multimodal: true` because the direct
1757-
* lane is the one lane that reads images. Premium and plan-only at limited
1758-
* access, on MiMo 2.6 Pro's terms: open to every full-access account, metered
1759-
* in Freebucks at its own price.
1757+
* lane is the one lane that reads images. Full access only
1758+
* (FREEBUFF_FULL_ACCESS_ONLY_MODEL_IDS): open to every full-access account on
1759+
* any plan, metered in Freebucks at its own price, and not offered at limited
1760+
* access even with a plan.
17601761
*/
17611762
const DEEPSEEK_V4_FLASH_FAST_MODEL = {
17621763
id: FREEBUFF_DEEPSEEK_V4_FLASH_FAST_MODEL_ID,
@@ -3061,9 +3062,9 @@ export const FREEBUFF_MODELS = [
30613062
GLM_V53_FLASH_MODEL,
30623063
DEEPSEEK_V4_FLASH_MODEL,
30633064
// FAST MODE beside the row it is a variant of (2026-09-26), behind
3064-
// FREEBUFF_ENABLE_FAST_MODE_IN_UI until it ships. Order here only matters
3065+
// FREEBUFF_ENABLE_FAST_MODE_IN_UI. Order here only matters
30653066
// off the meter: every picker sorts its rows cheapest first once prices
3066-
// arrive, and at 25-45 Freebucks this lands among the premium rows.
3067+
// arrive, and at 25-50 Freebucks this lands among the premium rows.
30673068
...(FREEBUFF_ENABLE_FAST_MODE_IN_UI ? [DEEPSEEK_V4_FLASH_FAST_MODEL] : []),
30683069
// GPT-6 LUNA TAKES 5.6'S SLOT (2026-09-22). Same position, same tagline,
30693070
// half the price on the flex lane; 5.6 left this list in the same change and
@@ -4247,12 +4248,8 @@ export const FREEBUFF_LIMITED_TIER_PLAN_ONLY_MODEL_IDS: readonly string[] =
42474248
Object.freeze([
42484249
FREEBUFF_GPT_6_LUNA_MODEL_ID,
42494250
FREEBUFF_MIMO_V26_PRO_MODEL_ID,
4250-
// Flash FAST MODE (2026-09-26), on MiMo 2.6 Pro's terms: three times the
4251-
// Flash price, so plan-only where the free pool is 25 a day. Behind the
4252-
// UI switch with the picker row: a plan cannot sell what no picker lists.
4253-
...(FREEBUFF_ENABLE_FAST_MODE_IN_UI
4254-
? [FREEBUFF_DEEPSEEK_V4_FLASH_FAST_MODEL_ID]
4255-
: []),
4251+
// Flash fast mode is not here: it is not offered at limited access at all
4252+
// (FREEBUFF_FULL_ACCESS_ONLY_MODEL_IDS).
42564253
// Muse Spark 1.2 is excluded: it is not offered at limited access at all
42574254
// (not even with a plan), only kept for released binaries at full access.
42584255
...FREEBUFF_PRO_ONLY_CATALOG_MODEL_IDS.filter(
@@ -4264,6 +4261,22 @@ export function isFreebuffLimitedTierPlanOnlyModelId(id: string): boolean {
42644261
return FREEBUFF_LIMITED_TIER_PLAN_ONLY_MODEL_IDS.includes(id)
42654262
}
42664263

4264+
/**
4265+
* Rows offered at FULL access only, on any plan or none. A limited-tier
4266+
* account does not see them, locked or otherwise, and a plan does not unlock
4267+
* them there, so they stay out of the geo-exempt set, the limited plan-only
4268+
* set and the plan-metered set alike.
4269+
*
4270+
* Flash fast mode (2026-10-04): a fan-out on DeepSeek direct at 25-50
4271+
* Freebucks an hour, which the limited tier's pool cannot carry.
4272+
*/
4273+
export const FREEBUFF_FULL_ACCESS_ONLY_MODEL_IDS: readonly string[] =
4274+
Object.freeze([FREEBUFF_DEEPSEEK_V4_FLASH_FAST_MODEL_ID])
4275+
4276+
export function isFreebuffFullAccessOnlyModelId(id: string): boolean {
4277+
return FREEBUFF_FULL_ACCESS_ONLY_MODEL_IDS.includes(id)
4278+
}
4279+
42674280
export const FREEBUFF_WEB_GEO_EXEMPT_MODEL_IDS: readonly string[] = [
42684281
...new Set<string>([
42694282
...LIMITED_FREEBUFF_MODEL_IDS,
@@ -4274,7 +4287,8 @@ export const FREEBUFF_WEB_GEO_EXEMPT_MODEL_IDS: readonly string[] = [
42744287
// This is deliberately broader than the global Pro set. Luna is free
42754288
// from the premium pool at full access but still plan-locked here;
42764289
// Gemini 3.8 Flash is plan-only at every tier.
4277-
!isFreebuffLimitedTierPlanOnlyModelId(model.id),
4290+
!isFreebuffLimitedTierPlanOnlyModelId(model.id) &&
4291+
!isFreebuffFullAccessOnlyModelId(model.id),
42784292
).map((model) => model.id),
42794293
]),
42804294
]
@@ -4557,12 +4571,10 @@ export const FREEBUFF_PLAN_METERED_CATALOG_MODEL_IDS: readonly string[] =
45574571
// With Luna: this list is what widens the limited tier for a subscriber,
45584572
// so a plan-only row missing here would be offered and then coerced away.
45594573
FREEBUFF_MIMO_V26_PRO_MODEL_ID,
4560-
// Flash fast mode (2026-09-26), for the same reason as MiMo 2.6 Pro —
4561-
// and, like the picker row, only once FREEBUFF_ENABLE_FAST_MODE_IN_UI is
4562-
// on: a plan can only cover a model a picker offers.
4563-
...(FREEBUFF_ENABLE_FAST_MODE_IN_UI
4564-
? [FREEBUFF_DEEPSEEK_V4_FLASH_FAST_MODEL_ID]
4565-
: []),
4574+
// Flash fast mode is not here: this list is what widens the limited tier
4575+
// for a subscriber, and fast mode is full access only
4576+
// (FREEBUFF_FULL_ACCESS_ONLY_MODEL_IDS). A full-access subscriber pays for
4577+
// it in Freebucks like everyone else at full access.
45664578
// Muse Spark 1.3 (2026-09-28): the plans page promises every plan model in
45674579
// every country, so a limited-region subscriber must be able to open it.
45684580
// (1.2 is deliberately absent: it survives only for released binaries at

‎common/src/util/__tests__/freebuff-off-peak-price.test.ts‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,50 @@ test('does not label a stale regular-price quote as discounted', () => {
8181
)!
8282
expect(copy.active).toBe(false)
8383
expect(copy.tooltip).toContain('10 Freebucks/hour')
84+
// The policy says off-peak, so the next change is the window's END: no
85+
// promise of a drop then.
86+
expect(copy.resumes).toBeUndefined()
87+
})
88+
89+
test('at peak, says when the off-peak price is back, with the weekday when it is not today', () => {
90+
// DeepSeek's clock: off-peak 10:00-00:00 UTC daily and all Beijing weekend.
91+
const fast = {
92+
prices: { fast: 50 },
93+
offPeak: {
94+
fast: {
95+
startHourUtc: 10,
96+
endHourUtc: 0,
97+
weekendsOffPeak: true,
98+
price: 25,
99+
regularPrice: 50,
100+
},
101+
},
102+
}
103+
// Monday 01:00 UTC is Sunday 6 PM in Los Angeles; the drop is Monday there.
104+
const sunday = freebucksOffPeakCopy(fast, 'fast', {
105+
now: Date.parse('2026-10-05T01:00:00Z'),
106+
timeZone: 'America/Los_Angeles',
107+
})!
108+
expect(sunday.resumes).toEqual({ price: 25, at: 'Mon 3:00 AM PDT' })
109+
expect(sunday.tooltip).toStartWith(
110+
'Back to 25 Freebucks/hour at Mon 3:00 AM PDT. Off-peak: 25 Freebucks/hour, daily',
111+
)
112+
// Same local day: the time alone.
113+
expect(
114+
freebucksOffPeakCopy(fast, 'fast', {
115+
now: Date.parse('2026-10-05T08:00:00Z'),
116+
timeZone: 'America/Los_Angeles',
117+
})!.resumes,
118+
).toEqual({ price: 25, at: '3:00 AM PDT' })
119+
// Off-peak: nothing to wait for.
120+
const offPeak = freebucksOffPeakCopy(
121+
{ ...fast, prices: { fast: 25 } },
122+
'fast',
123+
{
124+
now: Date.parse('2026-10-05T12:00:00Z'),
125+
timeZone: 'America/Los_Angeles',
126+
},
127+
)!
128+
expect(offPeak.resumes).toBeUndefined()
129+
expect(offPeak.tooltip).toStartWith('Off-peak:')
84130
})

‎common/src/util/freebuff-off-peak-price.ts‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function freebucksOffPeakCopy(
1818
const offer = info?.offPeak?.[modelId]
1919
if (!offer || info?.prices[modelId] === undefined) return undefined
2020

21-
const { start, end } = offPeakPriceAt(offer, now)
21+
const { start, end, nextChangeAt, price } = offPeakPriceAt(offer, now)
2222
const zone = resolveWindowTimeZone(timeZone)
2323
const fmt = new Intl.DateTimeFormat(undefined, {
2424
hour: 'numeric',
@@ -33,13 +33,35 @@ export function freebucksOffPeakCopy(
3333
: ''
3434
// The resolved quote owns the badge too; do not run a second pricing clock.
3535
const active = info.prices[modelId] === offer.price
36+
// At peak, when the off-peak price is back, so a reader can choose to wait.
37+
// Only when the policy agrees it is peak: a stale quote must not promise a
38+
// drop at what is really the END of the off-peak window.
39+
const resumes =
40+
!active && price !== offer.price
41+
? { price: offer.price, at: formatResumeAt(nextChangeAt, now, zone) }
42+
: undefined
3643
return {
3744
active,
3845
badge: 'Off-peak',
39-
tooltip: `Off-peak: ${offer.price} Freebucks/hour, daily ${hours}${weekend}.`,
46+
resumes,
47+
tooltip: `${resumes ? `Back to ${resumes.price} Freebucks/hour at ${resumes.at}. ` : ''}Off-peak: ${offer.price} Freebucks/hour, daily ${hours}${weekend}.`,
4048
}
4149
}
4250

51+
/** "3:00 AM PDT" when `at` falls on the reader's today, "Mon 3:00 AM PDT"
52+
* otherwise: a peak ending after local midnight must not read as tonight. */
53+
function formatResumeAt(at: number, now: number, zone: string) {
54+
const day = new Intl.DateTimeFormat('en-CA', { timeZone: zone })
55+
const today = day.format(at) === day.format(now)
56+
const time = new Intl.DateTimeFormat(undefined, {
57+
...(today ? {} : { weekday: 'short' as const }),
58+
hour: 'numeric',
59+
minute: '2-digit',
60+
timeZone: zone,
61+
}).format(at)
62+
return `${time} ${formatWindowTimeZoneLabel(new Date(at), zone)}`
63+
}
64+
4365
/** The weekend a policy with `weekendsOffPeak` means — Friday 16:00 to Sunday
4466
* 16:00 UTC, Saturday and Sunday in Beijing — as the reader's own clock shows
4567
* it, taken from the most recent such weekend so seasonal time applies. */

0 commit comments

Comments
 (0)