feat(config): add centralized API endpoint configuration
Add src/config/api-endpoints.ts with ENV variable support for all external API base URLs. This enables routing traffic through a proxy for multi-tenant deployments, usage metering, and security filtering. Supported ENV variables: - TELEGRAM_API_BASE (default: https://api.telegram.org) - DISCORD_API_BASE (default: https://discord.com/api/v10) - OPENAI_API_BASE (default: https://api.openai.com/v1) - ANTHROPIC_API_BASE (default: https://api.anthropic.com) - GOOGLE_GENERATIVE_API_BASE (default: https://generativelanguage.googleapis.com/v1beta) - GROQ_API_BASE (default: https://api.groq.com/openai/v1) - DEEPGRAM_API_BASE (default: https://api.deepgram.com/v1) - ELEVENLABS_API_BASE (default: https://api.elevenlabs.io) - OPENAI_TTS_BASE_URL (default: same as OPENAI_API_BASE) Updated files to use centralized configuration: - telegram/audit.ts, probe.ts, download.ts, bot/delivery.ts - channels/plugins/onboarding/telegram.ts - discord/api.ts, probe.ts - tts/tts.ts - memory/embeddings-openai.ts, embeddings-gemini.ts - media-understanding/providers/openai/audio.ts - media-understanding/providers/google/audio.ts, video.ts - media-understanding/providers/deepgram/audio.ts - media-understanding/providers/groq/index.ts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9a2be717b7
commit
da8c925e8e
@ -1,3 +1,4 @@
|
||||
import { TELEGRAM_API_BASE } from "../../../config/api-endpoints.js";
|
||||
import type { ClawdbotConfig } from "../../../config/config.js";
|
||||
import type { DmPolicy } from "../../../config/types.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../routing/session-key.js";
|
||||
@ -48,7 +49,7 @@ async function noteTelegramUserIdHelp(prompter: WizardPrompter): Promise<void> {
|
||||
await prompter.note(
|
||||
[
|
||||
`1) DM your bot, then read from.id in \`${formatCliCommand("clawdbot logs --follow")}\` (safest)`,
|
||||
"2) Or call https://api.telegram.org/bot<bot_token>/getUpdates and read message.from.id",
|
||||
`2) Or call ${TELEGRAM_API_BASE}/bot<bot_token>/getUpdates and read message.from.id`,
|
||||
"3) Third-party: DM @userinfobot or @getidsbot",
|
||||
`Docs: ${formatDocsLink("/telegram")}`,
|
||||
"Website: https://clawd.bot",
|
||||
@ -79,7 +80,7 @@ async function promptTelegramAllowFrom(params: {
|
||||
if (/^\d+$/.test(stripped)) return stripped;
|
||||
if (!token) return null;
|
||||
const username = stripped.startsWith("@") ? stripped : `@${stripped}`;
|
||||
const url = `https://api.telegram.org/bot${token}/getChat?chat_id=${encodeURIComponent(username)}`;
|
||||
const url = `${TELEGRAM_API_BASE}/bot${token}/getChat?chat_id=${encodeURIComponent(username)}`;
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return null;
|
||||
|
||||
104
src/config/api-endpoints.ts
Normal file
104
src/config/api-endpoints.ts
Normal file
@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Centralized API endpoint configuration.
|
||||
*
|
||||
* All external API base URLs with ENV variable overrides.
|
||||
* This enables routing all traffic through a proxy for:
|
||||
* - Multi-tenant deployments
|
||||
* - Usage metering and billing
|
||||
* - Security filtering
|
||||
* - Audit logging
|
||||
*
|
||||
* @example
|
||||
* ```bash
|
||||
* # Route all Telegram API calls through proxy
|
||||
* TELEGRAM_API_BASE=https://proxy.example.com/tg
|
||||
*
|
||||
* # Route all Discord API calls through proxy
|
||||
* DISCORD_API_BASE=https://proxy.example.com/dc
|
||||
* ```
|
||||
*/
|
||||
|
||||
const trimSlash = (url: string | undefined): string | undefined =>
|
||||
url?.trim().replace(/\/+$/, "") || undefined;
|
||||
|
||||
// =============================================================================
|
||||
// MESSAGING PLATFORMS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Telegram Bot API base URL.
|
||||
* Default: https://api.telegram.org
|
||||
*
|
||||
* Note: This is used for all outbound Telegram API calls including:
|
||||
* - sendMessage, sendVoice, sendPhoto, etc.
|
||||
* - getFile (file downloads)
|
||||
* - setWebhook, deleteWebhook
|
||||
*/
|
||||
export const TELEGRAM_API_BASE =
|
||||
trimSlash(process.env.TELEGRAM_API_BASE) ?? "https://api.telegram.org";
|
||||
|
||||
/**
|
||||
* Discord API base URL.
|
||||
* Default: https://discord.com/api/v10
|
||||
*/
|
||||
export const DISCORD_API_BASE =
|
||||
trimSlash(process.env.DISCORD_API_BASE) ?? "https://discord.com/api/v10";
|
||||
|
||||
// =============================================================================
|
||||
// LLM PROVIDERS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* OpenAI API base URL.
|
||||
* Default: https://api.openai.com/v1
|
||||
*/
|
||||
export const OPENAI_API_BASE =
|
||||
trimSlash(process.env.OPENAI_API_BASE) ?? "https://api.openai.com/v1";
|
||||
|
||||
/**
|
||||
* Anthropic API base URL.
|
||||
* Default: https://api.anthropic.com
|
||||
*/
|
||||
export const ANTHROPIC_API_BASE =
|
||||
trimSlash(process.env.ANTHROPIC_API_BASE) ?? "https://api.anthropic.com";
|
||||
|
||||
/**
|
||||
* Google Generative AI (Gemini) API base URL.
|
||||
* Default: https://generativelanguage.googleapis.com/v1beta
|
||||
*/
|
||||
export const GOOGLE_GENERATIVE_API_BASE =
|
||||
trimSlash(process.env.GOOGLE_GENERATIVE_API_BASE) ??
|
||||
"https://generativelanguage.googleapis.com/v1beta";
|
||||
|
||||
/**
|
||||
* Groq API base URL.
|
||||
* Default: https://api.groq.com/openai/v1
|
||||
*/
|
||||
export const GROQ_API_BASE =
|
||||
trimSlash(process.env.GROQ_API_BASE) ?? "https://api.groq.com/openai/v1";
|
||||
|
||||
/**
|
||||
* Deepgram API base URL.
|
||||
* Default: https://api.deepgram.com/v1
|
||||
*/
|
||||
export const DEEPGRAM_API_BASE =
|
||||
trimSlash(process.env.DEEPGRAM_API_BASE) ?? "https://api.deepgram.com/v1";
|
||||
|
||||
// =============================================================================
|
||||
// TTS PROVIDERS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* OpenAI TTS API base URL.
|
||||
* Default: Same as OPENAI_API_BASE
|
||||
*
|
||||
* Note: Supports custom OpenAI-compatible TTS endpoints (e.g., Kokoro, LocalAI).
|
||||
*/
|
||||
export const OPENAI_TTS_BASE = trimSlash(process.env.OPENAI_TTS_BASE_URL) ?? OPENAI_API_BASE;
|
||||
|
||||
/**
|
||||
* ElevenLabs API base URL.
|
||||
* Default: https://api.elevenlabs.io
|
||||
*/
|
||||
export const ELEVENLABS_API_BASE =
|
||||
trimSlash(process.env.ELEVENLABS_API_BASE) ?? "https://api.elevenlabs.io";
|
||||
@ -1,7 +1,6 @@
|
||||
import { DISCORD_API_BASE } from "../config/api-endpoints.js";
|
||||
import { resolveFetch } from "../infra/fetch.js";
|
||||
import { resolveRetryConfig, retryAsync, type RetryConfig } from "../infra/retry.js";
|
||||
|
||||
const DISCORD_API_BASE = "https://discord.com/api/v10";
|
||||
const DISCORD_API_RETRY_DEFAULTS = {
|
||||
attempts: 3,
|
||||
minDelayMs: 500,
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { DISCORD_API_BASE } from "../config/api-endpoints.js";
|
||||
import { resolveFetch } from "../infra/fetch.js";
|
||||
import { normalizeDiscordToken } from "./token.js";
|
||||
|
||||
const DISCORD_API_BASE = "https://discord.com/api/v10";
|
||||
|
||||
export type DiscordProbe = {
|
||||
ok: boolean;
|
||||
status?: number | null;
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { DEEPGRAM_API_BASE } from "../../../config/api-endpoints.js";
|
||||
import type { AudioTranscriptionRequest, AudioTranscriptionResult } from "../../types.js";
|
||||
import { fetchWithTimeout, normalizeBaseUrl, readErrorResponse } from "../shared.js";
|
||||
|
||||
export const DEFAULT_DEEPGRAM_AUDIO_BASE_URL = "https://api.deepgram.com/v1";
|
||||
export const DEFAULT_DEEPGRAM_AUDIO_BASE_URL = DEEPGRAM_API_BASE;
|
||||
export const DEFAULT_DEEPGRAM_AUDIO_MODEL = "nova-3";
|
||||
|
||||
function resolveModel(model?: string): string {
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import type { AudioTranscriptionRequest, AudioTranscriptionResult } from "../../types.js";
|
||||
import { GOOGLE_GENERATIVE_API_BASE } from "../../../config/api-endpoints.js";
|
||||
import { normalizeGoogleModelId } from "../../../agents/models-config.providers.js";
|
||||
import type { AudioTranscriptionRequest, AudioTranscriptionResult } from "../../types.js";
|
||||
import { fetchWithTimeout, normalizeBaseUrl, readErrorResponse } from "../shared.js";
|
||||
|
||||
export const DEFAULT_GOOGLE_AUDIO_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
||||
export const DEFAULT_GOOGLE_AUDIO_BASE_URL = GOOGLE_GENERATIVE_API_BASE;
|
||||
const DEFAULT_GOOGLE_AUDIO_MODEL = "gemini-3-flash-preview";
|
||||
const DEFAULT_GOOGLE_AUDIO_PROMPT = "Transcribe the audio.";
|
||||
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import type { VideoDescriptionRequest, VideoDescriptionResult } from "../../types.js";
|
||||
import { GOOGLE_GENERATIVE_API_BASE } from "../../../config/api-endpoints.js";
|
||||
import { normalizeGoogleModelId } from "../../../agents/models-config.providers.js";
|
||||
import type { VideoDescriptionRequest, VideoDescriptionResult } from "../../types.js";
|
||||
import { fetchWithTimeout, normalizeBaseUrl, readErrorResponse } from "../shared.js";
|
||||
|
||||
export const DEFAULT_GOOGLE_VIDEO_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
||||
export const DEFAULT_GOOGLE_VIDEO_BASE_URL = GOOGLE_GENERATIVE_API_BASE;
|
||||
const DEFAULT_GOOGLE_VIDEO_MODEL = "gemini-3-flash-preview";
|
||||
const DEFAULT_GOOGLE_VIDEO_PROMPT = "Describe the video.";
|
||||
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { GROQ_API_BASE } from "../../../config/api-endpoints.js";
|
||||
import type { MediaUnderstandingProvider } from "../../types.js";
|
||||
import { transcribeOpenAiCompatibleAudio } from "../openai/audio.js";
|
||||
|
||||
const DEFAULT_GROQ_AUDIO_BASE_URL = "https://api.groq.com/openai/v1";
|
||||
const DEFAULT_GROQ_AUDIO_BASE_URL = GROQ_API_BASE;
|
||||
|
||||
export const groqProvider: MediaUnderstandingProvider = {
|
||||
id: "groq",
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import path from "node:path";
|
||||
|
||||
import { OPENAI_API_BASE } from "../../../config/api-endpoints.js";
|
||||
import type { AudioTranscriptionRequest, AudioTranscriptionResult } from "../../types.js";
|
||||
import { fetchWithTimeout, normalizeBaseUrl, readErrorResponse } from "../shared.js";
|
||||
|
||||
export const DEFAULT_OPENAI_AUDIO_BASE_URL = "https://api.openai.com/v1";
|
||||
export const DEFAULT_OPENAI_AUDIO_BASE_URL = OPENAI_API_BASE;
|
||||
const DEFAULT_OPENAI_AUDIO_MODEL = "gpt-4o-mini-transcribe";
|
||||
|
||||
function resolveModel(model?: string): string {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { requireApiKey, resolveApiKeyForProvider } from "../agents/model-auth.js";
|
||||
import { GOOGLE_GENERATIVE_API_BASE } from "../config/api-endpoints.js";
|
||||
import { isTruthyEnvValue } from "../infra/env.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import type { EmbeddingProvider, EmbeddingProviderOptions } from "./embeddings.js";
|
||||
@ -10,7 +11,7 @@ export type GeminiEmbeddingClient = {
|
||||
modelPath: string;
|
||||
};
|
||||
|
||||
const DEFAULT_GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
||||
const DEFAULT_GEMINI_BASE_URL = GOOGLE_GENERATIVE_API_BASE;
|
||||
export const DEFAULT_GEMINI_EMBEDDING_MODEL = "gemini-embedding-001";
|
||||
const debugEmbeddings = isTruthyEnvValue(process.env.CLAWDBOT_DEBUG_MEMORY_EMBEDDINGS);
|
||||
const log = createSubsystemLogger("memory/embeddings");
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { requireApiKey, resolveApiKeyForProvider } from "../agents/model-auth.js";
|
||||
import { OPENAI_API_BASE } from "../config/api-endpoints.js";
|
||||
import type { EmbeddingProvider, EmbeddingProviderOptions } from "./embeddings.js";
|
||||
|
||||
export type OpenAiEmbeddingClient = {
|
||||
@ -8,7 +9,7 @@ export type OpenAiEmbeddingClient = {
|
||||
};
|
||||
|
||||
export const DEFAULT_OPENAI_EMBEDDING_MODEL = "text-embedding-3-small";
|
||||
const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
||||
const DEFAULT_OPENAI_BASE_URL = OPENAI_API_BASE;
|
||||
|
||||
export function normalizeOpenAiModel(model: string): string {
|
||||
const trimmed = model.trim();
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { TELEGRAM_API_BASE } from "../config/api-endpoints.js";
|
||||
import type { TelegramGroupConfig } from "../config/types.js";
|
||||
import { makeProxyFetch } from "./proxy.js";
|
||||
|
||||
const TELEGRAM_API_BASE = "https://api.telegram.org";
|
||||
|
||||
export type TelegramGroupMembershipAuditEntry = {
|
||||
chatId: string;
|
||||
ok: boolean;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { type Bot, GrammyError, InputFile } from "grammy";
|
||||
import { TELEGRAM_API_BASE } from "../../config/api-endpoints.js";
|
||||
import {
|
||||
markdownToTelegramChunks,
|
||||
markdownToTelegramHtml,
|
||||
@ -274,7 +275,7 @@ export async function resolveMedia(
|
||||
if (!fetchImpl) {
|
||||
throw new Error("fetch is not available; set channels.telegram.proxy in config");
|
||||
}
|
||||
const url = `https://api.telegram.org/file/bot${token}/${file.file_path}`;
|
||||
const url = `${TELEGRAM_API_BASE}/file/bot${token}/${file.file_path}`;
|
||||
const fetched = await fetchRemoteMedia({
|
||||
url,
|
||||
fetchImpl,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { TELEGRAM_API_BASE } from "../config/api-endpoints.js";
|
||||
import { detectMime } from "../media/mime.js";
|
||||
import { type SavedMedia, saveMediaBuffer } from "../media/store.js";
|
||||
|
||||
@ -10,7 +11,7 @@ export type TelegramFileInfo = {
|
||||
|
||||
export async function getTelegramFile(token: string, fileId: string): Promise<TelegramFileInfo> {
|
||||
const res = await fetch(
|
||||
`https://api.telegram.org/bot${token}/getFile?file_id=${encodeURIComponent(fileId)}`,
|
||||
`${TELEGRAM_API_BASE}/bot${token}/getFile?file_id=${encodeURIComponent(fileId)}`,
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw new Error(`getFile failed: ${res.status} ${res.statusText}`);
|
||||
@ -28,7 +29,7 @@ export async function downloadTelegramFile(
|
||||
maxBytes?: number,
|
||||
): Promise<SavedMedia> {
|
||||
if (!info.file_path) throw new Error("file_path missing");
|
||||
const url = `https://api.telegram.org/file/bot${token}/${info.file_path}`;
|
||||
const url = `${TELEGRAM_API_BASE}/file/bot${token}/${info.file_path}`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok || !res.body) {
|
||||
throw new Error(`Failed to download telegram file: HTTP ${res.status}`);
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { TELEGRAM_API_BASE } from "../config/api-endpoints.js";
|
||||
import { makeProxyFetch } from "./proxy.js";
|
||||
|
||||
const TELEGRAM_API_BASE = "https://api.telegram.org";
|
||||
|
||||
export type TelegramProbe = {
|
||||
ok: boolean;
|
||||
status?: number | null;
|
||||
|
||||
@ -15,6 +15,7 @@ import { completeSimple, type TextContent } from "@mariozechner/pi-ai";
|
||||
import { EdgeTTS } from "node-edge-tts";
|
||||
|
||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||
import { ELEVENLABS_API_BASE, OPENAI_TTS_BASE } from "../config/api-endpoints.js";
|
||||
import { normalizeChannelId } from "../channels/plugins/index.js";
|
||||
import type { ChannelId } from "../channels/plugins/types.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
@ -43,7 +44,7 @@ const DEFAULT_TTS_SUMMARIZE = true;
|
||||
const DEFAULT_MAX_TEXT_LENGTH = 4096;
|
||||
const TEMP_FILE_CLEANUP_DELAY_MS = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
const DEFAULT_ELEVENLABS_BASE_URL = "https://api.elevenlabs.io";
|
||||
const DEFAULT_ELEVENLABS_BASE_URL = ELEVENLABS_API_BASE;
|
||||
const DEFAULT_ELEVENLABS_VOICE_ID = "pMsXgVXv3BLzUgSXRplE";
|
||||
const DEFAULT_ELEVENLABS_MODEL_ID = "eleven_multilingual_v2";
|
||||
const DEFAULT_OPENAI_MODEL = "gpt-4o-mini-tts";
|
||||
@ -758,9 +759,7 @@ export const OPENAI_TTS_MODELS = ["gpt-4o-mini-tts", "tts-1", "tts-1-hd"] as con
|
||||
* When set, model/voice validation is relaxed to allow non-OpenAI models.
|
||||
* Example: OPENAI_TTS_BASE_URL=http://localhost:8880/v1
|
||||
*/
|
||||
const OPENAI_TTS_BASE_URL = (
|
||||
process.env.OPENAI_TTS_BASE_URL?.trim() || "https://api.openai.com/v1"
|
||||
).replace(/\/+$/, "");
|
||||
const OPENAI_TTS_BASE_URL = OPENAI_TTS_BASE;
|
||||
const isCustomOpenAIEndpoint = OPENAI_TTS_BASE_URL !== "https://api.openai.com/v1";
|
||||
export const OPENAI_TTS_VOICES = [
|
||||
"alloy",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user