feat(freebuff): update model catalog + fable offer claims
This commit is contained in:
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user