diff --git a/docs/docs.json b/docs/docs.json index 09b248990..103bf83ab 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -101,6 +101,10 @@ "source": "/openrouter/", "destination": "/providers/openrouter" }, + { + "source": "/poe", + "destination": "/providers/poe" + }, { "source": "/opencode", "destination": "/providers/opencode" @@ -984,6 +988,7 @@ "providers/moonshot", "providers/minimax", "providers/openrouter", + "providers/poe", "providers/synthetic", "providers/opencode", "providers/glm", diff --git a/docs/providers/index.md b/docs/providers/index.md index c4f020192..71ab3259d 100644 --- a/docs/providers/index.md +++ b/docs/providers/index.md @@ -37,6 +37,7 @@ See [Venice AI](/providers/venice). - [Anthropic (API + Claude Code CLI)](/providers/anthropic) - [Qwen (OAuth)](/providers/qwen) - [OpenRouter](/providers/openrouter) +- [Poe (Claude, GPT, Gemini, Llama)](/providers/poe) - [Vercel AI Gateway](/providers/vercel-ai-gateway) - [Moonshot AI (Kimi + Kimi Code)](/providers/moonshot) - [OpenCode Zen](/providers/opencode) diff --git a/docs/providers/poe.md b/docs/providers/poe.md new file mode 100644 index 000000000..063ce1cb3 --- /dev/null +++ b/docs/providers/poe.md @@ -0,0 +1,73 @@ +--- +summary: "Use Poe API to access Claude, GPT, Gemini, Llama, and hundreds of bots" +read_when: + - You want to use Poe API in Clawdbot + - You need access to multiple models with one API key +--- +# Poe + +Poe provides an API at `api.poe.com` that gives access to Claude, GPT, Gemini, Llama, and hundreds of community bots through a single API key. + +## Quick start + +1. Get an API key from [poe.com/api_key](https://poe.com/api_key) +2. Enable the plugin and authenticate: + +```bash +clawdbot plugins enable poe +clawdbot models auth login --provider poe +``` + +3. Enter your API key when prompted and select a default model. + +## CLI setup + +```bash +clawdbot onboard --auth-choice apiKey --token-provider poe --token "$POE_API_KEY" +``` + +## Config snippet + +```json5 +{ + env: { POE_API_KEY: "..." }, + agents: { + defaults: { + model: { primary: "poe/claude-sonnet-4.5" } + } + } +} +``` + +## Available models + +| Model ID | Name | Reasoning | Vision | Context | +|----------|------|-----------|--------|---------| +| claude-opus-4.5 | Claude Opus 4.5 | No | Yes | 200K | +| claude-sonnet-4.5 | Claude Sonnet 4.5 | No | Yes | 200K | +| claude-haiku-4.5 | Claude Haiku 4.5 | No | Yes | 200K | +| claude-code | Claude Code | No | Yes | 200K | +| gpt-5.2-codex | GPT-5.2 Codex | No | Yes | 128K | +| gpt-5.1-codex | GPT-5.1 Codex | No | Yes | 128K | +| gpt-5.1-codex-mini | GPT-5.1 Codex Mini | No | Yes | 128K | +| gpt-5.1-codex-max | GPT-5.1 Codex Max | No | Yes | 128K | +| o3-pro | o3 Pro | Yes | Yes | 128K | +| gemini-3-pro | Gemini 3 Pro | No | Yes | 128K | +| gemini-3-flash | Gemini 3 Flash | No | Yes | 128K | +| grok-4 | Grok 4 | No | Yes | 128K | +| deepseek-r1 | DeepSeek R1 | Yes | No | 128K | +| deepseek-v3.2 | DeepSeek V3.2 | No | No | 128K | + +Model IDs match the Poe API. Access additional bots by adding them to `models.providers.poe.models`. + +## Notes + +- Rate limit: 500 requests per minute +- Bot names are case-sensitive (use exact names from poe.com) +- Pricing varies by bot; check poe.com for current rates + +## Troubleshooting + +**401 Unauthorized**: Your API key may be invalid or expired. Get a new key from [poe.com/api_key](https://poe.com/api_key). + +**Model not found**: Verify the bot name is correct and available on your Poe account. Bot names are case-sensitive. diff --git a/extensions/poe/clawdbot.plugin.json b/extensions/poe/clawdbot.plugin.json new file mode 100644 index 000000000..4f352dc93 --- /dev/null +++ b/extensions/poe/clawdbot.plugin.json @@ -0,0 +1,11 @@ +{ + "id": "poe", + "providers": [ + "poe" + ], + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": {} + } +} diff --git a/extensions/poe/index.test.ts b/extensions/poe/index.test.ts new file mode 100644 index 000000000..2f7d4be71 --- /dev/null +++ b/extensions/poe/index.test.ts @@ -0,0 +1,109 @@ +import { describe, test, expect, vi, beforeEach, afterEach } from "vitest"; + +import poePlugin, { validatePoeApiKey } from "./index.js"; + +describe("poe plugin", () => { + test("plugin has correct metadata", () => { + expect(poePlugin.id).toBe("poe"); + expect(poePlugin.name).toBe("Poe"); + expect(poePlugin.description).toContain("Poe API"); + expect(poePlugin.configSchema).toBeDefined(); + expect(poePlugin.register).toBeInstanceOf(Function); + }); + + test("registers provider with correct properties", () => { + const registeredProviders: unknown[] = []; + const mockApi = { + registerProvider: (provider: unknown) => { + registeredProviders.push(provider); + }, + }; + + poePlugin.register(mockApi as never); + + expect(registeredProviders).toHaveLength(1); + const provider = registeredProviders[0] as Record; + + expect(provider.id).toBe("poe"); + expect(provider.label).toBe("Poe"); + expect(provider.docsPath).toBe("/providers/poe"); + expect(provider.envVars).toContain("POE_API_KEY"); + expect(provider.auth).toHaveLength(1); + + const auth = provider.auth as Array>; + expect(auth[0].id).toBe("api_key"); + expect(auth[0].kind).toBe("api_key"); + }); +}); + +describe("validatePoeApiKey", () => { + const originalFetch = globalThis.fetch; + + beforeEach(() => { + vi.resetAllMocks(); + }); + + afterEach(() => { + globalThis.fetch = originalFetch; + }); + + test("returns true for valid API key", async () => { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + }); + + const result = await validatePoeApiKey("valid-key"); + + expect(result).toBe(true); + expect(globalThis.fetch).toHaveBeenCalledWith( + "https://api.poe.com/v1/models", + expect.objectContaining({ + method: "GET", + headers: { + Authorization: "Bearer valid-key", + }, + }), + ); + }); + + test("returns false for invalid API key", async () => { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 401, + }); + + const result = await validatePoeApiKey("invalid-key"); + + expect(result).toBe(false); + }); + + test("returns false on network error", async () => { + globalThis.fetch = vi.fn().mockRejectedValue(new Error("Network error")); + + const result = await validatePoeApiKey("any-key"); + + expect(result).toBe(false); + }); +}); + +describe("poe model definitions", () => { + test("all models have required fields", () => { + const registeredProviders: unknown[] = []; + const mockApi = { + registerProvider: (provider: unknown) => { + registeredProviders.push(provider); + }, + }; + + poePlugin.register(mockApi as never); + + // Access auth run function context to check model building + const provider = registeredProviders[0] as Record; + const auth = provider.auth as Array>; + expect(auth[0]).toBeDefined(); + + // Verify provider has expected structure + expect(provider.id).toBe("poe"); + }); +}); diff --git a/extensions/poe/index.ts b/extensions/poe/index.ts new file mode 100644 index 000000000..9fd219b98 --- /dev/null +++ b/extensions/poe/index.ts @@ -0,0 +1,253 @@ +import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk"; + +const POE_BASE_URL = "https://api.poe.com/v1"; +const ENV_VAR = "POE_API_KEY"; + +// Poe model definitions - IDs match api.poe.com/v1/models +const POE_MODELS = [ + // Claude models + { + id: "claude-opus-4.5", + name: "Claude Opus 4.5", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 200_000, + maxTokens: 8192, + }, + { + id: "claude-sonnet-4.5", + name: "Claude Sonnet 4.5", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 200_000, + maxTokens: 8192, + }, + { + id: "claude-haiku-4.5", + name: "Claude Haiku 4.5", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 200_000, + maxTokens: 8192, + }, + { + id: "claude-code", + name: "Claude Code", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 200_000, + maxTokens: 8192, + }, + // GPT models + { + id: "gpt-5.2-codex", + name: "GPT-5.2 Codex", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 128_000, + maxTokens: 16_384, + }, + { + id: "gpt-5.1-codex", + name: "GPT-5.1 Codex", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 128_000, + maxTokens: 16_384, + }, + { + id: "gpt-5.1-codex-mini", + name: "GPT-5.1 Codex Mini", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 128_000, + maxTokens: 16_384, + }, + { + id: "gpt-5.1-codex-max", + name: "GPT-5.1 Codex Max", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 128_000, + maxTokens: 16_384, + }, + { + id: "o3-pro", + name: "o3 Pro", + reasoning: true, + input: ["text", "image"] as const, + contextWindow: 128_000, + maxTokens: 16_384, + }, + // Gemini models + { + id: "gemini-3-pro", + name: "Gemini 3 Pro", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 128_000, + maxTokens: 8192, + }, + { + id: "gemini-3-flash", + name: "Gemini 3 Flash", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 128_000, + maxTokens: 8192, + }, + // Other models + { + id: "grok-4", + name: "Grok 4", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 128_000, + maxTokens: 8192, + }, + { + id: "deepseek-r1", + name: "DeepSeek R1", + reasoning: true, + input: ["text"] as const, + contextWindow: 128_000, + maxTokens: 8192, + }, + { + id: "deepseek-v3.2", + name: "DeepSeek V3.2", + reasoning: false, + input: ["text"] as const, + contextWindow: 128_000, + maxTokens: 8192, + }, +] as const; + +function buildModelDefinition(model: (typeof POE_MODELS)[number]) { + return { + id: model.id, + name: model.name, + api: "openai-completions" as const, + reasoning: model.reasoning, + input: [...model.input], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: model.contextWindow, + maxTokens: model.maxTokens, + }; +} + +/** + * Validates a Poe API key by making a test request to the models endpoint. + */ +export async function validatePoeApiKey(apiKey: string): Promise { + try { + const response = await fetch(`${POE_BASE_URL}/models`, { + method: "GET", + headers: { + Authorization: `Bearer ${apiKey}`, + }, + }); + return response.ok; + } catch { + return false; + } +} + +const poePlugin = { + id: "poe", + name: "Poe", + description: "Poe API provider plugin (api.poe.com)", + configSchema: emptyPluginConfigSchema(), + register(api) { + api.registerProvider({ + id: "poe", + label: "Poe", + docsPath: "/providers/poe", + envVars: [ENV_VAR], + auth: [ + { + id: "api_key", + label: "API Key", + hint: "Enter your Poe API key from poe.com/api_key", + kind: "api_key", + run: async (ctx) => { + const apiKeyInput = await ctx.prompter.text({ + message: "Poe API key (from poe.com/api_key)", + validate: (value) => { + const trimmed = value.trim(); + if (!trimmed) return "API key is required"; + return undefined; + }, + }); + + const apiKey = apiKeyInput.trim(); + const spin = ctx.prompter.progress("Validating Poe API key..."); + + const isValid = await validatePoeApiKey(apiKey); + if (!isValid) { + spin.stop("Validation failed"); + throw new Error( + "Invalid API key. Get a valid key from https://poe.com/api_key", + ); + } + + spin.stop("API key validated"); + + // Let user pick a default model + const modelChoices = POE_MODELS.map((m) => ({ + value: m.id, + label: m.name, + })); + + const selectedModel = await ctx.prompter.select({ + message: "Select a default model", + options: modelChoices, + }); + + const defaultModelRef = `poe/${selectedModel}`; + + return { + profiles: [ + { + profileId: "poe:default", + credential: { + type: "token", + provider: "poe", + token: apiKey, + }, + }, + ], + configPatch: { + models: { + providers: { + poe: { + baseUrl: POE_BASE_URL, + apiKey, + api: "openai-completions", + models: POE_MODELS.map((m) => buildModelDefinition(m)), + }, + }, + }, + agents: { + defaults: { + models: Object.fromEntries( + POE_MODELS.map((m) => [`poe/${m.id}`, {}]), + ), + }, + }, + }, + defaultModel: defaultModelRef, + notes: [ + "Poe API has a rate limit of 500 requests per minute.", + "Bot names are used as model IDs (e.g., Claude-Sonnet-4.5).", + "Access additional bots by adding them to models.providers.poe.models.", + ], + }; + }, + }, + ], + }); + }, +}; + +export default poePlugin; diff --git a/extensions/poe/package.json b/extensions/poe/package.json new file mode 100644 index 000000000..2415440ad --- /dev/null +++ b/extensions/poe/package.json @@ -0,0 +1,11 @@ +{ + "name": "@clawdbot/poe", + "version": "2026.1.25", + "type": "module", + "description": "Clawdbot Poe provider plugin", + "clawdbot": { + "extensions": [ + "./index.ts" + ] + } +}