From ff0a718e650841bfaf415fc16e702d55dea810d0 Mon Sep 17 00:00:00 2001 From: Jean Brito Date: Sat, 25 Apr 2026 16:45:23 -0300 Subject: [PATCH] feat(providers): add CrofAI as a built-in API-key provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- open-sse/config/providerRegistry.ts | 31 +++++++++++++++++++ src/shared/components/ProviderIcon.tsx | 1 + src/shared/constants/config.ts | 1 + src/shared/constants/providers.ts | 9 ++++++ tests/unit/crof-provider.test.ts | 43 ++++++++++++++++++++++++++ 5 files changed, 85 insertions(+) create mode 100644 tests/unit/crof-provider.test.ts diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index b48b3dc9d9..4bc9830c31 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -916,6 +916,37 @@ export const REGISTRY: Record = { ], }, + 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", diff --git a/src/shared/components/ProviderIcon.tsx b/src/shared/components/ProviderIcon.tsx index ef561fb77d..219150dc82 100644 --- a/src/shared/components/ProviderIcon.tsx +++ b/src/shared/components/ProviderIcon.tsx @@ -49,6 +49,7 @@ const LOBEHUB_PROVIDER_MAP: Record = { "ollama-cloud": "ollama", minimax: "minimax", "minimax-cn": "minimax", + crof: "crof", qwen: "qwen", alibaba: "qwen", moonshot: "moonshot", diff --git a/src/shared/constants/config.ts b/src/shared/constants/config.ts index 9b5df09757..68db579746 100644 --- a/src/shared/constants/config.ts +++ b/src/shared/constants/config.ts @@ -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", diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index 535477becb..0a6caf1fe1 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -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", diff --git a/tests/unit/crof-provider.test.ts b/tests/unit/crof-provider.test.ts new file mode 100644 index 0000000000..c6e7703555 --- /dev/null +++ b/tests/unit/crof-provider.test.ts @@ -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` + ); + } +});