Skip to content

Commit 87ca560

Browse files
Sync public snapshot from freebuff-private
Source: CodebuffAI/freebuff-private@32f9209199b167ec6ff61e1132d5c60f2281f4f4
1 parent 00eec5a commit 87ca560

9 files changed

Lines changed: 154 additions & 12 deletions

File tree

‎bun.lock‎

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎cli/src/components/__tests__/freebuff-chat-controls.test.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ if (process.env.FREEBUFF_CHAT_CONTROLS_TEST !== '1') {
176176
useFreebuffChatStore.setState({
177177
admission: { phase: 'requested', model: luna, metadataChecked: true },
178178
})
179+
useFreebuffModelStore.setState({ hasExplicitPick: true }) // Luna was picked
179180
const setup = await mount()
180181
expect(useFreebuffChatStore.getState().admission?.phase).toBe('confirm')
181182
expect(setup.captureCharFrame()).toContain('20')

‎cli/src/components/__tests__/freebuff-offer-invariants.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,14 @@ describe('freebuff rows the CLI offers', () => {
153153
resolveFreebuffModelSelectionForSession(
154154
FREEBUFF_GPT_6_LUNA_MODEL_ID,
155155
paidSession,
156+
true,
156157
),
157158
).toBe(FREEBUFF_GPT_6_LUNA_MODEL_ID)
158159
expect(
159160
resolveFreebuffModelSelectionForSession(
160161
FREEBUFF_GPT_6_LUNA_MODEL_ID,
161162
unpaidSession,
163+
true,
162164
),
163165
).toBe(LIMITED_FREEBUFF_MODEL_ID)
164166
})

‎cli/src/hooks/__tests__/freebuff-chat-admission-gate.test.ts‎

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
1+
import {
2+
FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
3+
FREEBUFF_MIMO_V25_MODEL_ID,
4+
} from '@codebuff/common/constants/freebuff-models'
5+
import { createTestRenderer } from '@opentui/core/testing'
6+
import { createRoot, flushSync } from '@opentui/react'
17
import { describe, expect, test } from 'bun:test'
8+
import React from 'react'
29

3-
import { freebuffChatAdmissionMayStart } from '../use-freebuff-chat-admission'
10+
import { useFreebuffChatStore } from '../../state/freebuff-chat-store'
11+
import { useFreebuffModelStore } from '../../state/freebuff-model-store'
12+
import { useFreebuffSessionStore } from '../../state/freebuff-session-store'
13+
import {
14+
freebuffChatAdmissionMayStart,
15+
useFreebuffChatAdmission,
16+
} from '../use-freebuff-chat-admission'
417

518
describe('freebuffChatAdmissionMayStart', () => {
619
const ready = {
@@ -34,3 +47,38 @@ describe('freebuffChatAdmissionMayStart', () => {
3447
).toBe(false)
3548
})
3649
})
50+
51+
test('a send before the first session response runs the default it resolved', async () => {
52+
// The send captured the launch seed (MiMo); the response moved the selection
53+
// to Flash. `freebucks: null` stops admission at confirm, before the network.
54+
useFreebuffModelStore.setState({
55+
hasExplicitPick: false,
56+
selectedModel: FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
57+
})
58+
useFreebuffSessionStore.getState().setSession({
59+
status: 'none',
60+
accessTier: 'full',
61+
freebucks: null,
62+
} as never)
63+
useFreebuffChatStore.setState({
64+
admission: {
65+
phase: 'requested',
66+
model: FREEBUFF_MIMO_V25_MODEL_ID,
67+
metadataChecked: true,
68+
},
69+
})
70+
const setup = await createTestRenderer({ width: 20, height: 2 })
71+
const root = createRoot(setup.renderer)
72+
const Harness = () => {
73+
useFreebuffChatAdmission(true)
74+
return React.createElement('text', null, 'x')
75+
}
76+
flushSync(() => root.render(React.createElement(Harness)))
77+
await setup.renderOnce()
78+
expect(useFreebuffChatStore.getState().admission).toMatchObject({
79+
phase: 'confirm',
80+
model: FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
81+
})
82+
flushSync(() => root.unmount())
83+
setup.renderer.destroy()
84+
})

‎cli/src/hooks/use-freebuff-chat-admission.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
type ChatAdmission,
1212
} from '../state/freebuff-chat-store'
1313
import { getFreebuffModelDirectory } from '../state/freebuff-catalog-store'
14+
import { useFreebuffModelStore } from '../state/freebuff-model-store'
1415
import { useFreebuffSessionStore } from '../state/freebuff-session-store'
1516
import {
1617
freebucksOf,
@@ -195,7 +196,12 @@ export function useFreebuffChatAdmission(enabled: boolean) {
195196
})
196197
return
197198
}
198-
const model = resolveFreebuffModelPickForSession(admission.model, session)
199+
// A send before the first session response captured the launch seed.
200+
const { hasExplicitPick, selectedModel } = useFreebuffModelStore.getState()
201+
const model = resolveFreebuffModelPickForSession(
202+
hasExplicitPick ? admission.model : selectedModel,
203+
session,
204+
)
199205
const intent = freebucksRowIntent(
200206
freebucksOf(session),
201207
model,

‎cli/src/hooks/use-freebuff-session.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
useFreebuffCatalogStore,
1515
} from '../state/freebuff-catalog-store'
1616
import {
17+
freebuffDefaultModelForSession,
1718
getSelectedFreebuffModel,
1819
persistFreebuffModelPick,
1920
useFreebuffModelStore,
@@ -345,8 +346,12 @@ export function resolveFreebuffModelPickForSession(
345346
export function resolveFreebuffModelSelectionForSession(
346347
selectedModel: string,
347348
session: FreebuffSessionResponse,
349+
hasExplicitPick: boolean,
348350
) {
349351
if (session.status === 'active') return session.model
352+
if (session.status === 'none' && !hasExplicitPick) {
353+
return freebuffDefaultModelForSession(session)
354+
}
350355
if (session.status === 'none' && session.accessTier === 'limited') {
351356
return resolveFreebuffModelPickForSession(selectedModel, session)
352357
}
@@ -587,6 +592,7 @@ export function useFreebuffSession({
587592
const resolvedModel = resolveFreebuffModelSelectionForSession(
588593
selectedModel,
589594
next,
595+
useFreebuffModelStore.getState().hasExplicitPick,
590596
)
591597
if (resolvedModel !== selectedModel) {
592598
useFreebuffModelStore.getState().setSelectedModel(resolvedModel)

‎cli/src/state/freebuff-model-store.ts‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,19 @@ import {
22
DEFAULT_FREEBUFF_MODEL_ID,
33
resolveAvailableFreebuffModel,
44
} from '@codebuff/common/constants/freebuff-models'
5+
import {
6+
getFreebucksInfo,
7+
getFreebuffServerAccessTier,
8+
getSubscriptionInfo,
9+
} from '@codebuff/common/types/freebuff-session'
10+
import { getFreebuffModelPresets } from '@codebuff/common/util/freebuff-model-presets'
11+
import { getFreebuffModelMeter } from '@codebuff/common/util/freebuff-session-pools'
512
import { create } from 'zustand'
613

714
import { getFreebuffModelDirectory } from './freebuff-catalog-store'
815
import {
916
loadFreebuffCatalogReasoningEfforts,
17+
loadFreebuffModelKeyPreference,
1018
loadFreebuffModelPreference,
1119
loadFreebuffReasoningEfforts,
1220
saveFreebuffCatalogReasoningEffort,
@@ -15,6 +23,7 @@ import {
1523
saveFreebuffReasoningEffort,
1624
} from '../utils/settings'
1725

26+
import type { FreebuffSessionResponse } from '../types/freebuff-session'
1827
import type { ReasoningEffort } from '@codebuff/common/constants/reasoning-effort'
1928

2029
/**
@@ -46,6 +55,8 @@ import type { ReasoningEffort } from '@codebuff/common/constants/reasoning-effor
4655
interface FreebuffModelStore {
4756
selectedModel: string
4857
setSelectedModel: (model: string) => void
58+
/** Until the user picks, the selection follows freebuffDefaultModelForSession. */
59+
hasExplicitPick: boolean
4960
/** Per-model effort overrides. A model absent from this map runs its catalog
5061
* default; see saveFreebuffReasoningEffort for why absence is the "default"
5162
* state rather than a stored null. */
@@ -56,12 +67,17 @@ interface FreebuffModelStore {
5667
) => void
5768
}
5869

70+
const savedFreebuffModelPick = loadFreebuffModelPreference()
71+
5972
export const useFreebuffModelStore = create<FreebuffModelStore>((set) => ({
6073
selectedModel: resolveAvailableFreebuffModel(
61-
loadFreebuffModelPreference() ?? DEFAULT_FREEBUFF_MODEL_ID,
74+
savedFreebuffModelPick ?? DEFAULT_FREEBUFF_MODEL_ID,
6275
),
6376
setSelectedModel: (model) =>
6477
set({ selectedModel: getFreebuffModelDirectory().resolveSelection(model) }),
78+
hasExplicitPick:
79+
savedFreebuffModelPick !== undefined ||
80+
loadFreebuffModelKeyPreference() !== undefined,
6581
reasoningEffortByModel: {
6682
...loadFreebuffReasoningEfforts(),
6783
...loadFreebuffCatalogReasoningEfforts(),
@@ -90,6 +106,27 @@ export function getSelectedFreebuffModel(): string {
90106
return useFreebuffModelStore.getState().selectedModel
91107
}
92108

109+
/** The Desktop and Web slider's Balanced stop, or the catalog's
110+
* recommendation when the meter cannot start it, as on Desktop and Web. */
111+
export function freebuffDefaultModelForSession(
112+
session: FreebuffSessionResponse,
113+
): string {
114+
const directory = getFreebuffModelDirectory()
115+
const accessTier = getFreebuffServerAccessTier(session)
116+
const freebucks = getFreebucksInfo(session)
117+
const balanced = getFreebuffModelPresets({
118+
accessTier,
119+
isSubscriber: Boolean(
120+
getSubscriptionInfo(session)?.tierId ?? freebucks?.planId,
121+
),
122+
balance: freebucks?.balance,
123+
}).find((preset) => preset.id === 'balanced')!
124+
const model = directory.resolveSelection(balanced.modelId)
125+
return getFreebuffModelMeter({ model, freebucks }).canStart
126+
? model
127+
: directory.recommendedModelId(accessTier)
128+
}
129+
93130
/**
94131
* The user's effort override for a model, or null when they have none.
95132
*
@@ -133,6 +170,7 @@ function legacyEffortForCatalogKey(
133170
* key; see `saveFreebuffModelPreference`).
134171
*/
135172
export function persistFreebuffModelPick(model: string): void {
173+
useFreebuffModelStore.setState({ hasExplicitPick: true })
136174
if (getFreebuffModelDirectory().row(model)?.key === model) {
137175
saveFreebuffModelKeyPreference(model)
138176
} else {

‎cli/src/utils/__tests__/freebuff-model-catalog.test.ts‎

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from '../../state/freebuff-catalog-store'
1919
import { useFreebuffChatStore } from '../../state/freebuff-chat-store'
2020
import {
21+
freebuffDefaultModelForSession,
2122
getEffectiveFreebuffReasoningEffort,
2223
getSelectedFreebuffModel,
2324
persistFreebuffModelPick,
@@ -428,14 +429,56 @@ describe('session picks in catalog mode', () => {
428429
test('an active session re-keyed by the server wins the selection', () => {
429430
setFreebuffCatalog(CATALOG)
430431
expect(
431-
resolveFreebuffModelSelectionForSession('m-mimo', {
432-
status: 'active',
433-
model: 'm-flash',
434-
} as never),
432+
resolveFreebuffModelSelectionForSession(
433+
'm-mimo',
434+
{ status: 'active', model: 'm-flash' } as never,
435+
true,
436+
),
435437
).toBe('m-flash')
436438
})
437439
})
438440

441+
describe('the default for an account with no explicit pick', () => {
442+
const FLASH = FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID
443+
const MIMO = FREEBUFF_MIMO_V25_MODEL_ID
444+
const none = (accessTier: 'full' | 'limited', balance = 50) =>
445+
({
446+
status: 'none',
447+
accessTier,
448+
freebucks: { balance, planId: null, prices: { [FLASH]: 15, [MIMO]: 10 } },
449+
}) as never
450+
451+
test('is the Balanced stop: DeepSeek V4.1 Flash at full access, MiMo at limited', () => {
452+
expect(
453+
resolveFreebuffModelSelectionForSession(MIMO, none('full'), false),
454+
).toBe(FLASH)
455+
expect(
456+
resolveFreebuffModelSelectionForSession(FLASH, none('limited'), false),
457+
).toBe(MIMO)
458+
})
459+
460+
test('an explicit pick keeps its model', () => {
461+
expect(
462+
resolveFreebuffModelSelectionForSession(MIMO, none('full'), true),
463+
).toBe(MIMO)
464+
})
465+
466+
test('steps down to the recommendation when the balance cannot start Balanced', () => {
467+
expect(freebuffDefaultModelForSession(none('full', 12))).toBe(MIMO)
468+
})
469+
470+
test('names the catalog row in catalog mode', () => {
471+
setFreebuffCatalog(CATALOG)
472+
expect(freebuffDefaultModelForSession(none('full'))).toBe('m-flash')
473+
})
474+
475+
test('a pick stops the selection following the default', () => {
476+
useFreebuffModelStore.setState({ hasExplicitPick: false })
477+
persistFreebuffModelPick(MIMO)
478+
expect(useFreebuffModelStore.getState().hasExplicitPick).toBe(true)
479+
})
480+
})
481+
439482
describe('reasoning effort per catalog row', () => {
440483
test('uses the row ladder and default', () => {
441484
applyFreebuffCatalog(CATALOG)

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3833,9 +3833,9 @@ export type SupportedFreebuffModelId =
38333833
(typeof SUPPORTED_FREEBUFF_MODELS)[number]['id']
38343834
export type FreebuffWebModelId = (typeof FREEBUFF_WEB_ALL_MODELS)[number]['id']
38353835

3836-
/** What new freebuff users see selected in the CLI picker, and the model its
3837-
* "RECOMMENDED" hero opens on; Desktop starts on freebuffAppDefaultModelIds
3838-
* and falls back to this. DeepSeek V4.1 Flash as of
3836+
/** A new CLI user's selection until a session response moves it to Balanced,
3837+
* and the model the picker's "RECOMMENDED" hero opens on; Desktop starts on
3838+
* freebuffAppDefaultModelIds and falls back to this. DeepSeek V4.1 Flash as of
38393839
* 2026-09-02 (unmetered on the Luminal lane; it carries the AI-training
38403840
* notice, and it is also the limited tier's default). The paragraphs below
38413841
* were written for the GLM 5.3 Flash default of 2026-08-30 and still hold.

0 commit comments

Comments
 (0)