mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-21 14:42:20 +03:00
Adds native support for Freebuff (Codebuff CLI free-tier gateway): executor with upstream session acquisition, agent-run lifecycle (START/FINISH), canonical system-prompt injection, and model→agent mapping for 9 free models; registry entry, dashboard branding/icons, and API-key validation. Closes #6793. Validated in an isolated worktree boarded onto origin/release/v3.8.50 (0 conflicts after resolving generated provider-count drift, 15 files): - 9/9 focused tests pass (freebuff-provider, providers-constants-split). - Dropped one out-of-scope, unrelated hunk in scripts/build/build-next-isolated.mjs (build-memory heap tuning) that had nothing to do with the Freebuff provider — kept the branch scoped to its stated purpose. - check-changelog-integrity: OK. - file-size: gateways.ts and AddApiKeyModal.tsx crossed the frozen cap by +15/+5 lines (irreducible catalog-entry + credential-hint additions) — rebaselined with justification, pushed fix-in-place to the PR branch. - typecheck:core: clean. - check-complexity / check-cognitive-complexity: OK, both under baseline. Co-authored-by: adrianaryaputra <adrianaryaputra@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
28788cb9af
commit
cbf23772ec
@@ -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,
|
||||
|
||||
@@ -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<string, RegistryEntry> = {
|
||||
deepai: deepaiProvider,
|
||||
nebius: nebiusProvider,
|
||||
fireworks: fireworksProvider,
|
||||
freebuff: freebuffProvider,
|
||||
llamagate: llamagateProvider,
|
||||
glm: glmProvider,
|
||||
glmt: glmtProvider,
|
||||
|
||||
70
open-sse/config/providers/registry/freebuff/index.ts
Normal file
70
open-sse/config/providers/registry/freebuff/index.ts
Normal file
@@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
172
open-sse/executors/freebuff.ts
Normal file
172
open-sse/executors/freebuff.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import {
|
||||
BaseExecutor,
|
||||
type ExecuteInput,
|
||||
} from "./base.ts";
|
||||
import { PROVIDERS } from "../config/constants.ts";
|
||||
|
||||
const MODEL_TO_AGENT: Record<string, string> = {
|
||||
"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<string, unknown>).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<string, unknown>)?.codebuff_metadata as Record<string, unknown> || {}),
|
||||
},
|
||||
};
|
||||
|
||||
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 };
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
7
public/providers/freebuff-dark.svg
Normal file
7
public/providers/freebuff-dark.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<rect width="100" height="100" rx="22" fill="#000000"/>
|
||||
<!-- Sparkle in upper-left -->
|
||||
<path d="M 36 16 C 36 28 26 38 14 38 C 26 38 36 48 36 60 C 36 48 46 38 58 38 C 46 38 36 28 36 16 Z" fill="#FFFFFF"/>
|
||||
<!-- Terminal cursor bar -->
|
||||
<rect x="44" y="46" width="34" height="14" rx="3" fill="#FFFFFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 404 B |
7
public/providers/freebuff-light.svg
Normal file
7
public/providers/freebuff-light.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<rect width="100" height="100" rx="22" fill="#000000"/>
|
||||
<!-- Sparkle in upper-left -->
|
||||
<path d="M 36 16 C 36 28 26 38 14 38 C 26 38 36 48 36 60 C 36 48 46 38 58 38 C 46 38 36 28 36 16 Z" fill="#FFFFFF"/>
|
||||
<!-- Terminal cursor bar -->
|
||||
<rect x="44" y="46" width="34" height="14" rx="3" fill="#FFFFFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 404 B |
BIN
public/providers/freebuff.png
Normal file
BIN
public/providers/freebuff.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
7
public/providers/freebuff.svg
Normal file
7
public/providers/freebuff.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<rect width="100" height="100" rx="22" fill="#000000"/>
|
||||
<!-- Sparkle in upper-left -->
|
||||
<path d="M 36 16 C 36 28 26 38 14 38 C 26 38 36 48 36 60 C 36 48 46 38 58 38 C 46 38 36 28 36 16 Z" fill="#FFFFFF"/>
|
||||
<!-- Terminal cursor bar -->
|
||||
<rect x="44" y="46" width="34" height="14" rx="3" fill="#FFFFFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 404 B |
@@ -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 || "",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -115,6 +115,7 @@ const KNOWN_SVGS = new Set([
|
||||
"fal",
|
||||
"fireworks",
|
||||
"freeaiapikey",
|
||||
"freebuff",
|
||||
"freemodel-dev",
|
||||
"friendli",
|
||||
"galadriel",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -2164,6 +2164,29 @@
|
||||
"stream": "https://freeinference.org/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"freebuff": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"nonStream": "https://www.codebuff.com/api/v1",
|
||||
"stream": "https://www.codebuff.com/api/v1"
|
||||
}
|
||||
},
|
||||
"freemodel-dev": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
|
||||
59
tests/unit/freebuff-provider.test.ts
Normal file
59
tests/unit/freebuff-provider.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
@@ -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<string, object>).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", () => {
|
||||
|
||||
Reference in New Issue
Block a user