diff --git a/README.md b/README.md index 8cfde70..3a7e832 100644 --- a/README.md +++ b/README.md @@ -614,6 +614,7 @@ curl -X POST http://localhost:21100/api/v1/ingest \ | **DashScope** | qwen-plus, qwen-turbo | 通义千问, OpenAI-compatible | | **Ollama** | qwen2.5, llama3.2 | Fully local, zero cost | | **OpenRouter** | Any of 100+ models | Unified gateway | +| **Requesty** | Any of 300+ models | OpenAI-compatible gateway | Each extraction/lifecycle LLM can be configured with a primary provider, an optional fallback provider, retry settings, and per-provider timeout. diff --git a/packages/dashboard/src/pages/Settings/types.ts b/packages/dashboard/src/pages/Settings/types.ts index 3460790..f0e6335 100644 --- a/packages/dashboard/src/pages/Settings/types.ts +++ b/packages/dashboard/src/pages/Settings/types.ts @@ -67,6 +67,21 @@ export const LLM_PROVIDERS: Record = { ], envKey: 'OPENROUTER_API_KEY', }, + requesty: { + label: 'Requesty', + defaultBaseUrl: 'https://router.requesty.ai/v1', + models: [ + 'openai/gpt-4o-mini', + 'openai/gpt-5.2', + 'anthropic/claude-haiku-4-5', + 'anthropic/claude-sonnet-4-6', + 'google/gemini-2.5-flash', + 'google/gemini-2.5-pro', + 'deepseek/deepseek-chat', + 'meta-llama/llama-4-maverick', + ], + envKey: 'REQUESTY_API_KEY', + }, ollama: { label: 'Ollama (Local)', defaultBaseUrl: 'http://localhost:11434', diff --git a/packages/server/src/llm/cascade.ts b/packages/server/src/llm/cascade.ts index 70e43bf..0c8cf59 100644 --- a/packages/server/src/llm/cascade.ts +++ b/packages/server/src/llm/cascade.ts @@ -4,6 +4,7 @@ import { AnthropicLLMProvider } from './anthropic.js'; import { OllamaLLMProvider } from './ollama.js'; import { GoogleLLMProvider } from './google.js'; import { OpenRouterLLMProvider } from './openrouter.js'; +import { RequestyLLMProvider } from './requesty.js'; import { DeepSeekLLMProvider } from './deepseek.js'; import { normalizeLLMRetryConfig } from './config-utils.js'; import { createLogger } from '../utils/logger.js'; @@ -144,6 +145,8 @@ export function createLLMProvider(config: LLMProviderConfig): LLMProvider { }); case 'openrouter': return new OpenRouterLLMProvider(config); + case 'requesty': + return new RequestyLLMProvider(config); case 'ollama': return new OllamaLLMProvider(config); case 'none': diff --git a/packages/server/src/llm/requesty.ts b/packages/server/src/llm/requesty.ts new file mode 100644 index 0000000..885ed96 --- /dev/null +++ b/packages/server/src/llm/requesty.ts @@ -0,0 +1,58 @@ +import type { LLMProvider, LLMCompletionOpts } from './interface.js'; +import { createLogger } from '../utils/logger.js'; + +const log = createLogger('llm-requesty'); + +/** + * Requesty LLM Provider — routes to any model via Requesty's unified API. + * Uses OpenAI-compatible format. + */ +export class RequestyLLMProvider implements LLMProvider { + readonly name = 'requesty'; + private apiKey: string; + private model: string; + private baseUrl: string; + private timeoutMs: number; + + constructor(opts: { apiKey?: string; model?: string; baseUrl?: string; timeoutMs?: number }) { + this.apiKey = opts.apiKey || process.env.REQUESTY_API_KEY || ''; + this.model = opts.model || 'openai/gpt-4o-mini'; + this.baseUrl = (opts.baseUrl || 'https://router.requesty.ai/v1').replace(/\/+$/, ''); + this.timeoutMs = opts.timeoutMs ?? 30000; + } + + async complete(prompt: string, opts?: LLMCompletionOpts): Promise { + if (!this.apiKey) throw new Error('Requesty API key not configured'); + + const messages: any[] = []; + if (opts?.systemPrompt) { + messages.push({ role: 'system', content: opts.systemPrompt }); + } + messages.push({ role: 'user', content: prompt }); + + const res = await fetch(`${this.baseUrl}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.apiKey}`, + 'HTTP-Referer': 'https://github.com/rikouu/cortex', + 'X-Title': 'Cortex Memory Service', + }, + body: JSON.stringify({ + model: this.model, + messages, + max_tokens: opts?.maxTokens || 500, + temperature: opts?.temperature ?? 0.3, + }), + signal: AbortSignal.timeout(this.timeoutMs), + }); + + if (!res.ok) { + const body = await res.text(); + throw new Error(`Requesty API error ${res.status}: ${body}`); + } + + const data = (await res.json()) as any; + return data.choices?.[0]?.message?.content || ''; + } +} diff --git a/packages/server/src/utils/config.ts b/packages/server/src/utils/config.ts index f8185f1..36ce98e 100644 --- a/packages/server/src/utils/config.ts +++ b/packages/server/src/utils/config.ts @@ -9,7 +9,7 @@ const LLMRetrySchema = z.object({ }); const LLMProviderSchema = z.object({ - provider: z.enum(['openai', 'anthropic', 'google', 'gemini', 'deepseek', 'dashscope', 'openrouter', 'ollama', 'none']), + provider: z.enum(['openai', 'anthropic', 'google', 'gemini', 'deepseek', 'dashscope', 'openrouter', 'requesty', 'ollama', 'none']), model: z.string().optional(), apiKey: z.string().optional(), baseUrl: z.string().optional(),