diff --git a/docs/providers/index.md b/docs/providers/index.md index b2793ee22..2fa3bc42d 100644 --- a/docs/providers/index.md +++ b/docs/providers/index.md @@ -46,6 +46,7 @@ See [Venice AI](/providers/venice). - [GLM models](/providers/glm) - [MiniMax](/providers/minimax) - [Venius (Venice AI, privacy-focused)](/providers/venice) +- [PixelML (multi-model API)](/providers/pixelml) - [Ollama (local models)](/providers/ollama) ## Transcription providers diff --git a/docs/providers/pixelml.md b/docs/providers/pixelml.md new file mode 100644 index 000000000..59a02dddc --- /dev/null +++ b/docs/providers/pixelml.md @@ -0,0 +1,141 @@ +--- +summary: "Use PixelML multi-model API in Clawdbot" +read_when: + - You want to use PixelML models in Clawdbot + - You need PixelML setup guidance +--- +# PixelML + +PixelML provides a unified OpenAI-compatible API for accessing multiple AI models including GPT-4o, Claude, and more. + +## Features + +- **Multi-model access**: Use GPT, Claude, and other models through a single API +- **OpenAI-compatible**: Standard `/v1` endpoints for easy integration +- **Streaming**: Supported on all models +- **Vision**: Supported on models with vision capability + +## Setup + +### 1. Get API Key + +1. Sign up at [platform.pixelml.com](https://platform.pixelml.com) +2. Go to your account settings and create an API key +3. Copy your API key + +### 2. Configure Clawdbot + +**Option A: Environment Variable** + +```bash +export PIXELML_API_KEY="your-api-key" +``` + +**Option B: Interactive Setup (Recommended)** + +```bash +clawdbot onboard --auth-choice pixelml-api-key +``` + +This will: +1. Prompt for your API key (or use existing `PIXELML_API_KEY`) +2. Configure the provider automatically +3. Set the default model + +**Option C: Non-interactive** + +```bash +clawdbot onboard --non-interactive \ + --auth-choice pixelml-api-key \ + --pixelml-api-key "your-api-key" +``` + +### 3. Verify Setup + +```bash +clawdbot chat --model pixelml/gpt-4o-mini "Hello, are you working?" +``` + +## Available Models + +| Model ID | Name | Context | Features | +|----------|------|---------|----------| +| `gpt-4o-mini` | GPT-4o Mini | 128k | Vision | +| `gpt-4o` | GPT-4o | 128k | Vision | +| `claude-4.5-haiku` | Claude 4.5 Haiku | 200k | Vision | +| `claude-4.5-sonnet` | Claude 4.5 Sonnet | 200k | Vision, Reasoning | + +## Model Selection + +Change your default model anytime: + +```bash +clawdbot models set pixelml/gpt-4o-mini +clawdbot models set pixelml/claude-4.5-sonnet +``` + +List all available models: + +```bash +clawdbot models list | grep pixelml +``` + +## Usage Examples + +```bash +# Use GPT-4o Mini (default) +clawdbot chat --model pixelml/gpt-4o-mini "Hello" + +# Use Claude via PixelML +clawdbot chat --model pixelml/claude-4.5-sonnet "Write a poem" + +# Use GPT-4o for vision tasks +clawdbot chat --model pixelml/gpt-4o +``` + +## Config File Example + +```json5 +{ + env: { PIXELML_API_KEY: "your-api-key" }, + agents: { defaults: { model: { primary: "pixelml/gpt-4o-mini" } } }, + models: { + mode: "merge", + providers: { + pixelml: { + baseUrl: "https://ishi.pixelml.com/v1", + apiKey: "${PIXELML_API_KEY}", + api: "openai-completions", + models: [ + { + id: "gpt-4o-mini", + name: "GPT-4o Mini", + reasoning: false, + input: ["text", "image"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128000, + maxTokens: 16384 + } + ] + } + } + } +} +``` + +## Troubleshooting + +### API key not recognized + +```bash +echo $PIXELML_API_KEY +clawdbot models list | grep pixelml +``` + +### Connection issues + +PixelML API is at `https://ishi.pixelml.com/v1`. Ensure your network allows HTTPS connections. + +## Links + +- [PixelML](https://platform.pixelml.com) diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 1445b53f7..f83450e23 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -284,6 +284,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null { xiaomi: "XIAOMI_API_KEY", synthetic: "SYNTHETIC_API_KEY", venice: "VENICE_API_KEY", + pixelml: "PIXELML_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 0cd034c82..9c880c585 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -12,6 +12,7 @@ import { SYNTHETIC_BASE_URL, SYNTHETIC_MODEL_CATALOG, } from "./synthetic-models.js"; +import { discoverPixelmlModels, PIXELML_BASE_URL } from "./pixelml-models.js"; import { discoverVeniceModels, VENICE_BASE_URL } from "./venice-models.js"; type ModelsConfig = NonNullable; @@ -379,6 +380,15 @@ async function buildVeniceProvider(): Promise { }; } +async function buildPixelmlProvider(apiKey?: string): Promise { + const models = await discoverPixelmlModels(apiKey); + return { + baseUrl: PIXELML_BASE_URL, + api: "openai-completions", + models, + }; +} + async function buildOllamaProvider(): Promise { const models = await discoverOllamaModels(); return { @@ -431,6 +441,13 @@ export async function resolveImplicitProviders(params: { providers.venice = { ...(await buildVeniceProvider()), apiKey: veniceKey }; } + const pixelmlKey = + resolveEnvApiKeyVarName("pixelml") ?? + resolveApiKeyFromProfiles({ provider: "pixelml", store: authStore }); + if (pixelmlKey) { + providers.pixelml = { ...(await buildPixelmlProvider(pixelmlKey)), apiKey: pixelmlKey }; + } + const qwenProfiles = listProfilesForProvider(authStore, "qwen-portal"); if (qwenProfiles.length > 0) { providers["qwen-portal"] = { diff --git a/src/agents/pixelml-models.ts b/src/agents/pixelml-models.ts new file mode 100644 index 000000000..5ba72750f --- /dev/null +++ b/src/agents/pixelml-models.ts @@ -0,0 +1,188 @@ +import type { ModelDefinitionConfig } from "../config/types.js"; + +export const PIXELML_BASE_URL = "https://ishi.pixelml.com/v1"; +export const PIXELML_DEFAULT_MODEL_ID = "gpt-4o-mini"; +export const PIXELML_DEFAULT_MODEL_REF = `pixelml/${PIXELML_DEFAULT_MODEL_ID}`; + +// PixelML uses a unified pricing model; set to 0 as costs vary by model. +export const PIXELML_DEFAULT_COST = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, +}; + +/** + * Static catalog of PixelML models. + * PixelML provides a unified OpenAI-compatible API for multiple AI models. + * + * This catalog serves as a fallback when the PixelML API is unreachable. + */ +export const PIXELML_MODEL_CATALOG = [ + // GPT models + { + id: "gpt-4o-mini", + name: "GPT-4o Mini", + reasoning: false, + input: ["text", "image"], + contextWindow: 128000, + maxTokens: 16384, + }, + { + id: "gpt-4o", + name: "GPT-4o", + reasoning: false, + input: ["text", "image"], + contextWindow: 128000, + maxTokens: 16384, + }, + // Claude models + { + id: "claude-4.5-haiku", + name: "Claude 4.5 Haiku", + reasoning: false, + input: ["text", "image"], + contextWindow: 200000, + maxTokens: 8192, + }, + { + id: "claude-4.5-sonnet", + name: "Claude 4.5 Sonnet", + reasoning: true, + input: ["text", "image"], + contextWindow: 200000, + maxTokens: 8192, + }, +] as const; + +export type PixelmlCatalogEntry = (typeof PIXELML_MODEL_CATALOG)[number]; + +/** + * Build a ModelDefinitionConfig from a PixelML catalog entry. + */ +export function buildPixelmlModelDefinition(entry: PixelmlCatalogEntry): ModelDefinitionConfig { + return { + id: entry.id, + name: entry.name, + reasoning: entry.reasoning, + input: [...entry.input], + cost: PIXELML_DEFAULT_COST, + contextWindow: entry.contextWindow, + maxTokens: entry.maxTokens, + }; +} + +// PixelML API response types +interface PixelmlModelCost { + input: number; + output: number; +} + +interface PixelmlModelLimit { + context: number; + output: number; +} + +interface PixelmlModelModalities { + input: Array<"text" | "image">; + output: Array<"text" | "image">; +} + +interface PixelmlModel { + id: string; + name: string; + family?: string; + release_date?: string; + attachment?: boolean; + reasoning?: boolean; + temperature?: boolean; + tool_call?: boolean; + cost?: PixelmlModelCost; + limit?: PixelmlModelLimit; + modalities?: PixelmlModelModalities; + options?: Record; +} + +// Response can be either { data: [...] } or a flat array +type PixelmlModelsResponse = { data: PixelmlModel[] } | PixelmlModel[]; + +/** + * Discover models from PixelML API with fallback to static catalog. + * The /models endpoint requires authentication. + */ +export async function discoverPixelmlModels(apiKey?: string): Promise { + // Skip API discovery in test environment + if (process.env.NODE_ENV === "test" || process.env.VITEST) { + return PIXELML_MODEL_CATALOG.map(buildPixelmlModelDefinition); + } + + // Without an API key, return the static catalog + if (!apiKey?.trim()) { + return PIXELML_MODEL_CATALOG.map(buildPixelmlModelDefinition); + } + + try { + const response = await fetch(`${PIXELML_BASE_URL}/models`, { + headers: { + Authorization: `Bearer ${apiKey}`, + }, + signal: AbortSignal.timeout(5000), + }); + + if (!response.ok) { + console.warn( + `[pixelml-models] Failed to discover models: HTTP ${response.status}, using static catalog`, + ); + return PIXELML_MODEL_CATALOG.map(buildPixelmlModelDefinition); + } + + const rawData = (await response.json()) as PixelmlModelsResponse; + // Handle both { data: [...] } and flat array responses + const apiModels = Array.isArray(rawData) ? rawData : rawData.data; + if (!Array.isArray(apiModels) || apiModels.length === 0) { + console.warn("[pixelml-models] No models found from API, using static catalog"); + return PIXELML_MODEL_CATALOG.map(buildPixelmlModelDefinition); + } + + // Build models from API response + const models: ModelDefinitionConfig[] = []; + + for (const apiModel of apiModels) { + // Extract model ID without provider prefix (e.g., "pixelml/gpt-5.1" -> "gpt-5.1") + const modelId = apiModel.id.includes("/") + ? apiModel.id.split("/").slice(1).join("/") + : apiModel.id; + + // Use API-provided modalities, filtering to only supported types (text, image) + const rawModalities = apiModel.modalities?.input ?? ["text"]; + const inputModalities: Array<"text" | "image"> = rawModalities.filter( + (m): m is "text" | "image" => m === "text" || m === "image", + ); + // Ensure at least "text" is present + if (inputModalities.length === 0) { + inputModalities.push("text"); + } + + // Use API-provided values with sensible defaults + models.push({ + id: modelId, + name: apiModel.name || modelId, + reasoning: apiModel.reasoning ?? false, + input: inputModalities, + cost: { + input: apiModel.cost?.input ?? 0, + output: apiModel.cost?.output ?? 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: apiModel.limit?.context ?? 128000, + maxTokens: apiModel.limit?.output ?? 8192, + }); + } + + return models.length > 0 ? models : PIXELML_MODEL_CATALOG.map(buildPixelmlModelDefinition); + } catch (error) { + console.warn(`[pixelml-models] Discovery failed: ${String(error)}, using static catalog`); + return PIXELML_MODEL_CATALOG.map(buildPixelmlModelDefinition); + } +} diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 5acddf4e3..adb73445b 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -21,6 +21,7 @@ export type AuthChoiceGroupId = | "minimax" | "synthetic" | "venice" + | "pixelml" | "qwen"; export type AuthChoiceGroup = { @@ -72,6 +73,12 @@ const AUTH_CHOICE_GROUP_DEFS: { hint: "Privacy-focused (uncensored models)", choices: ["venice-api-key"], }, + { + value: "pixelml", + label: "PixelML", + hint: "Multi-model API (GPT, Claude)", + choices: ["pixelml-api-key"], + }, { value: "google", label: "Google", @@ -154,6 +161,11 @@ export function buildAuthChoiceOptions(params: { label: "Venice AI API key", hint: "Privacy-focused inference (uncensored models)", }); + options.push({ + value: "pixelml-api-key", + label: "PixelML API key", + hint: "Multi-model API (GPT, Claude via PixelML)", + }); 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 fa4fc77e7..0b29b0946 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, + applyPixelmlConfig, + applyPixelmlProviderConfig, applySyntheticConfig, applySyntheticProviderConfig, applyVeniceConfig, @@ -33,6 +35,7 @@ import { KIMI_CODE_MODEL_REF, MOONSHOT_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, + PIXELML_DEFAULT_MODEL_REF, SYNTHETIC_DEFAULT_MODEL_REF, VENICE_DEFAULT_MODEL_REF, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, @@ -42,6 +45,7 @@ import { setMoonshotApiKey, setOpencodeZenApiKey, setOpenrouterApiKey, + setPixelmlApiKey, setSyntheticApiKey, setVeniceApiKey, setVercelAiGatewayApiKey, @@ -89,6 +93,8 @@ export async function applyAuthChoiceApiProviders( authChoice = "synthetic-api-key"; } else if (params.opts.tokenProvider === "venice") { authChoice = "venice-api-key"; + } else if (params.opts.tokenProvider === "pixelml") { + authChoice = "pixelml-api-key"; } else if (params.opts.tokenProvider === "opencode") { authChoice = "opencode-zen"; } @@ -576,6 +582,64 @@ export async function applyAuthChoiceApiProviders( return { config: nextConfig, agentModelOverride }; } + if (authChoice === "pixelml-api-key") { + let hasCredential = false; + + if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "pixelml") { + await setPixelmlApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + hasCredential = true; + } + + if (!hasCredential) { + await params.prompter.note( + [ + "PixelML provides access to multiple AI models (GPT, Claude, etc.).", + "Get your API key at: https://platform.pixelml.com", + ].join("\n"), + "PixelML", + ); + } + + const envKey = resolveEnvApiKey("pixelml"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing PIXELML_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setPixelmlApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } + } + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter PixelML API key", + validate: validateApiKeyInput, + }); + await setPixelmlApiKey(normalizeApiKeyInput(String(key)), params.agentDir); + } + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "pixelml:default", + provider: "pixelml", + mode: "api_key", + }); + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: PIXELML_DEFAULT_MODEL_REF, + applyDefaultConfig: applyPixelmlConfig, + applyProviderConfig: applyPixelmlProviderConfig, + noteDefault: PIXELML_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/auth-choice.preferred-provider.ts b/src/commands/auth-choice.preferred-provider.ts index a4d831c92..a6db85e39 100644 --- a/src/commands/auth-choice.preferred-provider.ts +++ b/src/commands/auth-choice.preferred-provider.ts @@ -21,6 +21,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = { "xiaomi-api-key": "xiaomi", "synthetic-api-key": "synthetic", "venice-api-key": "venice", + "pixelml-api-key": "pixelml", "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 c94eeb51b..e75dfad8c 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -1,4 +1,10 @@ import { buildXiaomiProvider, XIAOMI_DEFAULT_MODEL_ID } from "../agents/models-config.providers.js"; +import { + buildPixelmlModelDefinition, + PIXELML_BASE_URL, + PIXELML_DEFAULT_MODEL_REF, + PIXELML_MODEL_CATALOG, +} from "../agents/pixelml-models.js"; import { buildSyntheticModelDefinition, SYNTHETIC_BASE_URL, @@ -484,6 +490,91 @@ export function applyVeniceConfig(cfg: OpenClawConfig): OpenClawConfig { }; } +/** + * Apply PixelML provider configuration without changing the default model. + * Registers PixelML models and sets up the provider, but preserves existing model selection. + */ +export function applyPixelmlProviderConfig(cfg: ClawdbotConfig): ClawdbotConfig { + const models = { ...cfg.agents?.defaults?.models }; + // Add all catalog models to the allowlist + for (const entry of PIXELML_MODEL_CATALOG) { + const modelRef = `pixelml/${entry.id}`; + if (!models[modelRef]) { + models[modelRef] = {}; + } + } + // Set alias for the default model + models[PIXELML_DEFAULT_MODEL_REF] = { + ...models[PIXELML_DEFAULT_MODEL_REF], + alias: models[PIXELML_DEFAULT_MODEL_REF]?.alias ?? "GPT-4o Mini", + }; + + const providers = { ...cfg.models?.providers }; + const existingProvider = providers.pixelml; + const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : []; + const pixelmlModels = PIXELML_MODEL_CATALOG.map(buildPixelmlModelDefinition); + const mergedModels = [ + ...existingModels, + ...pixelmlModels.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.pixelml = { + ...existingProviderRest, + baseUrl: PIXELML_BASE_URL, + api: "openai-completions", + ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}), + models: mergedModels.length > 0 ? mergedModels : pixelmlModels, + }; + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + models, + }, + }, + models: { + mode: cfg.models?.mode ?? "merge", + providers, + }, + }; +} + +/** + * Apply PixelML provider configuration AND set PixelML as the default model. + * Use this when PixelML is the primary provider choice during onboarding. + */ +export function applyPixelmlConfig(cfg: ClawdbotConfig): ClawdbotConfig { + const next = applyPixelmlProviderConfig(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: PIXELML_DEFAULT_MODEL_REF, + }, + }, + }, + }; +} + export function applyAuthProfileConfig( cfg: OpenClawConfig, params: { diff --git a/src/commands/onboard-auth.credentials.ts b/src/commands/onboard-auth.credentials.ts index fbf6dbfb9..c1374e619 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -112,6 +112,19 @@ export async function setVeniceApiKey(key: string, agentDir?: string) { }); } +export async function setPixelmlApiKey(key: string, agentDir?: string) { + // Write to resolved agent dir so gateway finds credentials on startup. + upsertAuthProfile({ + profileId: "pixelml:default", + credential: { + type: "api_key", + provider: "pixelml", + key, + }, + agentDir: resolveAuthAgentDir(agentDir), + }); +} + export const ZAI_DEFAULT_MODEL_REF = "zai/glm-4.7"; export const XIAOMI_DEFAULT_MODEL_REF = "xiaomi/mimo-v2-flash"; export const OPENROUTER_DEFAULT_MODEL_REF = "openrouter/auto"; diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index 612b24865..b39ee97e5 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -1,3 +1,4 @@ +export { PIXELML_DEFAULT_MODEL_ID, PIXELML_DEFAULT_MODEL_REF } from "../agents/pixelml-models.js"; export { SYNTHETIC_DEFAULT_MODEL_ID, SYNTHETIC_DEFAULT_MODEL_REF, @@ -11,6 +12,8 @@ export { applyMoonshotProviderConfig, applyOpenrouterConfig, applyOpenrouterProviderConfig, + applyPixelmlConfig, + applyPixelmlProviderConfig, applySyntheticConfig, applySyntheticProviderConfig, applyVeniceConfig, @@ -43,6 +46,7 @@ export { setMoonshotApiKey, setOpencodeZenApiKey, setOpenrouterApiKey, + setPixelmlApiKey, setSyntheticApiKey, setVeniceApiKey, setVercelAiGatewayApiKey, diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index f4154bc6d..3a19ce883 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" + | "pixelml-api-key" | "codex-cli" | "apiKey" | "gemini-api-key" @@ -72,6 +73,7 @@ export type OnboardOptions = { minimaxApiKey?: string; syntheticApiKey?: string; veniceApiKey?: string; + pixelmlApiKey?: string; opencodeZenApiKey?: string; gatewayPort?: number; gatewayBind?: GatewayBind;