From 683e9fee4f310b61f8be74c0d93d45dca86b58c5 Mon Sep 17 00:00:00 2001 From: User Date: Wed, 28 Jan 2026 19:22:38 -0500 Subject: [PATCH] feat: add Clarifai as model provider --- docs/providers/clarifai.md | 122 ++++++++++++++++++ docs/providers/index.md | 1 + src/agents/clarifai-models.ts | 73 +++++++++++ src/agents/model-auth.ts | 5 + src/agents/models-config.providers.ts | 20 +++ src/commands/auth-choice-options.ts | 12 ++ .../auth-choice.apply.api-providers.ts | 75 +++++++++++ src/commands/onboard-auth.config-core.ts | 82 ++++++++++++ src/commands/onboard-auth.credentials.ts | 15 +++ src/commands/onboard-auth.ts | 8 ++ src/commands/onboard-types.ts | 1 + 11 files changed, 414 insertions(+) create mode 100644 docs/providers/clarifai.md create mode 100644 src/agents/clarifai-models.ts diff --git a/docs/providers/clarifai.md b/docs/providers/clarifai.md new file mode 100644 index 000000000..1c54a8e76 --- /dev/null +++ b/docs/providers/clarifai.md @@ -0,0 +1,122 @@ +# Clarifai + +Clarifai provides access to many LLMs via an OpenAI-compatible API endpoint. + +## Important: Full Model URL Required + +Clarifai requires the **complete model URL** as the model ID, not just a simple model name. The format is: + +``` +https://clarifai.com/{user_id}/{app_id}/models/{model_id} +``` + +Or with a specific version: + +``` +https://clarifai.com/{user_id}/{app_id}/models/{model_id}/versions/{version_id} +``` + +For example: +- `https://clarifai.com/openai/chat-completion/models/gpt-4-turbo` +- `https://clarifai.com/openai/chat-completion/models/gpt-oss-120b/versions/f1d2ad8c01c74705868f5c8ae4a1ff7c` + + +Browse available models at: https://clarifai.com/explore/models + +## CLI setup + +```bash +moltbot onboard --auth-choice clarifai-api-key --token "$CLARIFAI_PAT" +``` + +Or interactively: + +```bash +moltbot onboard +# Select "Clarifai" when prompted for auth provider +``` + +## Environment variables + +Clarifai accepts either environment variable: + +- `CLARIFAI_API_KEY` — Your Clarifai Personal Access Token (PAT) +- `CLARIFAI_PAT` — Alternative name for the same token + +## Config snippet + +```json5 +{ + env: { CLARIFAI_API_KEY: "your-pat-here" }, + agents: { + defaults: { + // The model ID is the full Clarifai URL + model: { primary: "clarifai/https://clarifai.com/openai/chat-completion/models/gpt-4-turbo" } + } + } +} +``` + +## Getting your API key (PAT) + +1. Go to [Clarifai Settings → Security](https://clarifai.com/settings/security) +2. Create a new Personal Access Token (PAT) +3. Copy the token and use it as your API key + +## Available models + +Models are referenced using the full Clarifai URL. Examples: + +| Model Reference | Description | +|-----------------|-------------| +| `clarifai/https://clarifai.com/openai/chat-completion/models/gpt-4-turbo` | GPT-4 Turbo via Clarifai | +| `clarifai/https://clarifai.com/openai/chat-completion/models/gpt-oss-120b` | GPT-OSS 120B | +| `clarifai/https://clarifai.com/meta/Llama-2/models/llama2-70b-chat` | Llama 2 70B Chat | + +Find more models at: https://clarifai.com/explore/models + +## Notes + +- Model refs use the format `clarifai/{full_clarifai_url}` +- Uses OpenAI-compatible `/v1/chat/completions` endpoint +- PAT (Personal Access Token) is used for authentication +- The endpoint is: `https://api.clarifai.com/v2/ext/openai/v1` + +## Manual provider configuration + +For custom setups or additional models: + +```json5 +{ + models: { + providers: { + clarifai: { + baseUrl: "https://api.clarifai.com/v2/ext/openai/v1", + api: "openai-completions", + apiKey: "your-pat-here", + models: [ + { + // Full model URL as the ID + id: "https://clarifai.com/openai/chat-completion/models/gpt-4-turbo", + name: "GPT-4 Turbo (Clarifai)", + reasoning: false, + input: ["text", "image"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128000, + maxTokens: 4096 + }, + { + id: "https://clarifai.com/meta/Llama-2/models/llama2-70b-chat", + name: "Llama 2 70B Chat", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 4096, + maxTokens: 4096 + } + ] + } + } + } +} +``` diff --git a/docs/providers/index.md b/docs/providers/index.md index c18ad70fb..aa4b0f97b 100644 --- a/docs/providers/index.md +++ b/docs/providers/index.md @@ -45,6 +45,7 @@ See [Venice AI](/providers/venice). - [GLM models](/providers/glm) - [MiniMax](/providers/minimax) - [Venius (Venice AI, privacy-focused)](/providers/venice) +- [Clarifai (API & Local)](/providers/clarifai) - [Ollama (local models)](/providers/ollama) ## Transcription providers diff --git a/src/agents/clarifai-models.ts b/src/agents/clarifai-models.ts new file mode 100644 index 000000000..38c346725 --- /dev/null +++ b/src/agents/clarifai-models.ts @@ -0,0 +1,73 @@ +import type { ModelDefinitionConfig } from "../config/types.js"; + +// Clarifai OpenAI-compatible endpoint +// Docs: https://docs.clarifai.com/api-guide/predict/llms +// Note: The model ID must be the FULL Clarifai model URL +// Format: https://clarifai.com/{user_id}/{app_id}/models/{model_id} +// Or with version: https://clarifai.com/{user_id}/{app_id}/models/{model_id}/versions/{version_id} +export const CLARIFAI_BASE_URL = "https://api.clarifai.com/v2/ext/openai/v1"; + +// Default model - the FULL URL is required as the model name +// Example: https://clarifai.com/openai/chat-completion/models/gpt-oss-120b +export const CLARIFAI_DEFAULT_MODEL_ID = "https://clarifai.com/openai/chat-completion/models/gpt-oss-120b"; +export const CLARIFAI_DEFAULT_MODEL_REF = "clarifai/gpt-oss-120b"; + +// Clarifai uses PAT (Personal Access Token) for authentication +// Pricing varies by model and compute tier +export const CLARIFAI_DEFAULT_COST = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, +}; + +/** + * Clarifai model catalog. + * + * IMPORTANT: Clarifai requires the FULL model URL as the model ID. + * Format: https://clarifai.com/{user_id}/{app_id}/models/{model_id} + * Or with version: https://clarifai.com/{user_id}/{app_id}/models/{model_id}/versions/{version_id} + * + * Examples: + * - https://clarifai.com/openai/chat-completion/models/gpt-oss-120b/versions/f1d2a.... + * + * Authentication: Uses PAT (Personal Access Token), also called API key. + * Set via CLARIFAI_API_KEY or CLARIFAI_PAT environment variable. + * + * Browse available models at: https://clarifai.com/explore/models + */ +export const CLARIFAI_MODEL_CATALOG = [ + { + id: "https://clarifai.com/openai/chat-completion/models/gpt-oss-120b", + name: "GPT-OSS 120B (via Clarifai)", + reasoning: false, + input: ["text"] as Array<"text" | "image">, + contextWindow: 128000, + maxTokens: 4096, + } +]; + +export function buildClarifaiModelDefinition( + model: (typeof CLARIFAI_MODEL_CATALOG)[number], +): ModelDefinitionConfig { + return { + id: model.id, + name: model.name, + reasoning: model.reasoning, + input: model.input, + cost: CLARIFAI_DEFAULT_COST, + contextWindow: model.contextWindow, + maxTokens: model.maxTokens, + }; +} + +/** + * Discover models from Clarifai API. + * Falls back to static catalog if API is unreachable. + */ +export async function discoverClarifaiModels(): Promise { + // For now, use static catalog + // Clarifai's model listing requires specific API calls that differ from + // the OpenAI-compatible endpoint, so we use a curated catalog + return CLARIFAI_MODEL_CATALOG.map(buildClarifaiModelDefinition); +} diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 96e4e4ae6..df3f2a482 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -269,6 +269,10 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null { return pick("QWEN_OAUTH_TOKEN") ?? pick("QWEN_PORTAL_API_KEY"); } + if (normalized === "clarifai") { + return pick("CLARIFAI_API_KEY") ?? pick("CLARIFAI_PAT"); + } + const envMap: Record = { openai: "OPENAI_API_KEY", google: "GEMINI_API_KEY", @@ -283,6 +287,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null { minimax: "MINIMAX_API_KEY", synthetic: "SYNTHETIC_API_KEY", venice: "VENICE_API_KEY", + clarifai: "CLARIFAI_API_KEY", mistral: "MISTRAL_API_KEY", opencode: "OPENCODE_API_KEY", }; diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index a176dac8a..c1d585893 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -7,6 +7,11 @@ import { import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js"; import { resolveAwsSdkEnvVarName, resolveEnvApiKey } from "./model-auth.js"; import { discoverBedrockModels } from "./bedrock-discovery.js"; +import { + buildClarifaiModelDefinition, + CLARIFAI_BASE_URL, + CLARIFAI_MODEL_CATALOG, +} from "./clarifai-models.js"; import { buildSyntheticModelDefinition, SYNTHETIC_BASE_URL, @@ -350,6 +355,14 @@ async function buildVeniceProvider(): Promise { }; } +function buildClarifaiProvider(): ProviderConfig { + return { + baseUrl: CLARIFAI_BASE_URL, + api: "openai-completions", + models: CLARIFAI_MODEL_CATALOG.map(buildClarifaiModelDefinition), + }; +} + async function buildOllamaProvider(): Promise { const models = await discoverOllamaModels(); return { @@ -402,6 +415,13 @@ export async function resolveImplicitProviders(params: { providers.venice = { ...(await buildVeniceProvider()), apiKey: veniceKey }; } + const clarifaiKey = + resolveEnvApiKeyVarName("clarifai") ?? + resolveApiKeyFromProfiles({ provider: "clarifai", store: authStore }); + if (clarifaiKey) { + providers.clarifai = { ...buildClarifaiProvider(), apiKey: clarifaiKey }; + } + const qwenProfiles = listProfilesForProvider(authStore, "qwen-portal"); if (qwenProfiles.length > 0) { providers["qwen-portal"] = { diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 6b49ff17b..045ed02ce 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -20,6 +20,7 @@ export type AuthChoiceGroupId = | "minimax" | "synthetic" | "venice" + | "clarifai" | "qwen"; export type AuthChoiceGroup = { @@ -71,6 +72,12 @@ const AUTH_CHOICE_GROUP_DEFS: { hint: "Privacy-focused (uncensored models)", choices: ["venice-api-key"], }, + { + value: "clarifai", + label: "Clarifai", + hint: "Clarifai OpenAI-compatible API (PAT + model URL)", + choices: ["clarifai-api-key"], + }, { value: "google", label: "Google", @@ -147,6 +154,11 @@ export function buildAuthChoiceOptions(params: { label: "Venice AI API key", hint: "Privacy-focused inference (uncensored models)", }); + options.push({ + value: "clarifai-api-key", + label: "Clarifai API key (PAT)", + hint: "LLMs via OpenAI-compatible API (model URL required)", + }); options.push({ value: "github-copilot", label: "GitHub Copilot (GitHub device login)", diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index 8be02008b..576ef3cfa 100644 --- a/src/commands/auth-choice.apply.api-providers.ts +++ b/src/commands/auth-choice.apply.api-providers.ts @@ -13,6 +13,8 @@ import { } from "./google-gemini-model-default.js"; import { applyAuthProfileConfig, + applyClarifaiConfig, + applyClarifaiProviderConfig, applyKimiCodeConfig, applyKimiCodeProviderConfig, applyMoonshotConfig, @@ -28,12 +30,14 @@ import { applyVercelAiGatewayConfig, applyVercelAiGatewayProviderConfig, applyZaiConfig, + CLARIFAI_DEFAULT_MODEL_REF, KIMI_CODE_MODEL_REF, MOONSHOT_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, SYNTHETIC_DEFAULT_MODEL_REF, VENICE_DEFAULT_MODEL_REF, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, + setClarifaiApiKey, setGeminiApiKey, setKimiCodeApiKey, setMoonshotApiKey, @@ -83,6 +87,8 @@ export async function applyAuthChoiceApiProviders( authChoice = "synthetic-api-key"; } else if (params.opts.tokenProvider === "venice") { authChoice = "venice-api-key"; + } else if (params.opts.tokenProvider === "clarifai") { + authChoice = "clarifai-api-key"; } else if (params.opts.tokenProvider === "opencode") { authChoice = "opencode-zen"; } @@ -522,6 +528,75 @@ export async function applyAuthChoiceApiProviders( return { config: nextConfig, agentModelOverride }; } + if (authChoice === "clarifai-api-key") { + let hasCredential = false; + + if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "clarifai") { + await setClarifaiApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + hasCredential = true; + } + + if (!hasCredential) { + await params.prompter.note( + [ + "Clarifai provides access to LLMs via OpenAI-compatible API.", + "You need two things:", + " 1. Personal Access Token (PAT): https://clarifai.com/settings/security", + " 2. Full model URL (used as model name in API calls)", + "", + "Model URL format: https://clarifai.com/{user}/{app}/models/{model}", + "", + "Examples:", + " - https://clarifai.com/openai/chat-completion/models/gpt-4-turbo", + " - https://clarifai.com/openai/chat-completion/models/gpt-oss-120b", + " - https://clarifai.com/meta/Llama-2/models/llama2-70b-chat", + "", + "Browse models at: https://clarifai.com/explore/models", + ].join("\n"), + "Clarifai", + ); + } + + const envKey = resolveEnvApiKey("clarifai"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing CLARIFAI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setClarifaiApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } + } + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter Clarifai API key (PAT)", + validate: validateApiKeyInput, + }); + await setClarifaiApiKey(normalizeApiKeyInput(String(key)), params.agentDir); + } + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "clarifai:default", + provider: "clarifai", + mode: "api_key", + }); + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: CLARIFAI_DEFAULT_MODEL_REF, + applyDefaultConfig: applyClarifaiConfig, + applyProviderConfig: applyClarifaiProviderConfig, + noteDefault: CLARIFAI_DEFAULT_MODEL_REF, + noteAgentModel, + prompter: params.prompter, + }); + nextConfig = applied.config; + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; + } + return { config: nextConfig, agentModelOverride }; + } + if (authChoice === "opencode-zen") { let hasCredential = false; if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "opencode") { diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index 921ee01d1..8b87950fb 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -1,3 +1,9 @@ +import { + buildClarifaiModelDefinition, + CLARIFAI_BASE_URL, + CLARIFAI_DEFAULT_MODEL_REF, + CLARIFAI_MODEL_CATALOG, +} from "../agents/clarifai-models.js"; import { buildSyntheticModelDefinition, SYNTHETIC_BASE_URL, @@ -12,6 +18,7 @@ import { } from "../agents/venice-models.js"; import type { MoltbotConfig } from "../config/config.js"; import { + CLARIFAI_DEFAULT_MODEL_REF as CLARIFAI_DEFAULT_MODEL_REF_CRED, OPENROUTER_DEFAULT_MODEL_REF, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, ZAI_DEFAULT_MODEL_REF, @@ -411,6 +418,81 @@ export function applyVeniceConfig(cfg: MoltbotConfig): MoltbotConfig { }; } +/** + * Apply Clarifai provider configuration without changing the default model. + * Registers Clarifai models and sets up the provider, but preserves existing model selection. + */ +export function applyClarifaiProviderConfig(cfg: MoltbotConfig): MoltbotConfig { + const models = { ...cfg.agents?.defaults?.models }; + models[CLARIFAI_DEFAULT_MODEL_REF] = { + ...models[CLARIFAI_DEFAULT_MODEL_REF], + alias: models[CLARIFAI_DEFAULT_MODEL_REF]?.alias ?? "GPT-4 Turbo (Clarifai)", + }; + + const providers = { ...cfg.models?.providers }; + const existingProvider = providers.clarifai; + const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : []; + const clarifaiModels = CLARIFAI_MODEL_CATALOG.map(buildClarifaiModelDefinition); + const mergedModels = [ + ...existingModels, + ...clarifaiModels.filter((model) => !existingModels.some((existing) => existing.id === model.id)), + ]; + const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record< + string, + unknown + > as { apiKey?: string }; + const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined; + const normalizedApiKey = resolvedApiKey?.trim(); + providers.clarifai = { + ...existingProviderRest, + baseUrl: CLARIFAI_BASE_URL, + api: "openai-completions", + ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}), + models: mergedModels.length > 0 ? mergedModels : clarifaiModels, + }; + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + models, + }, + }, + models: { + mode: cfg.models?.mode ?? "merge", + providers, + }, + }; +} + +/** + * Apply Clarifai provider configuration AND set Clarifai as the default model. + * Use this when Clarifai is the primary provider choice during onboarding. + */ +export function applyClarifaiConfig(cfg: MoltbotConfig): MoltbotConfig { + const next = applyClarifaiProviderConfig(cfg); + const existingModel = next.agents?.defaults?.model; + return { + ...next, + agents: { + ...next.agents, + defaults: { + ...next.agents?.defaults, + model: { + ...(existingModel && "fallbacks" in (existingModel as Record) + ? { + fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks, + } + : undefined), + primary: CLARIFAI_DEFAULT_MODEL_REF, + }, + }, + }, + }; +} + export function applyAuthProfileConfig( cfg: MoltbotConfig, params: { diff --git a/src/commands/onboard-auth.credentials.ts b/src/commands/onboard-auth.credentials.ts index b2fb58542..93270f80b 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -112,9 +112,24 @@ export async function setVeniceApiKey(key: string, agentDir?: string) { }); } +export async function setClarifaiApiKey(key: string, agentDir?: string) { + // Write to resolved agent dir so gateway finds credentials on startup. + upsertAuthProfile({ + profileId: "clarifai:default", + credential: { + type: "api_key", + provider: "clarifai", + key, + }, + agentDir: resolveAuthAgentDir(agentDir), + }); +} + export const ZAI_DEFAULT_MODEL_REF = "zai/glm-4.7"; export const OPENROUTER_DEFAULT_MODEL_REF = "openrouter/auto"; export const VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF = "vercel-ai-gateway/anthropic/claude-opus-4.5"; +// Clarifai model IDs are full URL paths: {user_id}/{app_id}/models/{model_id} +export const CLARIFAI_DEFAULT_MODEL_REF = "clarifai/openai/chat-completion/models/gpt-4-turbo"; export async function setZaiApiKey(key: string, agentDir?: string) { // Write to resolved agent dir so gateway finds credentials on startup. diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index b122d89cf..2df6cf4ca 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -3,8 +3,14 @@ export { SYNTHETIC_DEFAULT_MODEL_REF, } from "../agents/synthetic-models.js"; export { VENICE_DEFAULT_MODEL_ID, VENICE_DEFAULT_MODEL_REF } from "../agents/venice-models.js"; +export { + CLARIFAI_DEFAULT_MODEL_ID, + CLARIFAI_DEFAULT_MODEL_REF, +} from "../agents/clarifai-models.js"; export { applyAuthProfileConfig, + applyClarifaiConfig, + applyClarifaiProviderConfig, applyKimiCodeConfig, applyKimiCodeProviderConfig, applyMoonshotConfig, @@ -33,8 +39,10 @@ export { applyOpencodeZenProviderConfig, } from "./onboard-auth.config-opencode.js"; export { + CLARIFAI_DEFAULT_MODEL_REF as CLARIFAI_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, setAnthropicApiKey, + setClarifaiApiKey, setGeminiApiKey, setKimiCodeApiKey, setMinimaxApiKey, diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index aa1d9afe0..4e1b6e85f 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -17,6 +17,7 @@ export type AuthChoice = | "kimi-code-api-key" | "synthetic-api-key" | "venice-api-key" + | "clarifai-api-key" | "codex-cli" | "apiKey" | "gemini-api-key"