add proxy support for llm
add proxy support for llm
This commit is contained in:
commit
90c5b1f4e5
62
src/agents/llm-proxy.ts
Normal file
62
src/agents/llm-proxy.ts
Normal 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;
|
||||
}
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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}"`);
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user