Skip to content

Commit 60b8652

Browse files
committed
Add fallback model
1 parent d9de78a commit 60b8652

4 files changed

Lines changed: 56 additions & 21 deletions

File tree

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
44

55
import { Button } from './button'
66
import {
7-
DEFAULT_FREEBUFF_MODEL_ID,
7+
FALLBACK_FREEBUFF_MODEL_ID,
88
FREEBUFF_DEPLOYMENT_HOURS_LABEL,
99
FREEBUFF_GLM_MODEL_ID,
1010
FREEBUFF_MODELS,
@@ -60,11 +60,16 @@ export const FreebuffModelSelector: React.FC = () => {
6060
}, [selectedModel])
6161

6262
useEffect(() => {
63+
// Landing-screen safety net: if the in-memory selection becomes
64+
// unavailable (e.g. deployment hours close while the picker is open),
65+
// swap to the always-available fallback so Enter doesn't POST a model
66+
// the server will immediately reject. In-memory only — the user's saved
67+
// preference (e.g. GLM) is preserved for the next launch.
6368
if (
6469
(session?.status === 'none' || !session) &&
6570
!isFreebuffModelAvailable(selectedModel, new Date(now))
6671
) {
67-
setSelectedModel(DEFAULT_FREEBUFF_MODEL_ID)
72+
setSelectedModel(FALLBACK_FREEBUFF_MODEL_ID)
6873
}
6974
}, [now, selectedModel, session, setSelectedModel])
7075

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { env } from '@codebuff/common/env'
2-
import { DEFAULT_FREEBUFF_MODEL_ID } from '@codebuff/common/constants/freebuff-models'
2+
import {
3+
FALLBACK_FREEBUFF_MODEL_ID,
4+
resolveFreebuffModel,
5+
} from '@codebuff/common/constants/freebuff-models'
36
import { useEffect } from 'react'
47

58
import {
@@ -10,6 +13,7 @@ import { useFreebuffSessionStore } from '../state/freebuff-session-store'
1013
import { getAuthTokenDetails } from '../utils/auth'
1114
import { IS_FREEBUFF } from '../utils/constants'
1215
import { logger } from '../utils/logger'
16+
import { saveFreebuffModelPreference } from '../utils/settings'
1317

1418
import type { FreebuffSessionResponse } from '../types/freebuff-session'
1519

@@ -280,7 +284,13 @@ export function returnToFreebuffLanding(
280284
*/
281285
export function joinFreebuffQueue(model: string): Promise<void> {
282286
if (!IS_FREEBUFF) return Promise.resolve()
283-
useFreebuffModelStore.getState().setSelectedModel(model)
287+
// This is the only explicit user-pick path (called from the picker on
288+
// click / Enter), so persistence belongs here — and ONLY here. Server-
289+
// driven flips (`model_locked`, `model_unavailable`, takeover) go
290+
// through `setSelectedModel` directly, which never writes to disk.
291+
const resolved = resolveFreebuffModel(model)
292+
useFreebuffModelStore.getState().setSelectedModel(resolved)
293+
saveFreebuffModelPreference(resolved)
284294
return restartFreebuffSession('rejoin')
285295
}
286296

@@ -419,7 +429,14 @@ export function useFreebuffSession(): UseFreebuffSessionResult {
419429
return
420430
}
421431
if (next.status === 'model_unavailable') {
422-
useFreebuffModelStore.getState().setSelectedModel(DEFAULT_FREEBUFF_MODEL_ID)
432+
// Server says the requested model isn't available right now (e.g.
433+
// GLM outside deployment hours). Flip to the always-available
434+
// fallback for this run. In-memory only — `setSelectedModel`
435+
// doesn't persist, so the user's saved preference (e.g. GLM)
436+
// is preserved for their next launch during deployment hours.
437+
useFreebuffModelStore
438+
.getState()
439+
.setSelectedModel(FALLBACK_FREEBUFF_MODEL_ID)
423440
nextMethod = 'GET'
424441
schedule(0)
425442
return

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

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
import {
22
DEFAULT_FREEBUFF_MODEL_ID,
33
resolveAvailableFreebuffModel,
4+
resolveFreebuffModel,
45
} from '@codebuff/common/constants/freebuff-models'
56
import { create } from 'zustand'
67

7-
import {
8-
loadFreebuffModelPreference,
9-
saveFreebuffModelPreference,
10-
} from '../utils/settings'
8+
import { loadFreebuffModelPreference } from '../utils/settings'
119

1210
/**
1311
* Holds the user's currently-selected freebuff model. Initialized from the
1412
* persisted settings file so freebuff defaults to whatever model the user
15-
* last picked. Writing through `setSelectedModel` also persists to disk so
16-
* the next launch picks it up without an explicit save call.
13+
* last picked.
14+
*
15+
* `setSelectedModel` is in-memory only — it does NOT persist. Persistence
16+
* happens exclusively in `joinFreebuffQueue` (the explicit-pick path), so
17+
* server-driven auto-flips (`model_locked`, `model_unavailable`, takeover)
18+
* can update the in-memory selection without overwriting the user's saved
19+
* preference. The latter previously caused users to get permanently flipped
20+
* to the fallback model after a single auto-fallback.
1721
*
1822
* Components in the waiting room read this to highlight the current row in
1923
* the model picker; the session hook reads it to decide which queue to join.
@@ -27,11 +31,8 @@ export const useFreebuffModelStore = create<FreebuffModelStore>((set) => ({
2731
selectedModel: resolveAvailableFreebuffModel(
2832
loadFreebuffModelPreference() ?? DEFAULT_FREEBUFF_MODEL_ID,
2933
),
30-
setSelectedModel: (model) => {
31-
const resolved = resolveAvailableFreebuffModel(model)
32-
saveFreebuffModelPreference(resolved)
33-
set({ selectedModel: resolved })
34-
},
34+
setSelectedModel: (model) =>
35+
set({ selectedModel: resolveFreebuffModel(model) }),
3536
}))
3637

3738
/** Imperative read for non-React callers (the session hook's tick loop and

common/src/constants/freebuff-models.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ export interface FreebuffModelOption {
1919

2020
export const FREEBUFF_DEPLOYMENT_HOURS_LABEL = '9am ET-5pm PT'
2121
export const FREEBUFF_GLM_MODEL_ID = 'z-ai/glm-5.1'
22+
export const FREEBUFF_MINIMAX_MODEL_ID = 'minimax/minimax-m2.7'
2223

2324
export const FREEBUFF_MODELS = [
2425
{
25-
id: 'minimax/minimax-m2.7',
26+
id: FREEBUFF_MINIMAX_MODEL_ID,
2627
displayName: 'MiniMax M2.7',
2728
tagline: 'Fastest',
2829
availability: 'always',
@@ -37,7 +38,18 @@ export const FREEBUFF_MODELS = [
3738

3839
export type FreebuffModelId = (typeof FREEBUFF_MODELS)[number]['id']
3940

40-
export const DEFAULT_FREEBUFF_MODEL_ID: FreebuffModelId = FREEBUFF_MODELS[0].id
41+
/** What new freebuff users see selected in the picker. May not be currently
42+
* available (GLM is closed outside deployment hours); callers that need an
43+
* always-available id for resolution / auto-fallbacks should use
44+
* FALLBACK_FREEBUFF_MODEL_ID instead. */
45+
export const DEFAULT_FREEBUFF_MODEL_ID: FreebuffModelId = FREEBUFF_GLM_MODEL_ID
46+
47+
/** Always-available fallback used when the requested model can't be served
48+
* right now (unknown id, deployment hours closed, etc.). Kept distinct from
49+
* DEFAULT_FREEBUFF_MODEL_ID so a new user's "preferred default" can be the
50+
* smartest model without auto-flipping anyone to a closed deployment. */
51+
export const FALLBACK_FREEBUFF_MODEL_ID: FreebuffModelId =
52+
FREEBUFF_MINIMAX_MODEL_ID
4153

4254
export function isFreebuffModelId(
4355
id: string | null | undefined,
@@ -49,13 +61,13 @@ export function isFreebuffModelId(
4961
export function resolveFreebuffModel(
5062
id: string | null | undefined,
5163
): FreebuffModelId {
52-
return isFreebuffModelId(id) ? id : DEFAULT_FREEBUFF_MODEL_ID
64+
return isFreebuffModelId(id) ? id : FALLBACK_FREEBUFF_MODEL_ID
5365
}
5466

5567
export function getFreebuffModel(id: string): FreebuffModelOption {
5668
return (
5769
FREEBUFF_MODELS.find((m) => m.id === id) ??
58-
FREEBUFF_MODELS.find((m) => m.id === DEFAULT_FREEBUFF_MODEL_ID)!
70+
FREEBUFF_MODELS.find((m) => m.id === FALLBACK_FREEBUFF_MODEL_ID)!
5971
)
6072
}
6173

@@ -102,5 +114,5 @@ export function resolveAvailableFreebuffModel(
102114
const resolved = resolveFreebuffModel(id)
103115
return isFreebuffModelAvailable(resolved, now)
104116
? resolved
105-
: DEFAULT_FREEBUFF_MODEL_ID
117+
: FALLBACK_FREEBUFF_MODEL_ID
106118
}

0 commit comments

Comments
 (0)