feat(providers): add Cline free-tier models and sync Freebuff catalog
- Cline: 6 free models, cline-cli product headers, API-key auth,
{data} envelope unwrap, workos: prefix handling
- Freebuff: muse-spark 1.3 → 1.2 (upstream withdrawal 2026-09-07),
DeepSeek V4.1 Flash rename
This commit is contained in:
@@ -137,6 +137,7 @@
|
||||
"combined": true,
|
||||
"header": "Authorization",
|
||||
"scheme": "bearer",
|
||||
"preserveHookAuth": true,
|
||||
"hooks": [
|
||||
"clineHeaders"
|
||||
]
|
||||
@@ -153,6 +154,7 @@
|
||||
"combined": true,
|
||||
"header": "Authorization",
|
||||
"scheme": "bearer",
|
||||
"preserveHookAuth": true,
|
||||
"hooks": [
|
||||
"clineHeaders"
|
||||
]
|
||||
@@ -276,6 +278,26 @@
|
||||
"validateUrl": "https://api.fireworks.ai/inference/v1/models",
|
||||
"format": "openai"
|
||||
},
|
||||
"freebuff": {
|
||||
"baseUrl": "https://www.codebuff.com/api/v1/chat/completions",
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"User-Agent": "ai-sdk/openai-compatible/1.0/codebuff"
|
||||
},
|
||||
"retry": {
|
||||
"429": {
|
||||
"attempts": 2,
|
||||
"delayMs": 2000
|
||||
},
|
||||
"503": {
|
||||
"attempts": 2,
|
||||
"delayMs": 1500
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"url": "https://www.codebuff.com/api/v1/freebuff/session"
|
||||
}
|
||||
},
|
||||
"gemini-cli": {
|
||||
"baseUrl": "https://cloudcode-pa.googleapis.com/v1internal",
|
||||
"format": "gemini-cli",
|
||||
@@ -437,6 +459,9 @@
|
||||
"groq": {
|
||||
"baseUrl": "https://api.groq.com/openai/v1/chat/completions",
|
||||
"validateUrl": "https://api.groq.com/openai/v1/models",
|
||||
"usage": {
|
||||
"url": "https://api.groq.com/openai/v1/models"
|
||||
},
|
||||
"format": "openai"
|
||||
},
|
||||
"hyperbolic": {
|
||||
@@ -924,6 +949,32 @@
|
||||
"format": "openai",
|
||||
"tokenUrl": "https://www.codebuddy.ai/v2/plugin/auth/token"
|
||||
},
|
||||
"trae": {
|
||||
"baseUrl": "https://core-normal.trae.ai/api/remote/v1",
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"X-Trae-Client-Type": "web",
|
||||
"X-Preferenced-Language": "en",
|
||||
"Referer": "https://solo.trae.ai/"
|
||||
},
|
||||
"auth": {
|
||||
"combined": true,
|
||||
"header": "Authorization",
|
||||
"scheme": "Cloud-IDE-JWT"
|
||||
},
|
||||
"usage": {
|
||||
"url": "https://api.marscode.com/cloudide/api/v3/trae/GetUserInfo"
|
||||
},
|
||||
"regions": {
|
||||
"cn": "https://api.marscode.com",
|
||||
"sg": "https://api.trae.ai",
|
||||
"us": "https://www.trae.ai"
|
||||
},
|
||||
"defaultRegion": "cn",
|
||||
"clientId": "ono9krqynydwx5",
|
||||
"clientSecret": "-",
|
||||
"tokenUrl": "https://api.marscode.com/cloudide/api/v3/trae/oauth/ExchangeToken"
|
||||
},
|
||||
"zed": {
|
||||
"baseUrl": "https://cloud.zed.dev/completions",
|
||||
"format": "openai",
|
||||
@@ -990,6 +1041,25 @@
|
||||
"validateUrl": "https://api.morphllm.com/v1/models",
|
||||
"format": "openai"
|
||||
},
|
||||
"devin-cli": {
|
||||
"baseUrl": "devin://acp/stdio",
|
||||
"format": "openai"
|
||||
},
|
||||
"windsurf": {
|
||||
"baseUrl": "https://server.codeium.com/exa.language_server_pb.LanguageServerService/GetChatMessage",
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"Content-Type": "application/grpc-web+proto",
|
||||
"Accept": "application/grpc-web+proto",
|
||||
"X-Grpc-Web": "1"
|
||||
},
|
||||
"auth": {
|
||||
"combined": true,
|
||||
"header": "Authorization",
|
||||
"scheme": "Bearer"
|
||||
},
|
||||
"clientId": "3GUryQ7ldAeKEuD2obYnppsnmj58eP5u"
|
||||
},
|
||||
"poolside": {
|
||||
"baseUrl": "https://inference.poolside.ai/v1/chat/completions",
|
||||
"validateUrl": "https://inference.poolside.ai/v1/models",
|
||||
@@ -1009,4 +1079,4 @@
|
||||
},
|
||||
"format": "openai"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/lib/usageDb.js", () => ({
|
||||
appendRequestLog: vi.fn(async () => {}),
|
||||
saveRequestDetail: vi.fn(async () => {}),
|
||||
saveRequestUsage: vi.fn(async () => {})
|
||||
}));
|
||||
|
||||
const { unwrapDataEnvelope } = await import("../../open-sse/handlers/chatCore/nonStreamingHandler.js");
|
||||
const { buildClineHeaders } = await import("../../open-sse/shared/clineAuth.js");
|
||||
|
||||
// Cline gateway wraps non-streaming Chat Completions in {data, success}.
|
||||
// handleNonStreamingResponse calls unwrapDataEnvelope unconditionally (it runs
|
||||
// before the needsTranslation gate, which openai→openai never passes).
|
||||
const ENVELOPED = {
|
||||
data: {
|
||||
id: "gen-123",
|
||||
object: "chat.completion",
|
||||
created: 1789056634,
|
||||
model: "deepseek/deepseek-v4-flash-0731",
|
||||
choices: [{ index: 0, message: { role: "assistant", content: "Hi there!" }, finish_reason: "stop" }],
|
||||
usage: { prompt_tokens: 9, completion_tokens: 29, total_tokens: 38 },
|
||||
},
|
||||
success: true,
|
||||
};
|
||||
|
||||
describe("cline {data} envelope unwrap", () => {
|
||||
it("unwraps choices/usage to top level", () => {
|
||||
const out = unwrapDataEnvelope(structuredClone(ENVELOPED));
|
||||
expect(out.choices?.[0]?.message?.content).toBe("Hi there!");
|
||||
expect(out.usage?.prompt_tokens).toBe(9);
|
||||
});
|
||||
|
||||
it("leaves plain OpenAI bodies untouched", () => {
|
||||
const out = unwrapDataEnvelope(structuredClone(ENVELOPED.data));
|
||||
expect(out.choices?.[0]?.message?.content).toBe("Hi there!");
|
||||
});
|
||||
|
||||
it("prefers top-level choices when both exist", () => {
|
||||
const body = { ...structuredClone(ENVELOPED), choices: [{ index: 0, message: { role: "assistant", content: "top" }, finish_reason: "stop" }] };
|
||||
const out = unwrapDataEnvelope(body);
|
||||
expect(out.choices?.[0]?.message?.content).toBe("top");
|
||||
});
|
||||
|
||||
it("ignores non-envelope bodies", () => {
|
||||
expect(unwrapDataEnvelope(null)).toBe(null);
|
||||
expect(unwrapDataEnvelope({ error: "x" })).toEqual({ error: "x" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("cline auth header shape", () => {
|
||||
it("sends API keys as plain Bearer", () => {
|
||||
expect(buildClineHeaders("sk_abc", {}, { isApiKey: true }).Authorization).toBe("Bearer sk_abc");
|
||||
});
|
||||
|
||||
it("prefixes OAuth tokens with workos:", () => {
|
||||
expect(buildClineHeaders("tok123").Authorization).toBe("Bearer workos:tok123");
|
||||
expect(buildClineHeaders("workos:tok123").Authorization).toBe("Bearer workos:tok123");
|
||||
});
|
||||
|
||||
it("sends Cline product identity headers (free-model gate)", () => {
|
||||
const h = buildClineHeaders("tok123");
|
||||
expect(h["X-CLIENT-TYPE"]).toBe("cline-cli");
|
||||
expect(h["User-Agent"]).toMatch(/^Cline\//);
|
||||
});
|
||||
});
|
||||
@@ -327,6 +327,53 @@ describe("freebuff session pre-flight", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("handles spend_limited arriving as HTTP 429 (the actual wire shape) — still skips until reset", async () => {
|
||||
const resetAt = "2099-01-01T00:00:00.000Z";
|
||||
fetchMock.mockResolvedValue(
|
||||
jsonResponse(
|
||||
{
|
||||
status: "spend_limited",
|
||||
accessTier: "full",
|
||||
upgrade: { url: "https://freebuff.com/plans", message: "Get 150 Freebucks a day from $8/mo." },
|
||||
message: "This account hit today's hard usage cap.",
|
||||
resetAt,
|
||||
},
|
||||
{ status: 429, ok: false },
|
||||
),
|
||||
);
|
||||
await expect(requestSession("tok-1", "deepseek/deepseek-v4-flash", null)).rejects.toMatchObject({
|
||||
status: 429,
|
||||
resetsAtMs: Date.parse(resetAt),
|
||||
});
|
||||
});
|
||||
|
||||
it("handles rate_limited arriving as HTTP 429 with only retryAfterMs", async () => {
|
||||
const retryAfterMs = 15 * 60 * 1000;
|
||||
fetchMock.mockResolvedValue(
|
||||
jsonResponse({ status: "rate_limited", retryAfterMs, message: "limit" }, { status: 429, ok: false }),
|
||||
);
|
||||
const before = Date.now();
|
||||
try {
|
||||
await requestSession("tok-1", "deepseek/deepseek-v4-flash", null);
|
||||
throw new Error("should have rejected");
|
||||
} catch (error) {
|
||||
expect(error.status).toBe(429);
|
||||
expect(error.resetsAtMs).toBeGreaterThanOrEqual(before + retryAfterMs - 1000);
|
||||
expect(error.resetsAtMs).toBeLessThanOrEqual(before + retryAfterMs + 1000);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps an unknown HTTP 429 as a generic failure (no gate status → no resetsAtMs)", async () => {
|
||||
fetchMock.mockResolvedValue(jsonResponse({ error: "nope" }, { status: 429, ok: false }));
|
||||
try {
|
||||
await requestSession("tok-1", "deepseek/deepseek-v4-flash", null);
|
||||
throw new Error("should have rejected");
|
||||
} catch (error) {
|
||||
expect(error.status).toBe(429);
|
||||
expect(error.resetsAtMs).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("rate_limited without any reset hint stays a plain error (transient cooldown path)", async () => {
|
||||
fetchMock.mockResolvedValue(jsonResponse({ status: "rate_limited", message: "busy" }));
|
||||
try {
|
||||
@@ -473,10 +520,11 @@ describe("freebuff run registration", () => {
|
||||
expect(rootAgentIdForModel("mimo/mimo-v2.5")).toBe("base3-free-mimo");
|
||||
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("meta/muse-spark-1.2-contributor")).toBe("base3-free-muse-spark");
|
||||
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("meta/muse-spark-1.3-contributor")).toBe("base2-free");
|
||||
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");
|
||||
|
||||
@@ -73,7 +73,7 @@ describe("getUsageForProvider(freebuff)", () => {
|
||||
resetAt: "2026-08-06T07:00:00.000Z",
|
||||
recurring: true,
|
||||
unlimited: false,
|
||||
displayName: "DeepSeek V4 Flash",
|
||||
displayName: "DeepSeek V4.1 Flash",
|
||||
});
|
||||
expect(usage.quotas["openai/gpt-5.6-luna"]).toMatchObject({
|
||||
used: 1,
|
||||
@@ -94,7 +94,7 @@ describe("getUsageForProvider(freebuff)", () => {
|
||||
status: "active",
|
||||
accessTier: "full",
|
||||
instanceId: "inst-1",
|
||||
model: "meta/muse-spark-1.3-contributor",
|
||||
model: "meta/muse-spark-1.2-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["meta/muse-spark-1.3-contributor"]).toMatchObject({
|
||||
expect(usage.quotas["meta/muse-spark-1.2-contributor"]).toMatchObject({
|
||||
used: 2.4,
|
||||
total: 6,
|
||||
displayName: "Muse Spark 1.3",
|
||||
displayName: "Muse Spark 1.2",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -150,7 +150,7 @@ describe("getUsageForProvider(freebuff)", () => {
|
||||
recurring: true,
|
||||
unlimited: false,
|
||||
price: 15,
|
||||
displayName: "DeepSeek V4 Flash",
|
||||
displayName: "DeepSeek V4.1 Flash",
|
||||
});
|
||||
expect(usage.quotas["z-ai/glm-5.3-flash"]).toMatchObject({
|
||||
used: 15,
|
||||
@@ -297,14 +297,14 @@ describe("parseQuotaData(freebuff)", () => {
|
||||
used: 4.1,
|
||||
total: 6,
|
||||
resetAt: "2026-08-06T07:00:00.000Z",
|
||||
displayName: "DeepSeek V4 Flash",
|
||||
displayName: "DeepSeek V4.1 Flash",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]).toMatchObject({
|
||||
name: "DeepSeek V4 Flash",
|
||||
name: "DeepSeek V4.1 Flash",
|
||||
modelKey: "deepseek/deepseek-v4-flash",
|
||||
used: 4.1,
|
||||
total: 6,
|
||||
|
||||
Reference in New Issue
Block a user