feat(freebuff): update model catalog + fable offer claims

This commit is contained in:
MUH. IQRAM BAHRING
2026-09-05 23:18:42 +08:00
parent cdbdcd3448
commit 9cd61d585c
4 changed files with 225 additions and 14 deletions
+95 -3
View File
@@ -16,6 +16,8 @@ const {
resetSessionCache,
rootAgentIdForModel,
injectFreebuffMarker,
fetchSessionOffers,
guardOfferClaim,
FREEBUFF_SYSTEM_MARKER,
} = __test__;
@@ -273,7 +275,7 @@ describe("freebuff session pre-flight", () => {
fetchMock.mockResolvedValue(
jsonResponse({ status: "active", instanceId: "inst-2", expiresAt: new Date(Date.now() + 3600000).toISOString() }),
);
await ensureSession("tok-1", "minimax/minimax-m3", null);
await ensureSession("tok-1", "z-ai/glm-5.3-flash", null);
expect(fetchMock.mock.calls.length).toBe(2);
});
@@ -305,6 +307,89 @@ describe("freebuff session pre-flight", () => {
});
});
describe("freebuff limited-offer (Claude Fable 5) claims", () => {
const FABLE = "anthropic/claude-fable-5";
const offerRow = (over = {}) => ({
model: FABLE,
remaining: 3,
total: 10,
userRemaining: 1,
userResetAt: new Date(Date.now() + 3600000).toISOString(),
...over,
});
it("GETs limitedModelOffers (never claims) and caches them per token", async () => {
fetchMock.mockResolvedValue(jsonResponse({ status: "none", limitedModelOffers: [offerRow()] }));
const offers = await fetchSessionOffers("tok-1", null);
expect(offers.map((o) => o.model)).toEqual([FABLE]);
const [url, opts] = fetchMock.mock.calls[0];
expect(url).toBe("https://www.codebuff.com/api/v1/freebuff/session");
expect(opts.method).toBe("GET");
expect(opts.headers.Authorization).toBe("Bearer tok-1");
expect(opts.headers.Accept).toBe("application/json");
// Second read within the cache TTL does not refetch.
await fetchSessionOffers("tok-1", null);
expect(fetchMock.mock.calls.length).toBe(1);
});
it("allows the claim while the offer is open", async () => {
fetchMock.mockResolvedValue(jsonResponse({ status: "none", limitedModelOffers: [offerRow()] }));
expect(await guardOfferClaim("tok-1", FABLE, null)).toMatchObject({ model: FABLE, remaining: 3 });
expect(fetchMock.mock.calls.length).toBe(1);
expect(fetchMock.mock.calls[0][1].method).toBe("GET");
});
it("lets non-offer models claim without any offer GET", async () => {
expect(await guardOfferClaim("tok-1", "deepseek/deepseek-v4-flash", null)).toBeNull();
expect(fetchMock.mock.calls.length).toBe(0);
});
it("refuses the claim when the wave pool is closed (no offer row)", async () => {
fetchMock.mockResolvedValue(jsonResponse({ status: "none", limitedModelOffers: [] }));
await expect(guardOfferClaim("tok-1", FABLE, null)).rejects.toThrow(/not being offered right now/i);
});
it("refuses the claim when the account's daily Fable sessions are used up", async () => {
fetchMock.mockResolvedValue(
jsonResponse({ status: "none", limitedModelOffers: [offerRow({ userRemaining: 0 })] }),
);
await expect(guardOfferClaim("tok-1", FABLE, null)).rejects.toThrow(/has used its Claude Fable 5 sessions/i);
});
it("claims a Fable session only after the offer passes: GET offers, then POST claim", async () => {
fetchMock.mockImplementation(async (url, opts = {}) => {
if (url.includes("/freebuff/session") && opts.method === "GET") {
return jsonResponse({ status: "none", limitedModelOffers: [offerRow()] });
}
if (url.includes("/freebuff/session")) {
return jsonResponse({ status: "active", instanceId: "inst-fable", expiresAt: new Date(Date.now() + 3600000).toISOString() });
}
return jsonResponse({ ok: false }, { status: 500, ok: false });
});
const res = await ensureSession("tok-1", FABLE, null);
expect(res).toEqual({ instanceId: "inst-fable", status: "active" });
const methods = fetchMock.mock.calls.map(([, o]) => o.method);
expect(methods).toEqual(["GET", "POST"]);
const [, postOpts] = fetchMock.mock.calls[1];
expect(postOpts.headers["x-freebuff-model"]).toBe(FABLE);
// Cached claim → no further requests.
await ensureSession("tok-1", FABLE, null);
expect(fetchMock.mock.calls.length).toBe(2);
});
it("never POSTs a claim when the Fable wave is closed", async () => {
fetchMock.mockResolvedValue(jsonResponse({ status: "none", limitedModelOffers: [] }));
await expect(ensureSession("tok-1", FABLE, null)).rejects.toThrow(/not being offered right now/i);
const methods = fetchMock.mock.calls.map(([, o]) => o.method);
expect(methods).toEqual(["GET"]);
});
});
describe("freebuff free-tier system marker", () => {
it("prepends the canonical marker when the first message is a system prompt", () => {
const out = injectFreebuffMarker({
@@ -339,10 +424,17 @@ describe("freebuff free-tier system marker", () => {
describe("freebuff run registration", () => {
it("maps freebuff models to their root free agent ids", () => {
expect(rootAgentIdForModel("deepseek/deepseek-v4-flash")).toBe("base3-free-deepseek-flash");
expect(rootAgentIdForModel("deepseek/deepseek-v4-pro")).toBe("base3-free-deepseek");
expect(rootAgentIdForModel("z-ai/glm-5.3-flash")).toBe("base3-free-glm-5-3-flash");
expect(rootAgentIdForModel("z-ai/glm-5.2")).toBe("base3-free-glm");
expect(rootAgentIdForModel("mimo/mimo-v2.5")).toBe("base3-free-mimo");
expect(rootAgentIdForModel("minimax/minimax-m3")).toBe("base3-free-minimax-m3");
expect(rootAgentIdForModel("openai/gpt-5.6-luna")).toBe("base3-free-luna");
expect(rootAgentIdForModel("upstage/solar-pro4")).toBe("base3-free-solar-pro4");
expect(rootAgentIdForModel("meta/muse-spark-1.3-contributor")).toBe("base3-free-muse-spark-1-3");
expect(rootAgentIdForModel("anthropic/claude-fable-5")).toBe("base3-free-fable");
// Withdrawn upstream models are unmapped — they fall back, and the backend
// refuses their sessions anyway.
expect(rootAgentIdForModel("deepseek/deepseek-v4-pro")).toBe("base2-free");
expect(rootAgentIdForModel("minimax/minimax-m3")).toBe("base2-free");
expect(rootAgentIdForModel("some/unknown-model")).toBe("base2-free");
});
+3 -3
View File
@@ -94,7 +94,7 @@ describe("getUsageForProvider(freebuff)", () => {
status: "active",
accessTier: "full",
instanceId: "inst-1",
model: "deepseek/deepseek-v4-pro",
model: "meta/muse-spark-1.3-contributor",
expiresAt: new Date(Date.now() + 3600000).toISOString(),
rateLimit: {
limit: 6,
@@ -112,10 +112,10 @@ describe("getUsageForProvider(freebuff)", () => {
});
expect(usage.plan).toBe("Freebuff");
expect(usage.quotas["deepseek/deepseek-v4-pro"]).toMatchObject({
expect(usage.quotas["meta/muse-spark-1.3-contributor"]).toMatchObject({
used: 2.4,
total: 6,
displayName: "DeepSeek V4 Pro",
displayName: "Muse Spark 1.3",
});
});