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
|
||||
TELEGRAM_BOT_TOKEN=123456:ABC-DEF... # From @BotFather
|
||||
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
|
||||
|
||||
105
secure/agent.ts
105
secure/agent.ts
@ -39,11 +39,12 @@ export type AgentResponse = {
|
||||
export type AgentCore = {
|
||||
chat: (messages: Message[], systemPrompt?: 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_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.
|
||||
|
||||
@ -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 {
|
||||
if (config.ai.provider === "anthropic") {
|
||||
return createAnthropicAgent(config, audit);
|
||||
}
|
||||
if (config.ai.provider === "openrouter") {
|
||||
return createOpenRouterAgent(config, audit);
|
||||
}
|
||||
return createOpenAIAgent(config, audit);
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ export type SecureConfig = {
|
||||
|
||||
// AI Provider
|
||||
ai: {
|
||||
provider: "anthropic" | "openai";
|
||||
provider: "anthropic" | "openai" | "openrouter";
|
||||
apiKey: string;
|
||||
model?: string;
|
||||
};
|
||||
@ -95,9 +95,10 @@ function parseAllowedUsers(value: string): number[] {
|
||||
.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 openaiKey = process.env.OPENAI_API_KEY;
|
||||
const openrouterKey = process.env.OPENROUTER_API_KEY;
|
||||
|
||||
if (anthropicKey) {
|
||||
return { provider: "anthropic", apiKey: anthropicKey };
|
||||
@ -105,8 +106,11 @@ function detectAiProvider(): { provider: "anthropic" | "openai"; apiKey: string
|
||||
if (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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user