Skip to content

Commit e8a3c79

Browse files
Sync public snapshot from freebuff-private
Source: CodebuffAI/freebuff-private@27cb5242be46f06b4c8e079490afb309aebb3307
1 parent f1ebb97 commit e8a3c79

4 files changed

Lines changed: 89 additions & 46 deletions

File tree

bun.lock

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

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

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { describe, expect, test } from 'bun:test'
22

33
import {
4+
FREEBUFF_GPT_5_6_LUNA_MODEL_ID,
5+
FREEBUFF_LIMITED_TIER_PLAN_ONLY_MODEL_IDS,
46
FREEBUFF_WEB_LIMITED_MODEL_IDS,
57
LIMITED_FREEBUFF_MODEL_ID,
68
LIMITED_FREEBUFF_MODEL_IDS,
@@ -103,6 +105,34 @@ describe('paid plans at limited access', () => {
103105
}
104106
})
105107

108+
test('Luna is free at full access and plan-locked only at limited access', () => {
109+
expect(FREEBUFF_SUBSCRIPTION_PRO_MODEL_IDS).not.toContain(
110+
FREEBUFF_GPT_5_6_LUNA_MODEL_ID,
111+
)
112+
expect(FREEBUFF_LIMITED_TIER_PLAN_ONLY_MODEL_IDS).toContain(
113+
FREEBUFF_GPT_5_6_LUNA_MODEL_ID,
114+
)
115+
expect(
116+
isFreebuffSessionModelAllowedForAccessTier(
117+
FREEBUFF_GPT_5_6_LUNA_MODEL_ID,
118+
'full',
119+
),
120+
).toBe(true)
121+
expect(
122+
isFreebuffSessionModelAllowedForAccessTier(
123+
FREEBUFF_GPT_5_6_LUNA_MODEL_ID,
124+
'limited',
125+
),
126+
).toBe(false)
127+
expect(
128+
isFreebuffSessionModelAllowedForAccessTier(
129+
FREEBUFF_GPT_5_6_LUNA_MODEL_ID,
130+
'limited',
131+
true,
132+
),
133+
).toBe(true)
134+
})
135+
106136
test('every plan model is admissible at limited access with a plan', () => {
107137
// The two lists CANNOT drift any more: both the plan set and the predicate
108138
// read FREEBUFF_PLAN_METERED_CATALOG_MODEL_IDS, since 2026-09-04. This
@@ -121,14 +151,11 @@ describe('paid plans at limited access', () => {
121151
expect(FREEBUFF_SUBSCRIPTION_MODEL_IDS).toHaveLength(5)
122152
})
123153

124-
test('a Pro row is never free at limited access', () => {
125-
// The limited catalog excluded Luna BY NAME until 2026-09-04, which was
126-
// indistinguishable from "excludes the Pro rows" while Luna was the only
127-
// one. Gemini 3.8 Flash separated the two readings, and the by-name
128-
// version would have put the dearest row in the catalog into the FREE
129-
// limited catalog. Asserted over the whole Pro set so the next row added
130-
// to it cannot repeat that.
131-
for (const model of FREEBUFF_SUBSCRIPTION_PRO_MODEL_IDS) {
154+
test('a limited-tier plan-only row is never free at limited access', () => {
155+
// This set is intentionally broader than the global Pro set: Luna is free
156+
// at full access and still plan-locked here. Assert over the actual tier
157+
// boundary so changing either policy cannot silently widen this catalog.
158+
for (const model of FREEBUFF_LIMITED_TIER_PLAN_ONLY_MODEL_IDS) {
132159
expect(freeAtLimitedTier(model)).toBe(false)
133160
expect(isFreebuffSessionModelAllowedForAccessTier(model, 'limited')).toBe(
134161
false,
@@ -138,7 +165,7 @@ describe('paid plans at limited access', () => {
138165
isFreebuffSessionModelAllowedForAccessTier(model, 'limited', true),
139166
).toBe(true)
140167
}
141-
expect(FREEBUFF_SUBSCRIPTION_PRO_MODEL_IDS.length).toBeGreaterThan(0)
168+
expect(FREEBUFF_LIMITED_TIER_PLAN_ONLY_MODEL_IDS.length).toBeGreaterThan(0)
142169
})
143170

144171
test('every plan model resolves in the Web catalog', () => {

common/src/constants/freebuff-models.ts

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2893,7 +2893,8 @@ export const FREEBUFF_CLOUD_PLANNER_TURN_LIMIT = 12
28932893
/**
28942894
* Models available to limited-region Freebuff Web users.
28952895
*
2896-
* **Widened 2026-09-04 to the whole Web free catalog except GPT-5.6 Luna.**
2896+
* **Widened 2026-09-04 to the whole Web free catalog except the rows a plan
2897+
* unlocks at limited access.**
28972898
* It used to alias `LIMITED_FREEBUFF_MODEL_IDS` (Flash, MiMo, Solar), which
28982899
* left limited regions without the CHEAPEST row we serve — GLM 5.3 Flash at 5
28992900
* Freebucks an hour, a third of the Flash price — so the tier that can least
@@ -2906,10 +2907,12 @@ export const FREEBUFF_CLOUD_PLANNER_TURN_LIMIT = 12
29062907
* on a row that goes further. Before the meter, catalog WAS the control, which
29072908
* is why this list was narrow.
29082909
*
2909-
* Luna is the deliberate exception: it stays plan-gated at this tier
2910+
* Luna is the deliberate full-access exception: it stays plan-gated only at
2911+
* the limited tier
29102912
* (`isFreebuffSubscriptionModelIdForAccessTier` admits it only with a live
2911-
* paid plan), and the Web picker lists it locked rather than hiding it, so the
2912-
* row is an offer instead of an absence.
2913+
* paid plan there), while a full-access account may spend its ordinary premium
2914+
* allowance on it. The Web picker lists it locked at limited access rather
2915+
* than hiding it, so the row is an offer instead of an absence.
29132916
*
29142917
* Its own name, and NOT `LIMITED_FREEBUFF_MODEL_IDS`, because this is a
29152918
* broader browser catalog: CLI/Desktop also offer GLM 5.3 Flash, while
@@ -2931,17 +2934,13 @@ export const FREEBUFF_CLOUD_PLANNER_TURN_LIMIT = 12
29312934
* cannot import that one — freebuff-subscriptions.ts already imports this
29322935
* module, so the dependency only runs one way.
29332936
*
2934-
* That it is needed here at all is the point. The limited tier's catalog used
2935-
* to exclude Luna by NAME, which was correct only while Luna was the sole row
2936-
* a plan stood in front of; the moment a second one existed, the free limited
2937-
* catalog silently handed it out. One definition, consumed by both, is what
2938-
* stops the next Pro row repeating it.
2937+
* The limited-tier plan-only set below extends this one. Defining the global
2938+
* paid boundary once keeps a future Pro row out of the free limited catalog
2939+
* without making Luna globally Pro-only just because it has the narrower
2940+
* limited-tier restriction.
29392941
*/
29402942
export const FREEBUFF_PRO_ONLY_CATALOG_MODEL_IDS: readonly string[] =
2941-
Object.freeze([
2942-
FREEBUFF_GPT_5_6_LUNA_MODEL_ID,
2943-
FREEBUFF_GEMINI_38_FLASH_MODEL_ID,
2944-
])
2943+
Object.freeze([FREEBUFF_GEMINI_38_FLASH_MODEL_ID])
29452944

29462945
/** Whether the catalog marks `id` openable only on a paid session. Exact match:
29472946
* the suffix-tolerant public predicate is
@@ -2950,20 +2949,36 @@ export function isFreebuffProOnlyCatalogModelId(id: string): boolean {
29502949
return FREEBUFF_PRO_ONLY_CATALOG_MODEL_IDS.includes(id)
29512950
}
29522951

2952+
/**
2953+
* Rows an unpaid LIMITED-tier account cannot open.
2954+
*
2955+
* Every globally Pro-only row belongs here, plus Luna: Luna is part of the
2956+
* ordinary full-access premium pool, but a paid plan is still what unlocks it
2957+
* at limited access. Keeping this distinction explicit prevents the global Pro
2958+
* gate from accidentally paywalling full-access users just to preserve the
2959+
* limited-tier catalog boundary.
2960+
*/
2961+
export const FREEBUFF_LIMITED_TIER_PLAN_ONLY_MODEL_IDS: readonly string[] =
2962+
Object.freeze([
2963+
FREEBUFF_GPT_5_6_LUNA_MODEL_ID,
2964+
...FREEBUFF_PRO_ONLY_CATALOG_MODEL_IDS,
2965+
])
2966+
2967+
export function isFreebuffLimitedTierPlanOnlyModelId(id: string): boolean {
2968+
return FREEBUFF_LIMITED_TIER_PLAN_ONLY_MODEL_IDS.includes(id)
2969+
}
2970+
29532971
export const FREEBUFF_WEB_GEO_EXEMPT_MODEL_IDS: readonly string[] = [
29542972
...new Set<string>([
29552973
...LIMITED_FREEBUFF_MODEL_IDS,
29562974
...FREEBUFF_WEB_MODELS.filter(
29572975
(model) =>
29582976
isFreebuffWebSelectableModelId(model.id) &&
29592977
!isFreebuffWebGodOnlyModelId(model.id) &&
2960-
// Every Pro row, not just Luna. This read `!== LUNA` while Luna was
2961-
// the only model a plan stood in front of, so "the row a plan gates"
2962-
// and "Luna" were the same set and the narrower reading was the one
2963-
// written down. Gemini 3.8 Flash joining the Pro rows on 2026-09-04
2964-
// separated them: the old test would have put the dearest row in the
2965-
// catalog into the FREE limited catalog.
2966-
!isFreebuffProOnlyCatalogModelId(model.id),
2978+
// This is deliberately broader than the global Pro set. Luna is free
2979+
// from the premium pool at full access but still plan-locked here;
2980+
// Gemini 3.8 Flash is plan-only at every tier.
2981+
!isFreebuffLimitedTierPlanOnlyModelId(model.id),
29672982
).map((model) => model.id),
29682983
]),
29692984
]

common/src/constants/freebuff-subscriptions.ts

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -340,22 +340,19 @@ export const FREEBUFF_SUBSCRIPTION_FIVE_DAY_WINDOW_DAYS = 7
340340
* reads as the way in, and sends the user to spend a balance that will not
341341
* open it.
342342
*
343-
* ## Why these two, as of 2026-09-04
343+
* ## Why Gemini 3.8 Flash
344344
*
345-
* GPT-5.6 Luna and Gemini 3.8 Flash are the two dearest rows we serve, and
346-
* both were already half-fenced. Luna has been plan-locked for the limited
347-
* tier since 2026-09-04 (`LIMITED_TIER_PLAN_LOCKED_MODELS`) and is in the
348-
* sub-capped premium half above; Gemini 3.8 Flash was withdrawn outright the
349-
* day it shipped. Putting both behind the plan is the same decision applied to
350-
* the whole audience rather than to one tier.
345+
* Gemini 3.8 Flash was withdrawn outright the day it shipped and restored as
346+
* a Web-only paid row. GPT-5.6 Luna is deliberately NOT in this set: it stays
347+
* plan-locked at limited access, but full-access users may spend their shared
348+
* premium allowance on it just as CLI and Desktop users can.
351349
*
352350
* ## Scope
353351
*
354-
* Enforced on FREEBUFF WEB ONLY — see FREEBUFF_PRO_ENFORCED_SURFACES. Desktop
355-
* and the CLI keep serving Luna free, because a released binary holding the id
356-
* must not have the model taken away by a server deploy it did not ask for.
357-
* Widening is one edit to that constant, and should be made with a client
358-
* release rather than ahead of one.
352+
* Enforced on FREEBUFF WEB ONLY — see FREEBUFF_PRO_ENFORCED_SURFACES. Gemini is
353+
* absent from the CLI/Desktop catalog, so the Web-only listing and Web-only
354+
* gate stay paired. Widening is one edit to that constant, and should be made
355+
* with a client release rather than ahead of one.
359356
*
360357
* Withdrawn history: V4 Pro was the one entry for a few hours (#2254) and left
361358
* on 2026-08-26 with its withdrawal from free mode — a row nothing may admit
@@ -368,11 +365,13 @@ export const FREEBUFF_SUBSCRIPTION_PRO_MODEL_IDS: readonly string[] =
368365
/**
369366
* The Pro rows are enforced on **Freebuff Web only**, for now.
370367
*
371-
* Desktop and the CLI keep serving V4 Pro exactly as they do today — free, at
372-
* every hour, on the Cheaper Inference lane with its existing fallbacks —
373-
* while Web moves to the paid, direct, off-peak arrangement. Legacy clients
374-
* are mid-transition and must not have a model taken away by a server deploy
375-
* they did not ask for.
368+
* The current Pro row, Gemini 3.8 Flash, is Web-only too. A future native
369+
* rollout must add the surface here before adding the row to the shared
370+
* CLI/Desktop catalog, and must ship clients that understand the refusal
371+
* before the server can take a model away from them.
372+
*
373+
* Luna is deliberately outside this policy: it is available from the shared
374+
* premium pool on every full-access surface.
376375
*
377376
* Web is identified server-side by the Freebuff Web SERVICE ACCOUNT key, never
378377
* by a client-supplied header, so a CLI cannot claim to be Web to dodge the

0 commit comments

Comments
 (0)