diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index f19fc0ea0b..72bc030875 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -1,4 +1,5 @@ { + "_rebaseline_2026_08_20_10531_freebuff_provider": "PR #10531 (adrianaryaputra, feat/freebuff-provider-support, closes #6793) own growth: src/shared/constants/providers/apikey/gateways.ts 1283->1298 (+15, the freebuff APIKEY_PROVIDERS_GATEWAYS catalog entry, additive data at the existing registry chokepoint, same god-file no-split rationale as prior gateways.ts rebaselines) and src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx 1062->1067 (+5, freebuff credential placeholder/hint at the existing per-provider switch chokepoint). Covered by tests/unit/freebuff-provider.test.ts (9/9 passing).", "_rebaseline_2026_08_20_10574_reasoning_transport_fallback": "PR #10574 (jackjinke, fix/responses-reasoning-transport, fixes #10550) own growth: src/sse/handlers/chatHelpers.ts 1017->1019 (+2 = the new reasoningTransportFallback option threaded through executeChatWithBreaker's options destructure and its downstream handleSingleModel call, at the existing per-attempt options-passthrough chokepoint; not extractable without splitting the option-forwarding call itself). Covered by the PR's own reasoning-policy test suite (tests/unit/chatcore-translation-paths.test.ts, tests/unit/combo-attempt-body-isolation-7847.test.ts, tests/unit/reasoning-cache.test.ts, tests/unit/strip-reasoning-blobs-agentic-context-1599.test.ts among others), 446/446 focused tests passing.", "_rebaseline_2026_08_18_10517_zed_hosted_oauth_callback_port": "PR #10517 (phatchau036, fix/zed-hosted-oauth-callback-port) own growth: src/shared/components/OAuthModal.tsx 1131->1148 (wc -l; check-file-size.mjs counts via split(\"\\n\").length so the gate sees 1134->1149, +15/+18, crosses the frozen 1134 cap). Wires the zed-hosted native-app callback auto-complete: forceManual gating on isTrueLocalhost for zed-hosted, the loopback-redirect-URI comment block, and the exchangeToken full-URL-as-code branch, all at the existing provider-switch chokepoints this modal already carries growth for (seventh bump: 969->989->993->998->1030->1056->1100->1149; structural shrink tracked in #3501). The actual port-derivation logic lives in src/lib/oauth/providers/zed-hosted.ts (not frozen here) and was hardened during pre-merge review to use the server's own getRuntimePorts() instead of a browser-guessed scheme/port, covered by the new tests/unit/zed-hosted-loopback-port-derivation.test.ts (8/8 passing).", "_rebaseline_2026_08_13_10243_codex_fingerprint_merge": "PR #10243 (xz-dev, Codex OAuth fingerprint convergence) merge into release/v3.8.50: src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts crossed the 1000-line new-file cap for the first time (974 on base, 997 on the PR's own branch, 1013 after merging + prettier reflow) purely from combining two independent, already-legitimate feature additions that landed on the same shared UI-helper file — this PR's own Codex fingerprint-mode select/toggle wiring (CODEX_FINGERPRINT_MODE_VALUES, getCodexFingerprintModeLabel, CodexFingerprintModeValue) plus #8949's unrelated Codex account-service-tier helpers merged concurrently on release/v3.8.50. Neither addition alone crosses the cap; git's line-level auto-merge does not detect a threshold crossing. Not modularized as part of this conflict-resolution merge commit (out of scope — this is a merge, not a feature change). Covered by the PR's own tests/unit/codex-fingerprint-convergence.test.ts, tests/unit/executor-codex.test.ts, tests/unit/provider-specific-data-schema.test.ts (all passing post-merge).", @@ -442,10 +443,10 @@ "src/shared/components/ModelSelectModal.tsx": 1138, "src/shared/constants/providers/apikey/gateways.ts": 1250 }, - "src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 1062, + "src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 1067, "src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts": 1051, "src/shared/components/ModelSelectModal.tsx": 1138, - "src/shared/constants/providers/apikey/gateways.ts": 1283, + "src/shared/constants/providers/apikey/gateways.ts": 1298, "open-sse/vendor/codex-chatgpt-web/bridge.ts": 1387, "_rebaseline_2026_08_11_v3850_merge_storm_provider_registry": "DRIFT do merge-storm 2026-08-11 (99 PRs mergeados no release/v3.8.50). AddApiKeyModal.tsx (PR #8949 ChatGPT Web provider) e useProviderConnections.ts/ModelSelectModal.tsx (PRs #9011 combo test-all, #9499 image combos) = UI nova legitima acima do cap; gateways.ts = god-file de catalogo de providers que cresceu com PRs #9009/#9421/#9468/#9594 (qualquer split arriscaria corromper o merge de novo — o proprio PR #9421 quebrou o arquivo); bridge.ts (PR #8949) = ponte Chromium vendored; proxyFetch.ts 1207->1220 = drift herdado de merges. Owner autorizou rebaseline com anotacao (2026-08-11).", "src/lib/modelCapabilities.ts": 1006, diff --git a/open-sse/config/providers/index.ts b/open-sse/config/providers/index.ts index a7067a022e..00861b2422 100644 --- a/open-sse/config/providers/index.ts +++ b/open-sse/config/providers/index.ts @@ -18,6 +18,7 @@ import { deepaiProvider } from "./registry/deepai/index.ts"; import { upstageProvider } from "./registry/upstage/index.ts"; import { nebiusProvider } from "./registry/nebius/index.ts"; import { fireworksProvider } from "./registry/fireworks/index.ts"; +import { freebuffProvider } from "./registry/freebuff/index.ts"; import { llamagateProvider } from "./registry/llamagate/index.ts"; import { glmProvider } from "./registry/glm/index.ts"; import { glmtProvider } from "./registry/glm/t/index.ts"; @@ -282,6 +283,7 @@ export const REGISTRY: Record = { deepai: deepaiProvider, nebius: nebiusProvider, fireworks: fireworksProvider, + freebuff: freebuffProvider, llamagate: llamagateProvider, glm: glmProvider, glmt: glmtProvider, diff --git a/open-sse/config/providers/registry/freebuff/index.ts b/open-sse/config/providers/registry/freebuff/index.ts new file mode 100644 index 0000000000..713f469548 --- /dev/null +++ b/open-sse/config/providers/registry/freebuff/index.ts @@ -0,0 +1,70 @@ +import type { RegistryEntry } from "../../shared.ts"; + +export const freebuffProvider: RegistryEntry = { + id: "freebuff", + alias: "fb", + format: "openai", + executor: "freebuff", + baseUrl: "https://www.codebuff.com/api/v1", + authType: "apikey", + authHeader: "bearer", + models: [ + { + id: "deepseek/deepseek-v4-flash", + name: "DeepSeek V4 Flash", + supportsReasoning: true, + contextLength: 131_072, + }, + { + id: "deepseek/deepseek-v4-pro", + name: "DeepSeek V4 Pro", + supportsReasoning: true, + contextLength: 131_072, + }, + { + id: "openai/gpt-5.6-luna", + name: "GPT-5.6 Luna", + supportsReasoning: true, + contextLength: 131_072, + }, + { + id: "minimax/minimax-m3", + name: "MiniMax M3", + supportsVision: true, + supportsReasoning: true, + contextLength: 131_072, + }, + { + id: "mimo/mimo-v2.5", + name: "MiMo v2.5", + supportsReasoning: true, + contextLength: 131_072, + }, + { + id: "z-ai/glm-5.2", + name: "GLM 5.2", + supportsReasoning: true, + contextLength: 131_072, + }, + { + id: "crof/kimi-k3-eco", + name: "Kimi K3 Eco", + supportsVision: true, + supportsReasoning: true, + contextLength: 131_072, + }, + { + id: "anthropic/claude-fable-5", + name: "Claude Fable 5", + supportsVision: true, + supportsReasoning: true, + contextLength: 131_072, + }, + { + id: "meta/muse-spark-1.2-contributor", + name: "Meta Muse Spark 1.2 Contributor", + supportsReasoning: true, + contextLength: 131_072, + }, + ], +}; diff --git a/open-sse/executors/freebuff.ts b/open-sse/executors/freebuff.ts new file mode 100644 index 0000000000..bc2de30f63 --- /dev/null +++ b/open-sse/executors/freebuff.ts @@ -0,0 +1,172 @@ +import { + BaseExecutor, + type ExecuteInput, +} from "./base.ts"; +import { PROVIDERS } from "../config/constants.ts"; + +const MODEL_TO_AGENT: Record = { + "deepseek/deepseek-v4-flash": "base2-free-deepseek-flash", + "deepseek/deepseek-v4-pro": "base2-free-deepseek", + "openai/gpt-5.6-luna": "base2-free-luna", + "minimax/minimax-m3": "base2-free-minimax-m3", + "mimo/mimo-v2.5": "base2-free-mimo", + "z-ai/glm-5.2": "base2-free-glm", + "crof/kimi-k3-eco": "base2-free-kimi-k3-eco", + "anthropic/claude-fable-5": "base2-free-fable", + "meta/muse-spark-1.2-contributor": "base2-free-muse-spark", +}; + +function generateClientSessionId(): string { + const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"; + let out = ""; + for (let i = 0; i < 13; i++) { + out += alphabet[Math.floor(Math.random() * alphabet.length)]; + } + return out; +} + +export class FreebuffExecutor extends BaseExecutor { + constructor() { + super("freebuff", (PROVIDERS as Record).freebuff as string || "freebuff"); + } + + override async execute(input: ExecuteInput) { + const { model, body, stream, credentials, signal } = input; + const token = credentials?.apiKey || credentials?.accessToken || ""; + + if (!token) { + return { + response: new Response( + JSON.stringify({ error: { message: "Freebuff Auth Token required", type: "authentication_error" } }), + { status: 401, headers: { "Content-Type": "application/json" } } + ), + }; + } + + const requestedModel = typeof model === "string" ? model.replace(/^freebuff\//, "") : (model || "deepseek/deepseek-v4-flash"); + const agentId = MODEL_TO_AGENT[requestedModel] || "base2-free"; + + const authHeaders = { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + "User-Agent": "codebuff/0.1.0 (darwin-arm64)", + }; + + let instanceId = ""; + let runId = ""; + + // 1. Session acquisition + try { + const sessionRes = await fetch("https://www.codebuff.com/api/v1/freebuff/session", { + method: "POST", + headers: { + ...authHeaders, + "x-freebuff-model": requestedModel, + }, + body: JSON.stringify({}), + signal, + }); + if (sessionRes.ok) { + const data = (await sessionRes.json()) as { instanceId?: string }; + instanceId = data.instanceId || ""; + } else { + const errText = await sessionRes.text(); + return { + response: new Response( + JSON.stringify({ error: { message: `Freebuff session failed (${sessionRes.status}): ${errText}`, type: "upstream_error" } }), + { status: sessionRes.status, headers: { "Content-Type": "application/json" } } + ), + }; + } + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + return { + response: new Response( + JSON.stringify({ error: { message: `Freebuff session network error: ${msg}`, type: "upstream_error" } }), + { status: 502, headers: { "Content-Type": "application/json" } } + ), + }; + } + + // 2. Start agent run + try { + const runRes = await fetch("https://www.codebuff.com/api/v1/agent-runs", { + method: "POST", + headers: authHeaders, + body: JSON.stringify({ action: "START", agentId }), + signal, + }); + if (runRes.ok) { + const runData = (await runRes.json()) as { runId?: string }; + runId = runData.runId || ""; + } + } catch {} + + // 3. Prepare Chat Payload & Buffy System Prompt + const incomingMessages = Array.isArray(body?.messages) ? [...body.messages] : []; + const hasBuffyPrompt = + incomingMessages.length > 0 && + incomingMessages[0].role === "system" && + typeof incomingMessages[0].content === "string" && + incomingMessages[0].content.trim().startsWith("You are Buffy"); + + if (!hasBuffyPrompt) { + incomingMessages.unshift({ + role: "system", + content: "You are Buffy, the strategic coding assistant.", + }); + } + + const clientSessionId = generateClientSessionId(); + const upstreamBody = { + ...(body || {}), + model: requestedModel, + messages: incomingMessages, + stream: stream !== false, + codebuff_metadata: { + run_id: runId, + cost_mode: "free", + client_id: clientSessionId, + freebuff_instance_id: instanceId, + ...((body as Record)?.codebuff_metadata as Record || {}), + }, + }; + + const completionHeaders = { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + "User-Agent": "ai-sdk/openai-compatible/1.0.25/codebuff", + Accept: "application/json, text/event-stream", + "x-freebuff-instance-id": instanceId, + ...(runId ? { "x-codebuff-run-id": runId } : {}), + "x-codebuff-agent-id": agentId, + }; + + // 4. Chat Completion + const completionUrl = "https://www.codebuff.com/api/v1/chat/completions"; + const response = await fetch(completionUrl, { + method: "POST", + headers: completionHeaders, + body: JSON.stringify(upstreamBody), + signal, + }); + + // 5. Finish agent run (background) + if (runId) { + void fetch("https://www.codebuff.com/api/v1/agent-runs", { + method: "POST", + headers: authHeaders, + body: JSON.stringify({ + action: "FINISH", + runId, + status: "completed", + totalSteps: 1, + directCredits: 0, + totalCredits: 0, + }), + }).catch(() => {}); + } + + return { response }; + } +} diff --git a/open-sse/executors/index.ts b/open-sse/executors/index.ts index 8678fb6105..34f3bafe73 100644 --- a/open-sse/executors/index.ts +++ b/open-sse/executors/index.ts @@ -14,6 +14,7 @@ import { BedrockExecutor } from "./bedrock.ts"; import { GlmExecutor } from "./glm.ts"; import { PollinationsExecutor } from "./pollinations.ts"; import { CloudflareAIExecutor } from "./cloudflare-ai.ts"; +import { FreebuffExecutor } from "./freebuff.ts"; import { OpencodeExecutor } from "./opencode.ts"; import { VertexExecutor } from "./vertex.ts"; import { CliproxyapiExecutor } from "./cliproxyapi.ts"; @@ -117,6 +118,8 @@ const executors = { pol: new PollinationsExecutor(), // Alias "cloudflare-ai": new CloudflareAIExecutor(), cf: new CloudflareAIExecutor(), // Alias + freebuff: new FreebuffExecutor(), + fb: new FreebuffExecutor(), // Alias "opencode-zen": new OpencodeExecutor("opencode-zen"), "opencode-go": new OpencodeExecutor("opencode-go"), opencode: new OpencodeExecutor("opencode-zen"), // Alias for opencode-zen diff --git a/public/providers/freebuff-dark.svg b/public/providers/freebuff-dark.svg new file mode 100644 index 0000000000..0a6d1156fb --- /dev/null +++ b/public/providers/freebuff-dark.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/public/providers/freebuff-light.svg b/public/providers/freebuff-light.svg new file mode 100644 index 0000000000..0a6d1156fb --- /dev/null +++ b/public/providers/freebuff-light.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/public/providers/freebuff.png b/public/providers/freebuff.png new file mode 100644 index 0000000000..54806e0831 Binary files /dev/null and b/public/providers/freebuff.png differ diff --git a/public/providers/freebuff.svg b/public/providers/freebuff.svg new file mode 100644 index 0000000000..0a6d1156fb --- /dev/null +++ b/public/providers/freebuff.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx index de377cd9cb..8bdddc2aa7 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx @@ -88,6 +88,7 @@ export default function AddApiKeyModal({ const isModal = provider === "modal"; const isGlm = isGlmProvider(provider); const isQoder = provider === "qoder"; + const isFreebuff = provider === "freebuff"; const openRouterPreset = useOpenRouterPresetControl(provider, t); const isCloudflare = provider === "cloudflare-ai"; const localProviderMetadata = getLocalProviderMetadata(provider); @@ -191,9 +192,11 @@ export default function AddApiKeyModal({ ? webSessionCredential.placeholder : isQoder ? t("qoderPatPlaceholder") - : apiKeyOptional - ? t("optional") - : undefined; + : isFreebuff + ? "Enter Freebuff / Codebuff Auth Token (e.g. 038fcdf9-...)" + : apiKeyOptional + ? t("optional") + : undefined; const apiCredentialHint = isModal ? providerText( t, @@ -202,8 +205,10 @@ export default function AddApiKeyModal({ ) : isQoder ? t("qoderPatHint") - : isWebSessionCredential - ? getWebSessionCredentialHint(t, webSessionCredential, providerDisplayName, false) + : isFreebuff + ? "Freebuff uses an authentic CLI auth token obtained via codebuff CLI login or automated harvester." + : isWebSessionCredential + ? getWebSessionCredentialHint(t, webSessionCredential, providerDisplayName, false) : isLocalSelfHostedProvider ? t("localProviderApiKeyOptionalHint", { provider: localProviderMetadata?.name || providerName || provider || "", diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index bb970b7af5..7f8180dee0 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -140,6 +140,37 @@ export { validateWebCookieProvider, bytezValidationResultFromStatus }; // validateKiroApiKeyRuntimeProbe now live in ./validation/webCookie and ./validation/kiro. // They are re-exported above to preserve the historical public surface. +export async function validateFreebuffProvider({ apiKey }: { apiKey: string }) { + if (!apiKey) { + return { valid: false, error: "Freebuff Auth Token required", unsupported: false }; + } + try { + const res = await fetch("https://www.codebuff.com/api/v1/freebuff/session", { + method: "POST", + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + "User-Agent": "codebuff/0.1.0 (darwin-arm64)", + "x-freebuff-model": "deepseek/deepseek-v4-flash", + }, + body: JSON.stringify({}), + signal: AbortSignal.timeout(15000), + }); + + if (res.ok || res.status === 409) { + return { valid: true, error: null }; + } + if (res.status === 401 || res.status === 403) { + return { valid: false, error: "Invalid or expired Freebuff Auth Token", unsupported: false }; + } + const errText = await res.text().catch(() => ""); + return { valid: false, error: `Freebuff validation returned ${res.status}: ${errText.slice(0, 100)}`, unsupported: false }; + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + return { valid: false, error: `Freebuff validation network error: ${msg}`, unsupported: false }; + } +} + export async function validateProviderApiKey({ provider, apiKey, providerSpecificData = {} }: any) { provider = typeof provider === "string" ? resolveProviderId(provider) : provider; const requiresApiKey = !providerAllowsOptionalApiKey(provider); @@ -196,6 +227,7 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi firefly: validateAdobeFireflyProvider, qoder: validateQoderProvider, kiro: validateKiroProvider, + freebuff: validateFreebuffProvider, "command-code": validateCommandCodeProvider, huggingface: validateHuggingFaceProvider, // #5422: auth-only probe — Bytez 404s on every chat model until the account adds it to diff --git a/src/shared/components/ProviderIcon.tsx b/src/shared/components/ProviderIcon.tsx index 9e3524561a..aeb6f777ba 100644 --- a/src/shared/components/ProviderIcon.tsx +++ b/src/shared/components/ProviderIcon.tsx @@ -115,6 +115,7 @@ const KNOWN_SVGS = new Set([ "fal", "fireworks", "freeaiapikey", + "freebuff", "freemodel-dev", "friendli", "galadriel", diff --git a/src/shared/constants/providers/apikey/gateways.ts b/src/shared/constants/providers/apikey/gateways.ts index 1f2bd59979..606d6eb493 100644 --- a/src/shared/constants/providers/apikey/gateways.ts +++ b/src/shared/constants/providers/apikey/gateways.ts @@ -19,6 +19,21 @@ export const APIKEY_PROVIDERS_GATEWAYS = { "Create an API key at https://cheaperinference.com/?utm_source=omniroute (needs the `inference` scope), then paste the ir_live_… token here.", passthroughModels: true, }, + freebuff: { + id: "freebuff", + alias: "freebuff", + name: "Freebuff", + icon: "terminal", + color: "#10B981", + textIcon: "FB", + website: "https://freebuff.com", + hasFree: true, + serviceKinds: ["llm"], + authHint: "Enter Freebuff / Codebuff Auth Token (obtained via CLI login or automated harvester).", + freeNote: "Free Codebuff / Freebuff AI models.", + apiHint: "Token is authenticated against Codebuff upstream session pool.", + passthroughModels: true, + }, "charm-hyper": { id: "charm-hyper", alias: "charm-hyper", diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json index 4fc9db6fdd..1878b2ac41 100644 --- a/tests/snapshots/provider/translate-path.json +++ b/tests/snapshots/provider/translate-path.json @@ -2164,6 +2164,29 @@ "stream": "https://freeinference.org/v1/chat/completions" } }, + "freebuff": { + "format": "openai", + "headers": { + "apiKey": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "nonStream": { + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "oauth": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json" + } + }, + "url": { + "nonStream": "https://www.codebuff.com/api/v1", + "stream": "https://www.codebuff.com/api/v1" + } + }, "freemodel-dev": { "format": "openai", "headers": { diff --git a/tests/unit/freebuff-provider.test.ts b/tests/unit/freebuff-provider.test.ts new file mode 100644 index 0000000000..d67de75f97 --- /dev/null +++ b/tests/unit/freebuff-provider.test.ts @@ -0,0 +1,59 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { FreebuffExecutor } from "../../open-sse/executors/freebuff.ts"; +import type { ExecuteInput } from "../../open-sse/executors/base.ts"; +import { freebuffProvider } from "../../open-sse/config/providers/registry/freebuff/index.ts"; +import { APIKEY_PROVIDERS_GATEWAYS } from "../../src/shared/constants/providers/apikey/gateways.ts"; +import { validateFreebuffProvider } from "../../src/lib/providers/validation.ts"; + +test("FreebuffExecutor: constructor initializes provider name correctly", () => { + const executor = new FreebuffExecutor(); + assert.equal(executor.getProvider(), "freebuff"); +}); + +test("FreebuffExecutor: returns 401 response when credentials are missing", async () => { + const executor = new FreebuffExecutor(); + const res = await executor.execute({ + model: "deepseek/deepseek-v4-flash", + body: { messages: [{ role: "user", content: "hello" }] }, + stream: false, + credentials: { apiKey: "" }, + } as unknown as ExecuteInput); + + assert.equal(res.response.status, 401); + const data = (await res.response.json()) as { error: { message: string } }; + assert.match(data.error.message, /Freebuff Auth Token required/i); +}); + +test("freebuffProvider: registry entry has valid structure and catalog", () => { + assert.equal(freebuffProvider.id, "freebuff"); + assert.equal(freebuffProvider.format, "openai"); + assert.equal(freebuffProvider.executor, "freebuff"); + assert.equal(freebuffProvider.baseUrl, "https://www.codebuff.com/api/v1"); + assert.ok(Array.isArray(freebuffProvider.models)); + assert.ok(freebuffProvider.models.length >= 8); + + const flash = freebuffProvider.models.find((m) => m.id === "deepseek/deepseek-v4-flash"); + assert.ok(flash, "deepseek/deepseek-v4-flash must exist in freebuff models"); + assert.equal(flash?.supportsReasoning, true); + + const minimax = freebuffProvider.models.find((m) => m.id === "minimax/minimax-m3"); + assert.ok(minimax, "minimax/minimax-m3 must exist in freebuff models"); + assert.equal(minimax?.supportsVision, true); +}); + +test("APIKEY_PROVIDERS_GATEWAYS: freebuff gateway metadata is defined", () => { + const fb = APIKEY_PROVIDERS_GATEWAYS.freebuff; + assert.ok(fb, "freebuff must be in APIKEY_PROVIDERS_GATEWAYS"); + assert.equal(fb.id, "freebuff"); + assert.equal(fb.name, "Freebuff"); + assert.equal(fb.color, "#10B981"); + assert.equal(fb.hasFree, true); +}); + +test("validateFreebuffProvider: returns invalid when apiKey is empty", async () => { + const res = await validateFreebuffProvider({ apiKey: "" }); + assert.equal(res.valid, false); + assert.match(res.error || "", /Freebuff Auth Token required/i); +}); diff --git a/tests/unit/providers-constants-split.test.ts b/tests/unit/providers-constants-split.test.ts index abc46e6870..9276da2206 100644 --- a/tests/unit/providers-constants-split.test.ts +++ b/tests/unit/providers-constants-split.test.ts @@ -23,7 +23,8 @@ // gateways family to 228 measured on the tip; Puter retired (#10210) and chatanywhere restored // (base-reds round 3, #9985) are both included in that measurement; Cursor API (specialty-media, // #10729) brings it to 229; Token Kiosk (gateways, #10722) — merged in the same -// merge-train batch — independently bumped the gateways family too, landing at 231. +// merge-train batch — independently bumped the gateways family too, landing at 231; Freebuff +// (gateways, #10531) brings it to 232. import { test } from "node:test"; import assert from "node:assert/strict"; @@ -52,12 +53,12 @@ test("barrel still exports every catalog + key helpers", () => { } }); -test("APIKEY_PROVIDERS merges the 6 family files into 231 entries (no loss / no dup)", async () => { +test("APIKEY_PROVIDERS merges the 6 family files into 232 entries (no loss / no dup)", async () => { const keys = Object.keys((P as Record).APIKEY_PROVIDERS); - assert.equal(keys.length, 231); - assert.equal(new Set(keys).size, 231, "duplicate keys after spread-merge"); + assert.equal(keys.length, 232); + assert.equal(new Set(keys).size, 232, "duplicate keys after spread-merge"); // the merged object's entry-count equals the sum of the 6 semantic family files; families are a - // strict partition (every provider in exactly one), so the sum must be exactly 231. + // strict partition (every provider in exactly one), so the sum must be exactly 232. const families: [string, string][] = [ ["gateways", "APIKEY_PROVIDERS_GATEWAYS"], ["frontier-labs", "APIKEY_PROVIDERS_FRONTIER"], @@ -77,7 +78,7 @@ test("APIKEY_PROVIDERS merges the 6 family files into 231 entries (no loss / no seen.add(k); } } - assert.equal(famTotal, 231, "families must partition all 231 providers"); + assert.equal(famTotal, 232, "families must partition all 232 providers"); }); test("AI_PROVIDERS Proxy aggregates all sections; lookups resolve", () => {