feat: add OpenRouter support + Railway auto-wire template
- Add OpenRouter as third AI provider option (100+ models) - Create railway-template.json with auto-wired PostgreSQL + Redis - Template auto-references DATABASE_URL and REDIS_URL from services - Default model for OpenRouter: anthropic/claude-3.5-sonnet - Update README with OpenRouter configuration https://claude.ai/code/session_015VqJ7gN4vaxtYfYc92UjLs
This commit is contained in:
parent
b5d78db832
commit
6a0c49e5c7
70
railway-template.json
Normal file
70
railway-template.json
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://railway.app/railway.schema.json",
|
||||||
|
"name": "AssureBot",
|
||||||
|
"description": "Lean, secure, self-hosted AI assistant with Telegram, document analysis, and scheduled tasks",
|
||||||
|
"icon": "https://raw.githubusercontent.com/TNovs1/moltbot/main/secure/icon.png",
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"name": "assurebot",
|
||||||
|
"build": {
|
||||||
|
"builder": "DOCKERFILE",
|
||||||
|
"dockerfilePath": "secure/Dockerfile"
|
||||||
|
},
|
||||||
|
"deploy": {
|
||||||
|
"startCommand": "node dist/index.js",
|
||||||
|
"healthcheckPath": "/health",
|
||||||
|
"healthcheckTimeout": 30,
|
||||||
|
"restartPolicyType": "ON_FAILURE",
|
||||||
|
"restartPolicyMaxRetries": 3
|
||||||
|
},
|
||||||
|
"variables": {
|
||||||
|
"DATABASE_URL": {
|
||||||
|
"reference": "postgres.DATABASE_URL"
|
||||||
|
},
|
||||||
|
"REDIS_URL": {
|
||||||
|
"reference": "redis.REDIS_URL"
|
||||||
|
},
|
||||||
|
"TELEGRAM_BOT_TOKEN": {
|
||||||
|
"description": "Telegram bot token from @BotFather",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"ALLOWED_USERS": {
|
||||||
|
"description": "Comma-separated Telegram user IDs (e.g., 123456789,987654321)",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"ANTHROPIC_API_KEY": {
|
||||||
|
"description": "Anthropic API key (or use OPENAI_API_KEY or OPENROUTER_API_KEY)",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"OPENAI_API_KEY": {
|
||||||
|
"description": "OpenAI API key (or use ANTHROPIC_API_KEY or OPENROUTER_API_KEY)",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"OPENROUTER_API_KEY": {
|
||||||
|
"description": "OpenRouter API key (or use ANTHROPIC_API_KEY or OPENAI_API_KEY)",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"AI_MODEL": {
|
||||||
|
"description": "Model to use (e.g., claude-3-5-sonnet-20241022, gpt-4o, anthropic/claude-3.5-sonnet)",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WEBHOOK_SECRET": {
|
||||||
|
"description": "Secret for authenticating webhooks (auto-generated if empty)",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"SANDBOX_ENABLED": {
|
||||||
|
"description": "Enable Docker sandbox for code execution",
|
||||||
|
"default": "false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "postgres",
|
||||||
|
"plugin": "postgresql"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "redis",
|
||||||
|
"plugin": "redis"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -70,7 +70,14 @@ Your AI agent that runs on your infrastructure, answers only to you, and you can
|
|||||||
```bash
|
```bash
|
||||||
TELEGRAM_BOT_TOKEN=123456:ABC-DEF... # From @BotFather
|
TELEGRAM_BOT_TOKEN=123456:ABC-DEF... # From @BotFather
|
||||||
ALLOWED_USERS=123456789,987654321 # Telegram user IDs
|
ALLOWED_USERS=123456789,987654321 # Telegram user IDs
|
||||||
ANTHROPIC_API_KEY=sk-ant-... # Or OPENAI_API_KEY
|
|
||||||
|
# AI Provider (one required)
|
||||||
|
ANTHROPIC_API_KEY=sk-ant-... # Claude direct
|
||||||
|
# or
|
||||||
|
OPENAI_API_KEY=sk-... # OpenAI direct
|
||||||
|
# or
|
||||||
|
OPENROUTER_API_KEY=sk-or-... # OpenRouter (100+ models)
|
||||||
|
AI_MODEL=anthropic/claude-3.5-sonnet # Optional: override default model
|
||||||
```
|
```
|
||||||
|
|
||||||
### Optional
|
### Optional
|
||||||
|
|||||||
105
secure/agent.ts
105
secure/agent.ts
@ -39,11 +39,12 @@ export type AgentResponse = {
|
|||||||
export type AgentCore = {
|
export type AgentCore = {
|
||||||
chat: (messages: Message[], systemPrompt?: string) => Promise<AgentResponse>;
|
chat: (messages: Message[], systemPrompt?: string) => Promise<AgentResponse>;
|
||||||
analyzeImage: (imageData: string, mediaType: ImageContent["mediaType"], prompt?: string) => Promise<AgentResponse>;
|
analyzeImage: (imageData: string, mediaType: ImageContent["mediaType"], prompt?: string) => Promise<AgentResponse>;
|
||||||
provider: "anthropic" | "openai";
|
provider: "anthropic" | "openai" | "openrouter";
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-20250514";
|
const DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-20250514";
|
||||||
const DEFAULT_OPENAI_MODEL = "gpt-4o";
|
const DEFAULT_OPENAI_MODEL = "gpt-4o";
|
||||||
|
const DEFAULT_OPENROUTER_MODEL = "anthropic/claude-3.5-sonnet";
|
||||||
|
|
||||||
const DEFAULT_SYSTEM_PROMPT = `You are a helpful AI assistant running as a secure, self-hosted bot.
|
const DEFAULT_SYSTEM_PROMPT = `You are a helpful AI assistant running as a secure, self-hosted bot.
|
||||||
|
|
||||||
@ -234,10 +235,112 @@ function createOpenAIAgent(config: SecureConfig, audit: AuditLogger): AgentCore
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createOpenRouterAgent(config: SecureConfig, audit: AuditLogger): AgentCore {
|
||||||
|
// OpenRouter uses OpenAI-compatible API
|
||||||
|
const client = new OpenAI({
|
||||||
|
apiKey: config.ai.apiKey,
|
||||||
|
baseURL: "https://openrouter.ai/api/v1",
|
||||||
|
defaultHeaders: {
|
||||||
|
"HTTP-Referer": "https://github.com/TNovs1/moltbot",
|
||||||
|
"X-Title": "AssureBot",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const model = config.ai.model || DEFAULT_OPENROUTER_MODEL;
|
||||||
|
|
||||||
|
type OpenAIContent = OpenAI.ChatCompletionContentPart[];
|
||||||
|
|
||||||
|
function convertContent(content: MessageContent): string | OpenAIContent {
|
||||||
|
if (typeof content === "string") {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
return content.map((part) => {
|
||||||
|
if (part.type === "text") {
|
||||||
|
return { type: "text" as const, text: part.text };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: "image_url" as const,
|
||||||
|
image_url: {
|
||||||
|
url: `data:${part.mediaType};base64,${part.data}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
provider: "openrouter",
|
||||||
|
|
||||||
|
async chat(messages: Message[], systemPrompt?: string): Promise<AgentResponse> {
|
||||||
|
try {
|
||||||
|
const openaiMessages: OpenAI.ChatCompletionMessageParam[] = [
|
||||||
|
{ role: "system", content: systemPrompt || DEFAULT_SYSTEM_PROMPT },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const m of messages) {
|
||||||
|
if (m.role === "user") {
|
||||||
|
openaiMessages.push({
|
||||||
|
role: "user",
|
||||||
|
content: convertContent(m.content),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
openaiMessages.push({
|
||||||
|
role: "assistant",
|
||||||
|
content: typeof m.content === "string" ? m.content : "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await client.chat.completions.create({
|
||||||
|
model,
|
||||||
|
max_tokens: 4096,
|
||||||
|
messages: openaiMessages,
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = response.choices[0]?.message?.content || "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
text,
|
||||||
|
usage: response.usage
|
||||||
|
? {
|
||||||
|
inputTokens: response.usage.prompt_tokens,
|
||||||
|
outputTokens: response.usage.completion_tokens,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
audit.error({
|
||||||
|
error: `OpenRouter API error: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async analyzeImage(
|
||||||
|
imageData: string,
|
||||||
|
mediaType: ImageContent["mediaType"],
|
||||||
|
prompt = "What's in this image? Describe it in detail."
|
||||||
|
): Promise<AgentResponse> {
|
||||||
|
const messages: Message[] = [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{ type: "image", data: imageData, mediaType },
|
||||||
|
{ type: "text", text: prompt },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return this.chat(messages);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function createAgent(config: SecureConfig, audit: AuditLogger): AgentCore {
|
export function createAgent(config: SecureConfig, audit: AuditLogger): AgentCore {
|
||||||
if (config.ai.provider === "anthropic") {
|
if (config.ai.provider === "anthropic") {
|
||||||
return createAnthropicAgent(config, audit);
|
return createAnthropicAgent(config, audit);
|
||||||
}
|
}
|
||||||
|
if (config.ai.provider === "openrouter") {
|
||||||
|
return createOpenRouterAgent(config, audit);
|
||||||
|
}
|
||||||
return createOpenAIAgent(config, audit);
|
return createOpenAIAgent(config, audit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export type SecureConfig = {
|
|||||||
|
|
||||||
// AI Provider
|
// AI Provider
|
||||||
ai: {
|
ai: {
|
||||||
provider: "anthropic" | "openai";
|
provider: "anthropic" | "openai" | "openrouter";
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
};
|
};
|
||||||
@ -95,9 +95,10 @@ function parseAllowedUsers(value: string): number[] {
|
|||||||
.filter((n) => Number.isFinite(n) && n > 0);
|
.filter((n) => Number.isFinite(n) && n > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectAiProvider(): { provider: "anthropic" | "openai"; apiKey: string } {
|
function detectAiProvider(): { provider: "anthropic" | "openai" | "openrouter"; apiKey: string } {
|
||||||
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
||||||
const openaiKey = process.env.OPENAI_API_KEY;
|
const openaiKey = process.env.OPENAI_API_KEY;
|
||||||
|
const openrouterKey = process.env.OPENROUTER_API_KEY;
|
||||||
|
|
||||||
if (anthropicKey) {
|
if (anthropicKey) {
|
||||||
return { provider: "anthropic", apiKey: anthropicKey };
|
return { provider: "anthropic", apiKey: anthropicKey };
|
||||||
@ -105,8 +106,11 @@ function detectAiProvider(): { provider: "anthropic" | "openai"; apiKey: string
|
|||||||
if (openaiKey) {
|
if (openaiKey) {
|
||||||
return { provider: "openai", apiKey: openaiKey };
|
return { provider: "openai", apiKey: openaiKey };
|
||||||
}
|
}
|
||||||
|
if (openrouterKey) {
|
||||||
|
return { provider: "openrouter", apiKey: openrouterKey };
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error("Missing AI provider key. Set ANTHROPIC_API_KEY or OPENAI_API_KEY");
|
throw new Error("Missing AI provider key. Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or OPENROUTER_API_KEY");
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateSecureToken(): string {
|
function generateSecureToken(): string {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user