From dad1665b0e146875468682d2148c4cb5484b148a Mon Sep 17 00:00:00 2001 From: Bai-shisheng Date: Thu, 29 Jan 2026 20:28:26 +0800 Subject: [PATCH] feat: add POE API provider support for onboarding - Add POE as a new API provider option in auth choice - Implement POE API key configuration in onboard flow - Add POE credentials handling and validation - Support POE in non-interactive onboarding mode Co-Authored-By: Claude Opus 4.5 --- scripts/bundle-a2ui.sh | 3 + src/cli/program/register.onboard.ts | 4 +- src/commands/auth-choice-options.ts | 14 +++- .../auth-choice.apply.api-providers.ts | 72 +++++++++++++++++++ src/commands/onboard-auth.config-core.ts | 50 +++++++++++++ src/commands/onboard-auth.credentials.ts | 15 ++++ src/commands/onboard-auth.ts | 4 ++ .../local/auth-choice.ts | 21 ++++++ src/commands/onboard-types.ts | 2 + 9 files changed, 183 insertions(+), 2 deletions(-) diff --git a/scripts/bundle-a2ui.sh b/scripts/bundle-a2ui.sh index e04648090..ffaa1fdaa 100644 --- a/scripts/bundle-a2ui.sh +++ b/scripts/bundle-a2ui.sh @@ -1,6 +1,9 @@ #!/usr/bin/env bash set -euo pipefail +# Ensure NODE_PATH is defined (required for strict mode with -u flag) +export NODE_PATH="${NODE_PATH:-}" + on_error() { echo "A2UI bundling failed. Re-run with: pnpm canvas:a2ui:bundle" >&2 echo "If this persists, verify pnpm deps and try again." >&2 diff --git a/src/cli/program/register.onboard.ts b/src/cli/program/register.onboard.ts index 8f31635f0..2871ae797 100644 --- a/src/cli/program/register.onboard.ts +++ b/src/cli/program/register.onboard.ts @@ -52,7 +52,7 @@ export function registerOnboardCommand(program: Command) { .option("--mode ", "Wizard mode: local|remote") .option( "--auth-choice ", - "Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip", + "Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|poe-api-key|skip", ) .option( "--token-provider ", @@ -76,6 +76,7 @@ export function registerOnboardCommand(program: Command) { .option("--synthetic-api-key ", "Synthetic API key") .option("--venice-api-key ", "Venice API key") .option("--opencode-zen-api-key ", "OpenCode Zen API key") + .option("--poe-api-key ", "Poe API key") .option("--gateway-port ", "Gateway port") .option("--gateway-bind ", "Gateway bind: loopback|tailnet|lan|auto|custom") .option("--gateway-auth ", "Gateway auth: token|password") @@ -126,6 +127,7 @@ export function registerOnboardCommand(program: Command) { syntheticApiKey: opts.syntheticApiKey as string | undefined, veniceApiKey: opts.veniceApiKey as string | undefined, opencodeZenApiKey: opts.opencodeZenApiKey as string | undefined, + poeApiKey: opts.poeApiKey as string | undefined, gatewayPort: typeof gatewayPort === "number" && Number.isFinite(gatewayPort) ? gatewayPort diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 6b49ff17b..3ef6f6b25 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -20,7 +20,8 @@ export type AuthChoiceGroupId = | "minimax" | "synthetic" | "venice" - | "qwen"; + | "qwen" + | "poe"; export type AuthChoiceGroup = { value: AuthChoiceGroupId; @@ -113,6 +114,12 @@ const AUTH_CHOICE_GROUP_DEFS: { hint: "API key", choices: ["opencode-zen"], }, + { + value: "poe", + label: "Poe", + hint: "OpenAI-compatible API (80+ models)", + choices: ["poe-api-key"], + }, ]; export function buildAuthChoiceOptions(params: { @@ -183,6 +190,11 @@ export function buildAuthChoiceOptions(params: { label: "MiniMax M2.1 Lightning", hint: "Faster, higher output cost", }); + options.push({ + value: "poe-api-key", + label: "Poe API key", + hint: "OpenAI-compatible API with 80+ models (GPT-5, Claude 4.5, Gemini 3, etc.)", + }); if (params.includeSkip) { options.push({ value: "skip", label: "Skip for now" }); } diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index 8be02008b..74043c794 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, + applyPoeConfig, + applyPoeProviderConfig, applySyntheticConfig, applySyntheticProviderConfig, applyVeniceConfig, @@ -31,6 +33,7 @@ import { KIMI_CODE_MODEL_REF, MOONSHOT_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, + POE_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, + setPoeApiKey, setSyntheticApiKey, setVeniceApiKey, setVercelAiGatewayApiKey, @@ -85,6 +89,8 @@ export async function applyAuthChoiceApiProviders( authChoice = "venice-api-key"; } else if (params.opts.tokenProvider === "opencode") { authChoice = "opencode-zen"; + } else if (params.opts.tokenProvider === "poe") { + authChoice = "poe-api-key"; } } @@ -579,5 +585,71 @@ export async function applyAuthChoiceApiProviders( return { config: nextConfig, agentModelOverride }; } + if (authChoice === "poe-api-key") { + let hasCredential = false; + + if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "poe") { + await setPoeApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + hasCredential = true; + } + + if (!hasCredential) { + await params.prompter.note( + [ + "Poe provides OpenAI-compatible API access to 80+ models:", + "• OpenAI: GPT-5, GPT-4, o3, o1", + "• Anthropic: Claude Opus 4.5, Sonnet 4.5, Haiku 4.5", + "• Google: Gemini 3 Pro, Gemini 2.5 Flash", + "• Meta: Llama 4, Llama 3.3", + "• DeepSeek: DeepSeek R1, DeepSeek V3", + "• XAI: Grok 4", + "", + "Get your API key at: https://poe.com/api_key", + "Requires an active Poe subscription.", + ].join("\n"), + "Poe API", + ); + } + + const envKey = resolveEnvApiKey("poe"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing POE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setPoeApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } + } + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter Poe API key", + validate: validateApiKeyInput, + }); + await setPoeApiKey(normalizeApiKeyInput(String(key)), params.agentDir); + } + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "poe:default", + provider: "poe", + mode: "api_key", + }); + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: POE_DEFAULT_MODEL_REF, + applyDefaultConfig: applyPoeConfig, + applyProviderConfig: applyPoeProviderConfig, + noteDefault: POE_DEFAULT_MODEL_REF, + noteAgentModel, + prompter: params.prompter, + }); + nextConfig = applied.config; + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; + } + return { config: nextConfig, agentModelOverride }; + } + return null; } diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index 921ee01d1..f2a62cabe 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -13,6 +13,7 @@ import { import type { MoltbotConfig } from "../config/config.js"; import { OPENROUTER_DEFAULT_MODEL_REF, + POE_DEFAULT_MODEL_REF, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, ZAI_DEFAULT_MODEL_REF, } from "./onboard-auth.credentials.js"; @@ -411,6 +412,55 @@ export function applyVeniceConfig(cfg: MoltbotConfig): MoltbotConfig { }; } +/** + * Apply Poe provider configuration without changing the default model. + * Registers Poe provider, but preserves existing model selection. + */ +export function applyPoeProviderConfig(cfg: MoltbotConfig): MoltbotConfig { + const models = { ...cfg.agents?.defaults?.models }; + models[POE_DEFAULT_MODEL_REF] = { + ...models[POE_DEFAULT_MODEL_REF], + alias: models[POE_DEFAULT_MODEL_REF]?.alias ?? "GPT-5.2", + }; + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + models, + }, + }, + }; +} + +/** + * Apply Poe provider configuration AND set Poe as the default model. + * Use this when Poe is the primary provider choice during onboarding. + */ +export function applyPoeConfig(cfg: MoltbotConfig): MoltbotConfig { + const next = applyPoeProviderConfig(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: POE_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..a6e72562a 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -164,3 +164,18 @@ export async function setOpencodeZenApiKey(key: string, agentDir?: string) { agentDir: resolveAuthAgentDir(agentDir), }); } + +export const POE_DEFAULT_MODEL_REF = "poe/gpt-5.2"; + +export async function setPoeApiKey(key: string, agentDir?: string) { + // Write to resolved agent dir so gateway finds credentials on startup. + upsertAuthProfile({ + profileId: "poe:default", + credential: { + type: "api_key", + provider: "poe", + key, + }, + agentDir: resolveAuthAgentDir(agentDir), + }); +} diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index b122d89cf..ff66a1f9c 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -11,6 +11,8 @@ export { applyMoonshotProviderConfig, applyOpenrouterConfig, applyOpenrouterProviderConfig, + applyPoeConfig, + applyPoeProviderConfig, applySyntheticConfig, applySyntheticProviderConfig, applyVeniceConfig, @@ -34,6 +36,7 @@ export { } from "./onboard-auth.config-opencode.js"; export { OPENROUTER_DEFAULT_MODEL_REF, + POE_DEFAULT_MODEL_REF, setAnthropicApiKey, setGeminiApiKey, setKimiCodeApiKey, @@ -41,6 +44,7 @@ export { setMoonshotApiKey, setOpencodeZenApiKey, setOpenrouterApiKey, + setPoeApiKey, setSyntheticApiKey, setVeniceApiKey, setVercelAiGatewayApiKey, diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index 7d952730c..945c6b962 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -14,6 +14,7 @@ import { applyMoonshotConfig, applyOpencodeZenConfig, applyOpenrouterConfig, + applyPoeConfig, applySyntheticConfig, applyVeniceConfig, applyVercelAiGatewayConfig, @@ -25,6 +26,7 @@ import { setMoonshotApiKey, setOpencodeZenApiKey, setOpenrouterApiKey, + setPoeApiKey, setSyntheticApiKey, setVeniceApiKey, setVercelAiGatewayApiKey, @@ -355,6 +357,25 @@ export async function applyNonInteractiveAuthChoice(params: { return applyOpencodeZenConfig(nextConfig); } + if (authChoice === "poe-api-key") { + const resolved = await resolveNonInteractiveApiKey({ + provider: "poe", + cfg: baseConfig, + flagValue: opts.poeApiKey, + flagName: "--poe-api-key", + envVar: "POE_API_KEY", + runtime, + }); + if (!resolved) return null; + if (resolved.source !== "profile") await setPoeApiKey(resolved.key); + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "poe:default", + provider: "poe", + mode: "api_key", + }); + return applyPoeConfig(nextConfig); + } + if ( authChoice === "oauth" || authChoice === "chutes" || diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index aa1d9afe0..3253ed1a7 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -31,6 +31,7 @@ export type AuthChoice = | "github-copilot" | "copilot-proxy" | "qwen-portal" + | "poe-api-key" | "skip"; export type GatewayAuthChoice = "token" | "password"; export type ResetScope = "config" | "config+creds+sessions" | "full"; @@ -71,6 +72,7 @@ export type OnboardOptions = { syntheticApiKey?: string; veniceApiKey?: string; opencodeZenApiKey?: string; + poeApiKey?: string; gatewayPort?: number; gatewayBind?: GatewayBind; gatewayAuth?: GatewayAuthChoice;