add proxy support for llm
This commit is contained in:
parent
79a3a967f9
commit
c703e234cd
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 { resolveUserPath } from "../../utils.js";
|
||||||
import { resolveClawdbotAgentDir } from "../agent-paths.js";
|
import { resolveClawdbotAgentDir } from "../agent-paths.js";
|
||||||
import { resolveSessionAgentIds } from "../agent-scope.js";
|
import { resolveSessionAgentIds } from "../agent-scope.js";
|
||||||
|
import { setupLlmProxy } from "../llm-proxy.js";
|
||||||
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../bootstrap-files.js";
|
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../bootstrap-files.js";
|
||||||
import { resolveClawdbotDocsPath } from "../docs-path.js";
|
import { resolveClawdbotDocsPath } from "../docs-path.js";
|
||||||
import type { ExecElevatedDefaults } from "../bash-tools.js";
|
import type { ExecElevatedDefaults } from "../bash-tools.js";
|
||||||
@ -109,6 +110,9 @@ export type CompactEmbeddedPiSessionParams = {
|
|||||||
export async function compactEmbeddedPiSessionDirect(
|
export async function compactEmbeddedPiSessionDirect(
|
||||||
params: CompactEmbeddedPiSessionParams,
|
params: CompactEmbeddedPiSessionParams,
|
||||||
): Promise<EmbeddedPiCompactResult> {
|
): Promise<EmbeddedPiCompactResult> {
|
||||||
|
// Set up LLM proxy if configured (must be done before any fetch calls)
|
||||||
|
setupLlmProxy(params.config);
|
||||||
|
|
||||||
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
||||||
const prevCwd = process.cwd();
|
const prevCwd = process.cwd();
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import { isSubagentSessionKey } from "../../../routing/session-key.js";
|
|||||||
import { resolveUserPath } from "../../../utils.js";
|
import { resolveUserPath } from "../../../utils.js";
|
||||||
import { createCacheTrace } from "../../cache-trace.js";
|
import { createCacheTrace } from "../../cache-trace.js";
|
||||||
import { createAnthropicPayloadLogger } from "../../anthropic-payload-log.js";
|
import { createAnthropicPayloadLogger } from "../../anthropic-payload-log.js";
|
||||||
|
import { setupLlmProxy } from "../../llm-proxy.js";
|
||||||
import { resolveClawdbotAgentDir } from "../../agent-paths.js";
|
import { resolveClawdbotAgentDir } from "../../agent-paths.js";
|
||||||
import { resolveSessionAgentIds } from "../../agent-scope.js";
|
import { resolveSessionAgentIds } from "../../agent-scope.js";
|
||||||
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../../bootstrap-files.js";
|
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../../bootstrap-files.js";
|
||||||
@ -133,6 +134,9 @@ export function injectHistoryImagesIntoMessages(
|
|||||||
export async function runEmbeddedAttempt(
|
export async function runEmbeddedAttempt(
|
||||||
params: EmbeddedRunAttemptParams,
|
params: EmbeddedRunAttemptParams,
|
||||||
): Promise<EmbeddedRunAttemptResult> {
|
): Promise<EmbeddedRunAttemptResult> {
|
||||||
|
// Set up LLM proxy if configured (must be done before any fetch calls)
|
||||||
|
setupLlmProxy(params.config);
|
||||||
|
|
||||||
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
||||||
const prevCwd = process.cwd();
|
const prevCwd = process.cwd();
|
||||||
const runAbortController = new AbortController();
|
const runAbortController = new AbortController();
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { Type } from "@sinclair/typebox";
|
|||||||
|
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import { resolveUserPath } from "../../utils.js";
|
import { resolveUserPath } from "../../utils.js";
|
||||||
|
import { setupLlmProxy } from "../llm-proxy.js";
|
||||||
import { loadWebMedia } from "../../web/media.js";
|
import { loadWebMedia } from "../../web/media.js";
|
||||||
import { ensureAuthProfileStore, listProfilesForProvider } from "../auth-profiles.js";
|
import { ensureAuthProfileStore, listProfilesForProvider } from "../auth-profiles.js";
|
||||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
|
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
|
||||||
@ -219,6 +220,9 @@ async function runImagePrompt(params: {
|
|||||||
model: string;
|
model: string;
|
||||||
attempts: Array<{ provider: string; model: string; error: 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
|
const effectiveCfg: ClawdbotConfig | undefined = params.cfg
|
||||||
? {
|
? {
|
||||||
...params.cfg,
|
...params.cfg,
|
||||||
|
|||||||
@ -56,4 +56,6 @@ export type ModelsConfig = {
|
|||||||
mode?: "merge" | "replace";
|
mode?: "merge" | "replace";
|
||||||
providers?: Record<string, ModelProviderConfig>;
|
providers?: Record<string, ModelProviderConfig>;
|
||||||
bedrockDiscovery?: BedrockDiscoveryConfig;
|
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(),
|
mode: z.union([z.literal("merge"), z.literal("replace")]).optional(),
|
||||||
providers: z.record(z.string(), ModelProviderSchema).optional(),
|
providers: z.record(z.string(), ModelProviderSchema).optional(),
|
||||||
bedrockDiscovery: BedrockDiscoverySchema,
|
bedrockDiscovery: BedrockDiscoverySchema,
|
||||||
|
proxy: z.string().optional(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
||||||
|
import os from "node:os";
|
||||||
|
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
@ -71,6 +72,24 @@ const FEISHU_RESTART_POLICY = {
|
|||||||
jitter: 0.25,
|
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
|
* Process a Feishu message event
|
||||||
*/
|
*/
|
||||||
@ -458,13 +477,44 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi
|
|||||||
|
|
||||||
// Verify credentials by getting bot info
|
// Verify credentials by getting bot info
|
||||||
log(`feishu: verifying bot credentials...`);
|
log(`feishu: verifying bot credentials...`);
|
||||||
|
let botName = "unknown";
|
||||||
|
let botOpenId = "unknown";
|
||||||
try {
|
try {
|
||||||
const botInfo = await client.getBotInfo();
|
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) {
|
} catch (err) {
|
||||||
throw new Error(`Feishu authentication failed: ${formatErrorMessage(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)
|
// Default to websocket mode (recommended, no public URL required)
|
||||||
const eventMode = opts.eventMode ?? account.config.eventMode ?? "websocket";
|
const eventMode = opts.eventMode ?? account.config.eventMode ?? "websocket";
|
||||||
log(`feishu: event mode is "${eventMode}"`);
|
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 { discoverAuthStorage, discoverModels } from "@mariozechner/pi-coding-agent";
|
||||||
|
|
||||||
import { getApiKeyForModel, requireApiKey } from "../../agents/model-auth.js";
|
import { getApiKeyForModel, requireApiKey } from "../../agents/model-auth.js";
|
||||||
|
import { setupLlmProxy } from "../../agents/llm-proxy.js";
|
||||||
import { ensureClawdbotModelsJson } from "../../agents/models-config.js";
|
import { ensureClawdbotModelsJson } from "../../agents/models-config.js";
|
||||||
import { minimaxUnderstandImage } from "../../agents/minimax-vlm.js";
|
import { minimaxUnderstandImage } from "../../agents/minimax-vlm.js";
|
||||||
import { coerceImageAssistantText } from "../../agents/tools/image-tool.helpers.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(
|
export async function describeImageWithModel(
|
||||||
params: ImageDescriptionRequest,
|
params: ImageDescriptionRequest,
|
||||||
): Promise<ImageDescriptionResult> {
|
): Promise<ImageDescriptionResult> {
|
||||||
|
// Set up LLM proxy if configured (must be done before any fetch calls)
|
||||||
|
setupLlmProxy(params.cfg);
|
||||||
|
|
||||||
await ensureClawdbotModelsJson(params.cfg, params.agentDir);
|
await ensureClawdbotModelsJson(params.cfg, params.agentDir);
|
||||||
const authStorage = discoverAuthStorage(params.agentDir);
|
const authStorage = discoverAuthStorage(params.agentDir);
|
||||||
const modelRegistry = discoverModels(authStorage, params.agentDir);
|
const modelRegistry = discoverModels(authStorage, params.agentDir);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user