add proxy support for llm

add proxy support for llm
This commit is contained in:
Alex Xiang 2026-01-28 16:10:39 +08:00 committed by GitHub
commit 90c5b1f4e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 132 additions and 1 deletions

62
src/agents/llm-proxy.ts Normal file
View File

@ -0,0 +1,62 @@
/**
* LLM API proxy support.
*
* Uses undici's ProxyAgent to route all fetch() calls through a configured proxy.
* This is needed because pi-ai uses globalThis.fetch internally and doesn't
* accept a custom fetch function.
*/
import { ProxyAgent, setGlobalDispatcher, getGlobalDispatcher, type Dispatcher } from "undici";
import type { ClawdbotConfig } from "../config/config.js";
let originalDispatcher: Dispatcher | null = null;
let currentProxyUrl: string | null = null;
/**
* Configure the global fetch dispatcher to use an HTTP proxy for all requests.
* Call this before making LLM API calls when models.proxy is configured.
*
* Safe to call multiple times - only reconfigures if the proxy URL changes.
*/
export function setupLlmProxy(cfg: ClawdbotConfig | undefined): void {
const proxyUrl = cfg?.models?.proxy?.trim();
// No proxy configured - restore original dispatcher if we changed it
if (!proxyUrl) {
if (originalDispatcher) {
setGlobalDispatcher(originalDispatcher);
originalDispatcher = null;
currentProxyUrl = null;
}
return;
}
// Same proxy already configured - no action needed
if (proxyUrl === currentProxyUrl) {
return;
}
// Save original dispatcher on first proxy setup
if (!originalDispatcher) {
originalDispatcher = getGlobalDispatcher();
}
// Set up new proxy agent
const proxyAgent = new ProxyAgent(proxyUrl);
setGlobalDispatcher(proxyAgent);
currentProxyUrl = proxyUrl;
}
/**
* Check if an LLM proxy is currently configured and active.
*/
export function isLlmProxyActive(): boolean {
return currentProxyUrl !== null;
}
/**
* Get the currently configured proxy URL, if any.
*/
export function getLlmProxyUrl(): string | null {
return currentProxyUrl;
}

View File

@ -24,6 +24,7 @@ import { isReasoningTagProvider } from "../../utils/provider-utils.js";
import { resolveUserPath } from "../../utils.js";
import { resolveClawdbotAgentDir } from "../agent-paths.js";
import { resolveSessionAgentIds } from "../agent-scope.js";
import { setupLlmProxy } from "../llm-proxy.js";
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../bootstrap-files.js";
import { resolveClawdbotDocsPath } from "../docs-path.js";
import type { ExecElevatedDefaults } from "../bash-tools.js";
@ -109,6 +110,9 @@ export type CompactEmbeddedPiSessionParams = {
export async function compactEmbeddedPiSessionDirect(
params: CompactEmbeddedPiSessionParams,
): Promise<EmbeddedPiCompactResult> {
// Set up LLM proxy if configured (must be done before any fetch calls)
setupLlmProxy(params.config);
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
const prevCwd = process.cwd();

View File

@ -22,6 +22,7 @@ import { isSubagentSessionKey } from "../../../routing/session-key.js";
import { resolveUserPath } from "../../../utils.js";
import { createCacheTrace } from "../../cache-trace.js";
import { createAnthropicPayloadLogger } from "../../anthropic-payload-log.js";
import { setupLlmProxy } from "../../llm-proxy.js";
import { resolveClawdbotAgentDir } from "../../agent-paths.js";
import { resolveSessionAgentIds } from "../../agent-scope.js";
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../../bootstrap-files.js";
@ -133,6 +134,9 @@ export function injectHistoryImagesIntoMessages(
export async function runEmbeddedAttempt(
params: EmbeddedRunAttemptParams,
): Promise<EmbeddedRunAttemptResult> {
// Set up LLM proxy if configured (must be done before any fetch calls)
setupLlmProxy(params.config);
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
const prevCwd = process.cwd();
const runAbortController = new AbortController();

View File

@ -13,6 +13,7 @@ import { Type } from "@sinclair/typebox";
import type { ClawdbotConfig } from "../../config/config.js";
import { resolveUserPath } from "../../utils.js";
import { setupLlmProxy } from "../llm-proxy.js";
import { loadWebMedia } from "../../web/media.js";
import { ensureAuthProfileStore, listProfilesForProvider } from "../auth-profiles.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
@ -219,6 +220,9 @@ async function runImagePrompt(params: {
model: string;
attempts: Array<{ provider: string; model: string; error: string }>;
}> {
// Set up LLM proxy if configured (must be done before any fetch calls)
setupLlmProxy(params.cfg);
const effectiveCfg: ClawdbotConfig | undefined = params.cfg
? {
...params.cfg,

View File

@ -56,4 +56,6 @@ export type ModelsConfig = {
mode?: "merge" | "replace";
providers?: Record<string, ModelProviderConfig>;
bedrockDiscovery?: BedrockDiscoveryConfig;
/** HTTP proxy URL for all LLM API calls (e.g., "http://user:pass@proxy:8080"). */
proxy?: string;
};

View File

@ -77,6 +77,7 @@ export const ModelsConfigSchema = z
mode: z.union([z.literal("merge"), z.literal("replace")]).optional(),
providers: z.record(z.string(), ModelProviderSchema).optional(),
bedrockDiscovery: BedrockDiscoverySchema,
proxy: z.string().optional(),
})
.strict()
.optional();

View File

@ -7,6 +7,7 @@
*/
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
import os from "node:os";
import type { ClawdbotConfig } from "../config/config.js";
import { loadConfig } from "../config/config.js";
@ -71,6 +72,24 @@ const FEISHU_RESTART_POLICY = {
jitter: 0.25,
};
/**
* Get primary local IPv4 address
*/
function getLocalIPv4(): string {
const nets = os.networkInterfaces();
const prefer = ["en0", "eth0", "wlan0"];
for (const name of prefer) {
const list = nets[name];
const entry = list?.find((n) => n.family === "IPv4" && !n.internal);
if (entry?.address) return entry.address;
}
for (const list of Object.values(nets)) {
const entry = list?.find((n) => n.family === "IPv4" && !n.internal);
if (entry?.address) return entry.address;
}
return "unknown";
}
/**
* Process a Feishu message event
*/
@ -458,13 +477,44 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi
// Verify credentials by getting bot info
log(`feishu: verifying bot credentials...`);
let botName = "unknown";
let botOpenId = "unknown";
try {
const botInfo = await client.getBotInfo();
log(`feishu: connected as "${botInfo.app_name}" (${botInfo.open_id})`);
botName = botInfo.app_name ?? "unknown";
botOpenId = botInfo.open_id ?? "unknown";
log(`feishu: connected as "${botName}" (${botOpenId})`);
} catch (err) {
throw new Error(`Feishu authentication failed: ${formatErrorMessage(err)}`);
}
// Send startup message if startupChatId is configured
const startupChatId = account.config.startupChatId?.trim();
if (startupChatId) {
const localIp = getLocalIPv4();
const hostname = os.hostname();
const version = process.env.CLAWDBOT_VERSION ?? process.env.npm_package_version ?? "unknown";
const timestamp = new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
const startupMessage = [
`🤖 飞书机器人启动通知`,
``,
`机器人: ${botName}`,
`Bot ID: ${botOpenId}`,
`主机名: ${hostname}`,
`本地IP: ${localIp}`,
`版本: ${version}`,
`启动时间: ${timestamp}`,
].join("\n");
try {
await client.sendTextMessage(startupChatId, startupMessage);
log(`feishu: sent startup message to chat ${startupChatId}`);
} catch (err) {
log(`feishu: failed to send startup message: ${formatErrorMessage(err)}`);
}
}
// Default to websocket mode (recommended, no public URL required)
const eventMode = opts.eventMode ?? account.config.eventMode ?? "websocket";
log(`feishu: event mode is "${eventMode}"`);

View File

@ -3,6 +3,7 @@ import { complete } from "@mariozechner/pi-ai";
import { discoverAuthStorage, discoverModels } from "@mariozechner/pi-coding-agent";
import { getApiKeyForModel, requireApiKey } from "../../agents/model-auth.js";
import { setupLlmProxy } from "../../agents/llm-proxy.js";
import { ensureClawdbotModelsJson } from "../../agents/models-config.js";
import { minimaxUnderstandImage } from "../../agents/minimax-vlm.js";
import { coerceImageAssistantText } from "../../agents/tools/image-tool.helpers.js";
@ -11,6 +12,9 @@ import type { ImageDescriptionRequest, ImageDescriptionResult } from "../types.j
export async function describeImageWithModel(
params: ImageDescriptionRequest,
): Promise<ImageDescriptionResult> {
// Set up LLM proxy if configured (must be done before any fetch calls)
setupLlmProxy(params.cfg);
await ensureClawdbotModelsJson(params.cfg, params.agentDir);
const authStorage = discoverAuthStorage(params.agentDir);
const modelRegistry = discoverModels(authStorage, params.agentDir);