diff --git a/src/agents/pollinations-models.ts b/src/agents/pollinations-models.ts new file mode 100644 index 000000000..52eb11f80 --- /dev/null +++ b/src/agents/pollinations-models.ts @@ -0,0 +1,81 @@ +import type { ModelDefinitionConfig } from "../config/types.js"; + +export const POLLINATIONS_BASE_URL = "https://gen.pollinations.ai/v1"; +export const POLLINATIONS_DEFAULT_MODEL_ID = "openai"; +export const POLLINATIONS_DEFAULT_MODEL_REF = `pollinations/${POLLINATIONS_DEFAULT_MODEL_ID}`; +export const POLLINATIONS_MINIMAX_MODEL_ID = "minimax"; +export const POLLINATIONS_MINIMAX_MODEL_REF = `pollinations/${POLLINATIONS_MINIMAX_MODEL_ID}`; + +export const POLLINATIONS_DEFAULT_COST = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, +}; + +export const POLLINATIONS_MODEL_CATALOG = [ + { + id: POLLINATIONS_DEFAULT_MODEL_ID, + name: "Pollinations OpenAI", + reasoning: false, + input: ["text"], + contextWindow: 128000, + maxTokens: 8192, + }, + { + id: POLLINATIONS_MINIMAX_MODEL_ID, + name: "Pollinations MiniMax", + reasoning: false, + input: ["text"], + contextWindow: 128000, + maxTokens: 8192, + }, + { + id: "openai-fast", + name: "Pollinations OpenAI Fast", + reasoning: false, + input: ["text"], + contextWindow: 128000, + maxTokens: 8192, + }, + { + id: "openai-large", + name: "Pollinations OpenAI Large", + reasoning: false, + input: ["text"], + contextWindow: 128000, + maxTokens: 8192, + }, + { + id: "qwen-coder", + name: "Pollinations Qwen Coder", + reasoning: false, + input: ["text"], + contextWindow: 128000, + maxTokens: 8192, + }, + { + id: "mistral", + name: "Pollinations Mistral", + reasoning: false, + input: ["text"], + contextWindow: 128000, + maxTokens: 8192, + }, +] as const; + +export type PollinationsCatalogEntry = (typeof POLLINATIONS_MODEL_CATALOG)[number]; + +export function buildPollinationsModelDefinition( + entry: PollinationsCatalogEntry, +): ModelDefinitionConfig { + return { + id: entry.id, + name: entry.name, + reasoning: entry.reasoning, + input: [...entry.input] as ("text" | "image")[], + cost: POLLINATIONS_DEFAULT_COST, + contextWindow: entry.contextWindow, + maxTokens: entry.maxTokens, + }; +} diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 6b49ff17b..7a3209219 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -15,6 +15,7 @@ export type AuthChoiceGroupId = | "openrouter" | "ai-gateway" | "moonshot" + | "pollinations" | "zai" | "opencode-zen" | "minimax" @@ -101,6 +102,12 @@ const AUTH_CHOICE_GROUP_DEFS: { hint: "Kimi K2 + Kimi Code", choices: ["moonshot-api-key", "kimi-code-api-key"], }, + { + value: "pollinations", + label: "Pollinations", + hint: "API key (enter.pollinations.ai)", + choices: ["pollinations-api-key"], + }, { value: "zai", label: "Z.AI (GLM 4.7)", @@ -164,6 +171,7 @@ export function buildAuthChoiceOptions(params: { hint: "Uses the bundled Gemini CLI auth plugin", }); options.push({ value: "zai-api-key", label: "Z.AI (GLM 4.7) API key" }); + options.push({ value: "pollinations-api-key", label: "Pollinations API key" }); options.push({ value: "qwen-portal", label: "Qwen OAuth" }); options.push({ value: "copilot-proxy", diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index 8be02008b..52981fa38 100644 --- a/src/commands/auth-choice.apply.api-providers.ts +++ b/src/commands/auth-choice.apply.api-providers.ts @@ -21,6 +21,8 @@ import { applyOpencodeZenProviderConfig, applyOpenrouterConfig, applyOpenrouterProviderConfig, + applyPollinationsConfig, + applyPollinationsProviderConfig, applySyntheticConfig, applySyntheticProviderConfig, applyVeniceConfig, @@ -31,6 +33,7 @@ import { KIMI_CODE_MODEL_REF, MOONSHOT_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, + POLLINATIONS_DEFAULT_MODEL_REF, SYNTHETIC_DEFAULT_MODEL_REF, VENICE_DEFAULT_MODEL_REF, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, @@ -39,6 +42,7 @@ import { setMoonshotApiKey, setOpencodeZenApiKey, setOpenrouterApiKey, + setPollinationsApiKey, setSyntheticApiKey, setVeniceApiKey, setVercelAiGatewayApiKey, @@ -85,9 +89,69 @@ export async function applyAuthChoiceApiProviders( authChoice = "venice-api-key"; } else if (params.opts.tokenProvider === "opencode") { authChoice = "opencode-zen"; + } else if (params.opts.tokenProvider === "pollinations") { + authChoice = "pollinations-api-key"; } } + if (authChoice === "pollinations-api-key") { + let hasCredential = false; + + if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "pollinations") { + await setPollinationsApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + hasCredential = true; + } + + if (!hasCredential) { + await params.prompter.note( + [ + "Pollinations provides access to various AI models via a unified API.", + "Get your API key at: https://enter.pollinations.ai", + ].join("\n"), + "Pollinations", + ); + } + + const envKey = resolveEnvApiKey("pollinations"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing POLLINATIONS_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setPollinationsApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } + } + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter Pollinations API key", + validate: validateApiKeyInput, + }); + await setPollinationsApiKey(normalizeApiKeyInput(String(key)), params.agentDir); + } + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "pollinations:default", + provider: "pollinations", + mode: "api_key", + }); + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: POLLINATIONS_DEFAULT_MODEL_REF, + applyDefaultConfig: applyPollinationsConfig, + applyProviderConfig: applyPollinationsProviderConfig, + noteDefault: POLLINATIONS_DEFAULT_MODEL_REF, + noteAgentModel, + prompter: params.prompter, + }); + nextConfig = applied.config; + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; + } + return { config: nextConfig, agentModelOverride }; + } + if (authChoice === "openrouter-api-key") { const store = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false, diff --git a/src/commands/auth-choice.preferred-provider.ts b/src/commands/auth-choice.preferred-provider.ts index 6fe26b59a..60dd71e79 100644 --- a/src/commands/auth-choice.preferred-provider.ts +++ b/src/commands/auth-choice.preferred-provider.ts @@ -20,6 +20,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = { "zai-api-key": "zai", "synthetic-api-key": "synthetic", "venice-api-key": "venice", + "pollinations-api-key": "pollinations", "github-copilot": "github-copilot", "copilot-proxy": "copilot-proxy", "minimax-cloud": "minimax", diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index 921ee01d1..1d507d88c 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -4,6 +4,12 @@ import { SYNTHETIC_DEFAULT_MODEL_REF, SYNTHETIC_MODEL_CATALOG, } from "../agents/synthetic-models.js"; +import { + buildPollinationsModelDefinition, + POLLINATIONS_BASE_URL, + POLLINATIONS_DEFAULT_MODEL_REF, + POLLINATIONS_MODEL_CATALOG, +} from "../agents/pollinations-models.js"; import { buildVeniceModelDefinition, VENICE_BASE_URL, @@ -411,6 +417,81 @@ export function applyVeniceConfig(cfg: MoltbotConfig): MoltbotConfig { }; } +/** + * Apply Pollinations provider configuration without changing the default model. + */ +export function applyPollinationsProviderConfig(cfg: ClawdbotConfig): ClawdbotConfig { + const models = { ...cfg.agents?.defaults?.models }; + models[POLLINATIONS_DEFAULT_MODEL_REF] = { + ...models[POLLINATIONS_DEFAULT_MODEL_REF], + alias: models[POLLINATIONS_DEFAULT_MODEL_REF]?.alias ?? "Pollinations", + }; + + const providers = { ...cfg.models?.providers }; + const existingProvider = providers.pollinations; + const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : []; + const pollinationsModels = POLLINATIONS_MODEL_CATALOG.map(buildPollinationsModelDefinition); + const mergedModels = [ + ...existingModels, + ...pollinationsModels.filter( + (model: any) => !existingModels.some((existing: any) => 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.pollinations = { + ...existingProviderRest, + baseUrl: POLLINATIONS_BASE_URL, + api: "openai-completions", + ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}), + models: mergedModels.length > 0 ? mergedModels : pollinationsModels, + }; + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + models, + }, + }, + models: { + mode: cfg.models?.mode ?? "merge", + providers, + }, + }; +} + +/** + * Apply Pollinations provider configuration AND set Pollinations as the default model. + */ +export function applyPollinationsConfig(cfg: ClawdbotConfig): ClawdbotConfig { + const next = applyPollinationsProviderConfig(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: POLLINATIONS_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..16a0ac9a2 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -164,3 +164,15 @@ export async function setOpencodeZenApiKey(key: string, agentDir?: string) { agentDir: resolveAuthAgentDir(agentDir), }); } + +export async function setPollinationsApiKey(key: string, agentDir?: string) { + upsertAuthProfile({ + profileId: "pollinations:default", + credential: { + type: "api_key", + provider: "pollinations", + key, + }, + agentDir: resolveAuthAgentDir(agentDir), + }); +} diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index b122d89cf..b370e04e5 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -3,6 +3,10 @@ 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 { + POLLINATIONS_DEFAULT_MODEL_ID, + POLLINATIONS_DEFAULT_MODEL_REF, +} from "../agents/pollinations-models.js"; export { applyAuthProfileConfig, applyKimiCodeConfig, @@ -11,6 +15,8 @@ export { applyMoonshotProviderConfig, applyOpenrouterConfig, applyOpenrouterProviderConfig, + applyPollinationsConfig, + applyPollinationsProviderConfig, applySyntheticConfig, applySyntheticProviderConfig, applyVeniceConfig, @@ -41,6 +47,7 @@ export { setMoonshotApiKey, setOpencodeZenApiKey, setOpenrouterApiKey, + setPollinationsApiKey, setSyntheticApiKey, setVeniceApiKey, setVercelAiGatewayApiKey, diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index aa1d9afe0..66f78be99 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -23,6 +23,7 @@ export type AuthChoice = | "google-antigravity" | "google-gemini-cli" | "zai-api-key" + | "pollinations-api-key" | "minimax-cloud" | "minimax" | "minimax-api" @@ -70,6 +71,7 @@ export type OnboardOptions = { minimaxApiKey?: string; syntheticApiKey?: string; veniceApiKey?: string; + pollinationsApiKey?: string; opencodeZenApiKey?: string; gatewayPort?: number; gatewayBind?: GatewayBind;