feat(providers): add Poe API provider plugin
Add Poe (api.poe.com) as a provider plugin with OpenAI-compatible API. - Create extensions/poe with plugin implementation - Add API key validation via /v1/models endpoint - Include 8 popular models: Claude, GPT-4o, Gemini, Llama - Add unit tests (6 tests) - Add documentation at docs/providers/poe.md - Update provider index and docs navigation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c8063bdcd8
commit
8db61b8548
@ -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",
|
||||
|
||||
@ -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)
|
||||
|
||||
73
docs/providers/poe.md
Normal file
73
docs/providers/poe.md
Normal file
@ -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.
|
||||
11
extensions/poe/clawdbot.plugin.json
Normal file
11
extensions/poe/clawdbot.plugin.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"id": "poe",
|
||||
"providers": [
|
||||
"poe"
|
||||
],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
109
extensions/poe/index.test.ts
Normal file
109
extensions/poe/index.test.ts
Normal file
@ -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<string, unknown>;
|
||||
|
||||
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<Record<string, unknown>>;
|
||||
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<string, unknown>;
|
||||
const auth = provider.auth as Array<Record<string, unknown>>;
|
||||
expect(auth[0]).toBeDefined();
|
||||
|
||||
// Verify provider has expected structure
|
||||
expect(provider.id).toBe("poe");
|
||||
});
|
||||
});
|
||||
253
extensions/poe/index.ts
Normal file
253
extensions/poe/index.ts
Normal file
@ -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<boolean> {
|
||||
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;
|
||||
11
extensions/poe/package.json
Normal file
11
extensions/poe/package.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@clawdbot/poe",
|
||||
"version": "2026.1.25",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Poe provider plugin",
|
||||
"clawdbot": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user