This commit is contained in:
Gareth Paul Jones (GPJ) 2026-01-30 10:01:49 -06:00 committed by GitHub
commit 8da3138066
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 465 additions and 0 deletions

View File

@ -109,6 +109,10 @@
"source": "/openrouter/",
"destination": "/providers/openrouter"
},
{
"source": "/poe",
"destination": "/providers/poe"
},
{
"source": "/opencode",
"destination": "/providers/opencode"
@ -1029,6 +1033,7 @@
"providers/minimax",
"providers/vercel-ai-gateway",
"providers/openrouter",
"providers/poe",
"providers/synthetic",
"providers/opencode",
"providers/glm",

View File

@ -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
View 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.

View 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
View File

@ -0,0 +1,253 @@
import { emptyPluginConfigSchema } from "openclaw/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;

View File

@ -0,0 +1,11 @@
{
"id": "poe",
"providers": [
"poe"
],
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}

View File

@ -0,0 +1,11 @@
{
"name": "@openclaw/poe",
"version": "2026.1.29",
"type": "module",
"description": "OpenClaw Poe provider plugin",
"openclaw": {
"extensions": [
"./index.ts"
]
}
}

2
pnpm-lock.yaml generated
View File

@ -409,6 +409,8 @@ importers:
extensions/open-prose: {}
extensions/poe: {}
extensions/signal: {}
extensions/slack: {}