feat(providers): add CrofAI as a built-in API-key provider

CrofAI (https://crof.ai) ships an OpenAI-compatible /v1 endpoint with
Bearer auth and a /v1/models discovery route. It hosts a curated set of
hosted open models (DeepSeek V3.2/V4 Pro, Kimi K2.5/K2.6, GLM 4.7/5.x,
Gemma 4, MiniMax M2.5, Qwen3.5/3.6) that today users can only attach
through the generic "Add OpenAI Compatible" flow — losing branding,
defaults, prefix routing, and showing up under "API Key Compatible
Providers" instead of the curated "API Key Providers" section.

This change wires CrofAI as a first-class built-in, mirroring how Kimi
is registered (OpenAI format + bearer auth + seed model list).

Files touched (kept tight):

- src/shared/constants/providers.ts
  Add `crof` to APIKEY_PROVIDERS with id/alias/name/icon/color/textIcon/website.

- src/shared/constants/config.ts
  Register PROVIDER_ENDPOINTS.crof = "https://crof.ai/v1/chat/completions".

- open-sse/config/providerRegistry.ts
  Add a `crof` REGISTRY entry: format "openai", executor "default",
  authType "apikey", authHeader "bearer", and a seed model list pulled
  from a live GET https://crof.ai/v1/models on 2026-04-25 (DeepSeek,
  Kimi, GLM, Gemma, MiniMax, Qwen variants). Runtime /models discovery
  keeps the live catalog up to date.

- src/shared/components/ProviderIcon.tsx
  Map `crof -> "crof"` in PROVIDER_ICON_MAP. Not added to PNG_PROVIDERS
  so the UI falls back to the textIcon ("CR") until a logo asset ships
  at public/providers/crof.png.

- tests/unit/crof-provider.test.ts (new)
  Pins the registration shape (provider identity, base URL, registry
  entry, seed model families). Required by the repo's PR Test Policy.

Verified end-to-end against a locally rebuilt image:
- CrofAI appears under "API Key Providers" alongside GLM Coding/Minimax.
- The connection-add dialog opens with title "Add CrofAI API Key".
- POST /api/providers/validate returns "Valid" against /v1/models.
- POST /api/providers persists a connection (testStatus=active).
- POST /v1/chat/completions { model: "crof/kimi-k2.5", ... } returns 200
  with a real Kimi K2.5 response — full proxy path resolves through the
  new registry entry.
- npm run test:unit passes locally including tests/unit/crof-provider.test.ts.

Notes for the maintainer:

- No logo file is included; happy to follow up with a PNG for
  public/providers/crof.png. Until then the UI uses the "CR" textIcon.
- Pricing (src/shared/constants/pricing.ts) is intentionally not
  hardcoded — CrofAI exposes per-model pricing via /v1/models response,
  and runtime sync is preferable to stale baked-in numbers. Happy to add
  a stub block if requested.
- Anthropic-compatible endpoint (https://anthropic.nahcrof.com/v1/messages)
  is not wired in this PR. Users can still add it via "Add Anthropic
  Compatible" if needed.
This commit is contained in:
Jean Brito
2026-04-25 16:45:23 -03:00
parent 08d0e9f8b4
commit ff0a718e65
5 changed files with 85 additions and 0 deletions

View File

@@ -916,6 +916,37 @@ export const REGISTRY: Record<string, RegistryEntry> = {
],
},
crof: {
id: "crof",
alias: "crof",
format: "openai",
executor: "default",
baseUrl: "https://crof.ai/v1/chat/completions",
authType: "apikey",
authHeader: "bearer",
// Seed list — runtime /v1/models discovery keeps this fresh.
// Source: GET https://crof.ai/v1/models (2026-04-25).
models: [
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro" },
{ id: "deepseek-v3.2", name: "DeepSeek V3.2" },
{ id: "kimi-k2.6", name: "Kimi K2.6" },
{ id: "kimi-k2.6-precision", name: "Kimi K2.6 (Precision)" },
{ id: "kimi-k2.5", name: "Kimi K2.5" },
{ id: "kimi-k2.5-lightning", name: "Kimi K2.5 (Lightning)" },
{ id: "glm-5.1", name: "GLM 5.1" },
{ id: "glm-5.1-precision", name: "GLM 5.1 (Precision)" },
{ id: "glm-5", name: "GLM 5" },
{ id: "glm-4.7", name: "GLM 4.7" },
{ id: "glm-4.7-flash", name: "GLM 4.7 Flash" },
{ id: "gemma-4-31b-it", name: "Gemma 4 31B" },
{ id: "minimax-m2.5", name: "MiniMax M2.5" },
{ id: "qwen3.6-27b", name: "Qwen3.6 27B" },
{ id: "qwen3.5-397b-a17b", name: "Qwen3.5 397B A17B" },
{ id: "qwen3.5-9b", name: "Qwen3.5 9B" },
{ id: "qwen3.5-9b-chat", name: "Qwen3.5 9B (Chat)" },
],
},
alicode: {
id: "alicode",
alias: "alicode",

View File

@@ -49,6 +49,7 @@ const LOBEHUB_PROVIDER_MAP: Record<string, string> = {
"ollama-cloud": "ollama",
minimax: "minimax",
"minimax-cn": "minimax",
crof: "crof",
qwen: "qwen",
alibaba: "qwen",
moonshot: "moonshot",

View File

@@ -27,6 +27,7 @@ export const PROVIDER_ENDPOINTS = {
"kimi-coding-apikey": "https://api.kimi.com/coding/v1/messages",
minimax: "https://api.minimax.io/anthropic/v1/messages",
"minimax-cn": "https://api.minimaxi.com/anthropic/v1/messages",
crof: "https://crof.ai/v1/chat/completions",
openai: "https://api.openai.com/v1/chat/completions",
anthropic: "https://api.anthropic.com/v1/messages",
gemini: "https://generativelanguage.googleapis.com/v1beta/models",

View File

@@ -172,6 +172,15 @@ export const APIKEY_PROVIDERS = {
textIcon: "MC",
website: "https://www.minimaxi.com",
},
crof: {
id: "crof",
alias: "crof",
name: "CrofAI",
icon: "auto_awesome",
color: "#0EA5E9",
textIcon: "CR",
website: "https://crof.ai",
},
alicode: {
id: "alicode",
alias: "alicode",

View File

@@ -0,0 +1,43 @@
import test from "node:test";
import assert from "node:assert/strict";
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
const { PROVIDER_ENDPOINTS } = await import("../../src/shared/constants/config.ts");
const { REGISTRY: providerRegistry } = await import("../../open-sse/config/providerRegistry.ts");
test("CrofAI is registered as an API-key provider with the canonical identity", () => {
const crof = APIKEY_PROVIDERS.crof;
assert.ok(crof, "APIKEY_PROVIDERS.crof must be defined");
assert.equal(crof.id, "crof");
assert.equal(crof.alias, "crof");
assert.equal(crof.name, "CrofAI");
assert.equal(crof.website, "https://crof.ai");
assert.equal(typeof crof.textIcon, "string");
});
test("CrofAI exposes the OpenAI-compatible chat completions URL", () => {
assert.equal(PROVIDER_ENDPOINTS.crof, "https://crof.ai/v1/chat/completions");
});
test("CrofAI registry entry uses OpenAI format with bearer apikey auth", () => {
const entry = providerRegistry.crof;
assert.ok(entry, "providerRegistry.crof must be defined");
assert.equal(entry.id, "crof");
assert.equal(entry.format, "openai");
assert.equal(entry.executor, "default");
assert.equal(entry.authType, "apikey");
assert.equal(entry.authHeader, "bearer");
assert.equal(entry.baseUrl, "https://crof.ai/v1/chat/completions");
});
test("CrofAI seed model list includes the headline families and unique ids", () => {
const ids = providerRegistry.crof.models.map((m: { id: string }) => m.id);
assert.ok(ids.length >= 10, "expect a non-trivial seed list");
assert.equal(new Set(ids).size, ids.length, "model ids must be unique");
for (const family of ["deepseek-v", "kimi-k2", "glm-", "qwen3"]) {
assert.ok(
ids.some((id: string) => id.startsWith(family)),
`seed list must include ${family}* model`
);
}
});