From e0e408d1eb9750ebc53971d5679e6ff0cacbb194 Mon Sep 17 00:00:00 2001 From: MrSphay Date: Fri, 3 Jul 2026 23:06:33 +0200 Subject: [PATCH] feat: add LiteLLM provider and Code-Inc image target --- .codex/project.md | 2 +- .env.example | 5 ++- .gitea/workflows/build.yml | 4 +- AGENTS.md | 4 +- README.md | 34 ++++++++++------- crucix.config.mjs | 4 +- docker-compose.yml | 2 +- docs/agent-handoff.md | 53 ++++++++++++++------------ docs/release-checklist.md | 2 +- lib/llm/index.mjs | 5 +++ lib/llm/litellm.mjs | 32 ++++++++++++++++ package.json | 2 +- test/llm-litellm.test.mjs | 76 ++++++++++++++++++++++++++++++++++++++ 13 files changed, 175 insertions(+), 50 deletions(-) create mode 100644 lib/llm/litellm.mjs create mode 100644 test/llm-litellm.test.mjs diff --git a/.codex/project.md b/.codex/project.md index d14d465..b8709ab 100644 --- a/.codex/project.md +++ b/.codex/project.md @@ -18,7 +18,7 @@ Production-ready Crucix fork for Docker, Dockge, Pangolin, local OSINT sweeps, s - `npm run test:unit` - `npm test` - `docker compose config` -- `docker build -t git.wilkensxl.de/mrsphay/intelligence-terminal:latest .` +- `docker build -t git.wilkensxl.de/code-inc/intelligence-terminal:latest .` Heavy install/build/audit/release work should run on Gitea Ubuntu runners where possible. Local work should stay limited to targeted verification and Docker checks required for this deployment. diff --git a/.env.example b/.env.example index ca66862..fd4df9b 100644 --- a/.env.example +++ b/.env.example @@ -16,7 +16,7 @@ TERMINAL_ACTION_RATE_LIMIT_MAX=10 BRIEF_VERBOSITY=standard # LLM layer -# Providers: openrouter | openai-compatible | lmstudio | ollama | openai | anthropic | gemini | mistral | minimax | grok | codex +# Providers: litellm | openrouter | openai-compatible | lmstudio | ollama | openai | anthropic | gemini | mistral | minimax | grok | codex LLM_PROVIDER=openrouter LLM_BASE_URL=https://openrouter.ai/api/v1 LLM_API_KEY= @@ -24,10 +24,11 @@ LLM_MODEL=openrouter/free LLM_TEMPERATURE=0.2 LLM_MAX_TOKENS=2000 LLM_TIMEOUT_MS=90000 -OPENROUTER_SITE_URL=https://git.wilkensxl.de/MrSphay/intelligence-terminal +OPENROUTER_SITE_URL=https://git.wilkensxl.de/Code-Inc/intelligence-terminal OPENROUTER_APP_NAME=Intelligence Terminal # Local OpenAI-compatible examples +# LiteLLM: LLM_PROVIDER=litellm, LLM_BASE_URL=https://llm.example.com/v1, LLM_API_KEY=your-proxy-key, LLM_MODEL=your-model-alias # LM Studio: LLM_PROVIDER=lmstudio, LLM_BASE_URL=http://host.docker.internal:1234/v1, LLM_MODEL=local-model # Ollama: LLM_PROVIDER=ollama, LLM_BASE_URL=http://host.docker.internal:11434, LLM_MODEL=llama3.1:8b # Generic: LLM_PROVIDER=openai-compatible, LLM_BASE_URL=http://host.docker.internal:8000/v1, LLM_MODEL=your-model diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index cde440d..8efe16f 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -15,7 +15,7 @@ jobs: env: REGISTRY_HOST: git.wilkensxl.de REGISTRY_USERNAME: MrSphay - REGISTRY_NAMESPACE: mrsphay + REGISTRY_NAMESPACE: code-inc IMAGE_NAME: intelligence-terminal REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} steps: @@ -46,7 +46,7 @@ jobs: docker build -t "${image}:${build_tag}" . - name: Publish Docker image - if: ${{ env.REGISTRY_TOKEN != '' }} + if: ${{ env.REGISTRY_TOKEN != '' && github.event_name == 'push' && github.ref == 'refs/heads/codex/production-intelligence-terminal' }} shell: bash run: | image="${REGISTRY_HOST}/${REGISTRY_NAMESPACE}/${IMAGE_NAME}" diff --git a/AGENTS.md b/AGENTS.md index 5b010f0..980d995 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,8 +18,8 @@ Intelligence Terminal is a Docker-first Crucix fork for home-server OSINT, marke - Unit tests: `npm run test:unit` - Full tests: `npm test` - Compose validation: `docker compose config` -- Docker image: `docker build -t git.wilkensxl.de/mrsphay/intelligence-terminal:latest .` +- Docker image: `docker build -t git.wilkensxl.de/code-inc/intelligence-terminal:latest .` ## Release Target -Push source to `https://git.wilkensxl.de/MrSphay/intelligence-terminal.git` and publish the Docker image to `git.wilkensxl.de/mrsphay/intelligence-terminal`. +Push source to `https://git.wilkensxl.de/Code-Inc/intelligence-terminal.git` and publish the Docker image to `git.wilkensxl.de/code-inc/intelligence-terminal`. diff --git a/README.md b/README.md index a9bb0ff..219d1bb 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ **Self-hosted intelligence dashboard. 27 open sources. Docker-first. No telemetry.** -[![Gitea Repository](https://img.shields.io/badge/source-Gitea-609926?style=for-the-badge&logo=gitea&logoColor=white)](https://git.wilkensxl.de/MrSphay/intelligence-terminal) -[![Docker Image](https://img.shields.io/badge/image-Gitea%20Registry-0b1220?style=for-the-badge&logo=docker&logoColor=white)](https://git.wilkensxl.de/MrSphay/-/packages/container/intelligence-terminal/latest) +[![Gitea Repository](https://img.shields.io/badge/source-Gitea-609926?style=for-the-badge&logo=gitea&logoColor=white)](https://git.wilkensxl.de/Code-Inc/intelligence-terminal) +[![Docker Image](https://img.shields.io/badge/image-Gitea%20Registry-0b1220?style=for-the-badge&logo=docker&logoColor=white)](https://git.wilkensxl.de/Code-Inc/-/packages/container/intelligence-terminal/latest) [![Node.js 22+](https://img.shields.io/badge/node-22%2B-brightgreen)](#quick-start) [![License: AGPL v3](https://img.shields.io/badge/license-AGPLv3-blue.svg)](LICENSE) @@ -32,7 +32,7 @@ > **Supported deployment:** private home-server or lab deployment through Docker, Dockge, Pangolin, or local Node.js. > Runtime data stays in your configured `runs/` volume and API keys are operator-owned. -> **Source:** [git.wilkensxl.de/MrSphay/intelligence-terminal](https://git.wilkensxl.de/MrSphay/intelligence-terminal) +> **Source:** [git.wilkensxl.de/Code-Inc/intelligence-terminal](https://git.wilkensxl.de/Code-Inc/intelligence-terminal) > Pull the image or clone the repository to run Intelligence Terminal on your own infrastructure. Intelligence Terminal pulls satellite fire detection, flight tracking, radiation monitoring, satellite constellation tracking, economic indicators, live market prices, conflict data, sanctions lists, and social sentiment from 27 open-source intelligence feeds in parallel, every 15 minutes, and renders everything on a single self-contained dashboard. @@ -66,7 +66,7 @@ It was built for anyone who wants to understand what's actually happening in the ```bash # 1. Clone the repo -git clone https://git.wilkensxl.de/MrSphay/intelligence-terminal.git +git clone https://git.wilkensxl.de/Code-Inc/intelligence-terminal.git cd intelligence-terminal # 2. Install dependencies (just Express) @@ -92,7 +92,7 @@ The dashboard opens automatically at `http://localhost:3117` and immediately beg ### Docker ```bash -docker pull git.wilkensxl.de/mrsphay/intelligence-terminal:latest +docker pull git.wilkensxl.de/code-inc/intelligence-terminal:latest ``` Dashboard at `http://localhost:3117`. Sweep data persists in `./runs/` via volume mount. The container disables browser auto-open by default, exposes `/api/health` and `/api/metrics`, and is suitable for Dockge/Pangolin. @@ -102,7 +102,7 @@ Dashboard at `http://localhost:3117`. Sweep data persists in `./runs/` via volum ```yaml services: intelligence-terminal: - image: git.wilkensxl.de/mrsphay/intelligence-terminal:latest + image: git.wilkensxl.de/code-inc/intelligence-terminal:latest container_name: intelligence-terminal env_file: - path: .env @@ -146,7 +146,7 @@ LLM_MODEL=openrouter/free LLM_TEMPERATURE=0.2 LLM_MAX_TOKENS=2000 LLM_TIMEOUT_MS=90000 -OPENROUTER_SITE_URL=https://git.wilkensxl.de/MrSphay/intelligence-terminal +OPENROUTER_SITE_URL=https://git.wilkensxl.de/Code-Inc/intelligence-terminal OPENROUTER_APP_NAME=Intelligence Terminal FRED_API_KEY= @@ -169,6 +169,12 @@ DISCORD_WEBHOOK_URL= Local LLM examples: ```env +# LiteLLM proxy (the URL must include the OpenAI-compatible /v1 path) +LLM_PROVIDER=litellm +LLM_BASE_URL=https://llm.example.com/v1 +LLM_API_KEY=your-litellm-api-key +LLM_MODEL=your-model-alias + # LM Studio LLM_PROVIDER=lmstudio LLM_BASE_URL=http://host.docker.internal:1234/v1 @@ -288,10 +294,10 @@ Scenario states are `dormant`, `watching`, `building`, and `confirmed`. The dash ```bash docker login git.wilkensxl.de -u MrSphay -docker build -t git.wilkensxl.de/mrsphay/intelligence-terminal:latest . -docker tag git.wilkensxl.de/mrsphay/intelligence-terminal:latest git.wilkensxl.de/mrsphay/intelligence-terminal:20260516 -docker push git.wilkensxl.de/mrsphay/intelligence-terminal:latest -docker push git.wilkensxl.de/mrsphay/intelligence-terminal:20260516 +docker build -t git.wilkensxl.de/code-inc/intelligence-terminal:latest . +docker tag git.wilkensxl.de/code-inc/intelligence-terminal:latest git.wilkensxl.de/code-inc/intelligence-terminal:YYYYMMDD +docker push git.wilkensxl.de/code-inc/intelligence-terminal:latest +docker push git.wilkensxl.de/code-inc/intelligence-terminal:YYYYMMDD ``` Gitea Actions publishes the same image automatically when the repository secret `REGISTRY_TOKEN` is set with package read/write permissions. The workflow tags images as `latest`, the commit SHA, and a UTC `YYYYMMDD` release tag. @@ -380,7 +386,7 @@ Alerts are delivered as rich embeds with color-coded sidebars: red for FLASH, ye Connect cloud or local OpenAI-compatible LLM providers for enhanced analysis: - **AI trade ideas** — quantitative analyst producing 5-8 actionable ideas citing specific data - **Smarter alert evaluation** — LLM classifies signals into FLASH/PRIORITY/ROUTINE tiers with cross-domain correlation and confidence scoring -- Providers: OpenRouter, OpenAI-compatible APIs, LM Studio, Ollama, OpenAI, Anthropic Claude, Google Gemini, OpenAI Codex, MiniMax, Mistral, Grok +- Providers: LiteLLM, OpenRouter, OpenAI-compatible APIs, LM Studio, Ollama, OpenAI, Anthropic Claude, Google Gemini, OpenAI Codex, MiniMax, Mistral, Grok - Graceful fallback — when LLM is unavailable, a rule-based engine takes over alert evaluation. LLM failures never crash the sweep cycle. Primary env keys: @@ -625,7 +631,7 @@ All settings are in `.env` with sensible defaults: | `STALE_DATA_MAX_AGE_MINUTES` | `60` | Data age threshold for stale health state | | `STALE_ALERT_COOLDOWN_MINUTES` | `60` | Minimum time between repeated operator stale-data alerts | | `DASHBOARD_URL` | local URL | Dashboard URL included in operator alerts | -| `LLM_PROVIDER` | disabled | `anthropic`, `openai`, `gemini`, `codex`, `openrouter`, `minimax`, `mistral`, or `grok` | +| `LLM_PROVIDER` | disabled | `litellm`, `openrouter`, `openai-compatible`, `lmstudio`, `ollama`, `anthropic`, `openai`, `gemini`, `codex`, `minimax`, `mistral`, or `grok` | | `LLM_API_KEY` | — | API key (not needed for codex) | | `LLM_MODEL` | per-provider default | Override model selection | | `TELEGRAM_BOT_TOKEN` | disabled | For Telegram alerts + bot commands | @@ -751,7 +757,7 @@ For contribution guidelines, review expectations, and source-add rules, see `CON For bugs, feature requests, and integration ideas, use the Gitea issue tracker so discussion stays visible and actionable: -https://git.wilkensxl.de/MrSphay/intelligence-terminal/issues +https://git.wilkensxl.de/Code-Inc/intelligence-terminal/issues ## Upstream And License diff --git a/crucix.config.mjs b/crucix.config.mjs index b056dfa..ace7766 100644 --- a/crucix.config.mjs +++ b/crucix.config.mjs @@ -32,14 +32,14 @@ export default { sseHeartbeatIntervalMs: intEnv('SSE_HEARTBEAT_INTERVAL_MS', 25000), llm: { - provider: process.env.LLM_PROVIDER || null, // anthropic | openai | gemini | codex | openrouter | minimax | mistral | ollama | grok + provider: process.env.LLM_PROVIDER || null, // litellm | openrouter | openai-compatible | lmstudio | ollama | other supported providers apiKey: process.env.LLM_API_KEY || null, model: process.env.LLM_MODEL || null, baseUrl: process.env.LLM_BASE_URL || process.env.OPENAI_BASE_URL || process.env.OLLAMA_BASE_URL || null, temperature: floatEnv('LLM_TEMPERATURE', 0.2), maxTokens: intEnv('LLM_MAX_TOKENS', 2000), timeoutMs: intEnv('LLM_TIMEOUT_MS', 90000), - openRouterSiteUrl: process.env.OPENROUTER_SITE_URL || 'https://git.wilkensxl.de/MrSphay/intelligence-terminal', + openRouterSiteUrl: process.env.OPENROUTER_SITE_URL || 'https://git.wilkensxl.de/Code-Inc/intelligence-terminal', openRouterAppName: process.env.OPENROUTER_APP_NAME || 'Intelligence Terminal', }, diff --git a/docker-compose.yml b/docker-compose.yml index f152ece..76f873e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: intelligence-terminal: - image: git.wilkensxl.de/mrsphay/intelligence-terminal:latest + image: git.wilkensxl.de/code-inc/intelligence-terminal:latest build: context: . dockerfile: Dockerfile diff --git a/docs/agent-handoff.md b/docs/agent-handoff.md index 3eee3ed..e404a1b 100644 --- a/docs/agent-handoff.md +++ b/docs/agent-handoff.md @@ -1,6 +1,17 @@ # Agent Handoff -Last updated: 2026-05-17 +Last updated: 2026-07-03 + +## Current Work + +- Canonical repository: `https://git.wilkensxl.de/Code-Inc/intelligence-terminal` +- Production baseline before this work: `c159c83a0768486c8c6f445b458b760dba4ba385` +- Active branch: `codex/issue-47-litellm-provider` +- Issue: `#47 Add first-class LiteLLM provider and publish updated image` +- LiteLLM is implemented through the OpenAI-compatible `/chat/completions` API and requires `LLM_BASE_URL`, `LLM_API_KEY`, and `LLM_MODEL`. +- The build workflow now targets `git.wilkensxl.de/code-inc/intelligence-terminal` and publishes only from the production branch, not from pull requests. +- Runner build/test/image verification and the first `code-inc` registry publication must be recorded here after the branch is pushed and merged. +- Related maintenance: issue #21 tracks the failing security scan, #45 tracks the dependency workflow, and #46 tracks remaining namespace/handoff cleanup. ## Repository State @@ -15,7 +26,7 @@ C:\Users\MrSphay\Documents\Codex\Crucix\intelligence-terminal Remotes: ```text -origin https://git.wilkensxl.de/MrSphay/intelligence-terminal.git +origin https://git.wilkensxl.de/Code-Inc/intelligence-terminal.git upstream https://github.com/calesthio/Crucix.git ``` @@ -25,23 +36,22 @@ Current branch tip: Run `git rev-parse HEAD` after clone/pull. This handoff was updated by the `docs: sync issue tracker and handoff` commit after the implementation commit below. ``` -Latest implementation commit before issue-sync documentation: +Production baseline before the current LiteLLM work: ```text -53470cc701ec322080a89d220aef449b25850590 +c159c83a0768486c8c6f445b458b760dba4ba385 ``` -Both pushed branches currently point to this commit: +The default production branch points to this commit before the current PR: ```text origin/codex/production-intelligence-terminal -origin/main ``` Gitea repository: ```text -https://git.wilkensxl.de/MrSphay/intelligence-terminal +https://git.wilkensxl.de/Code-Inc/intelligence-terminal ``` Default branch observed through the Gitea API: @@ -79,7 +89,7 @@ Rules applied from the kit: - `docker-compose.yml` uses the Gitea Registry image by default: ```text -git.wilkensxl.de/mrsphay/intelligence-terminal:latest +git.wilkensxl.de/code-inc/intelligence-terminal:latest ``` ### API And Health @@ -226,32 +236,27 @@ README includes: ## Registry And Images -Registry image: +Target registry image after the current production merge: ```text -git.wilkensxl.de/mrsphay/intelligence-terminal +git.wilkensxl.de/code-inc/intelligence-terminal ``` -Verified package tags through Gitea API: +The legacy `mrsphay` package remains available. Verify these new `code-inc` tags after the current runner publication: ```text latest -20260517 -e933586b220656a2858d2215b934b22d1f08a908 -53470cc701ec322080a89d220aef449b25850590 +YYYYMMDD + ``` -Successful pull test: +Required pull verification after publication: ```bash -docker pull git.wilkensxl.de/mrsphay/intelligence-terminal:latest +docker pull git.wilkensxl.de/code-inc/intelligence-terminal:latest ``` -Observed digest: - -```text -sha256:780a41413921bd9a676461eca1cd1372591f523be4b7c9513d9bc085cbe7922d -``` +Record the resulting digest after the runner push. ## Gitea Actions @@ -460,7 +465,7 @@ if ($env:GITEA_TOKEN) { "GITEA_TOKEN=set" } else { "GITEA_TOKEN=missing" } ```bash npm run test:unit docker compose --env-file .env.example config -docker pull git.wilkensxl.de/mrsphay/intelligence-terminal:latest +docker pull git.wilkensxl.de/code-inc/intelligence-terminal:latest ``` 6. Start with Dockge/Pangolin using the README compose example and a `.env` based on `.env.example`. @@ -479,11 +484,11 @@ docker pull git.wilkensxl.de/mrsphay/intelligence-terminal:latest For deployment: ```bash -docker pull git.wilkensxl.de/mrsphay/intelligence-terminal:latest +docker pull git.wilkensxl.de/code-inc/intelligence-terminal:latest ``` For a pinned deployment: ```bash -docker pull git.wilkensxl.de/mrsphay/intelligence-terminal:20260517 +docker pull git.wilkensxl.de/code-inc/intelligence-terminal:YYYYMMDD ``` diff --git a/docs/release-checklist.md b/docs/release-checklist.md index 3f1f91b..eed4ae8 100644 --- a/docs/release-checklist.md +++ b/docs/release-checklist.md @@ -3,7 +3,7 @@ 1. Confirm `.env.example`, README compose sample, and registry image name match. 2. Run `npm run test:unit`. 3. Run `docker compose config`. -4. Build `git.wilkensxl.de/mrsphay/intelligence-terminal:latest`. +4. Build `git.wilkensxl.de/code-inc/intelligence-terminal:latest`. 5. Start the image and verify `/api/health`. 6. Push branch to Gitea. 7. Push `latest` and a dated image tag to the Gitea Registry. diff --git a/lib/llm/index.mjs b/lib/llm/index.mjs index 241b7e6..7e4905b 100644 --- a/lib/llm/index.mjs +++ b/lib/llm/index.mjs @@ -10,6 +10,7 @@ import { MistralProvider } from "./mistral.mjs"; import { OllamaProvider } from "./ollama.mjs"; import { GrokProvider } from "./grok.mjs"; import { OpenAICompatibleProvider } from "./openai-compatible.mjs"; +import { LiteLLMProvider } from "./litellm.mjs"; export { LLMProvider } from "./provider.mjs"; export { AnthropicProvider } from "./anthropic.mjs"; @@ -22,6 +23,7 @@ export { MistralProvider } from "./mistral.mjs"; export { OllamaProvider } from "./ollama.mjs"; export { GrokProvider } from "./grok.mjs"; export { OpenAICompatibleProvider } from "./openai-compatible.mjs"; +export { LiteLLMProvider } from "./litellm.mjs"; /** * Create an LLM provider based on config. @@ -66,6 +68,9 @@ export function createLLMProvider(llmConfig) { model: model || 'local-model', requiresApiKey: false, }); + case "litellm": + case "lite-llm": + return new LiteLLMProvider(common); case "openrouter": return new OpenRouterProvider(common); case "gemini": diff --git a/lib/llm/litellm.mjs b/lib/llm/litellm.mjs new file mode 100644 index 0000000..e9fa770 --- /dev/null +++ b/lib/llm/litellm.mjs @@ -0,0 +1,32 @@ +// LiteLLM proxy provider using the OpenAI-compatible chat completions API. + +import { OpenAICompatibleProvider } from './openai-compatible.mjs'; + +export class LiteLLMProvider extends OpenAICompatibleProvider { + constructor(config = {}) { + const baseUrl = config.baseUrl?.replace(/\/+$/, '') || null; + const model = config.model || null; + + super({ + ...config, + name: 'litellm', + baseUrl: baseUrl || 'http://localhost:4000/v1', + model: model || 'unconfigured', + requiresApiKey: true, + }); + + this.baseUrl = baseUrl; + this.model = model; + } + + get isConfigured() { + return Boolean(this.baseUrl && this.apiKey && this.model); + } + + get status() { + if (!this.baseUrl) return { state: 'misconfigured', reason: 'LLM_BASE_URL is required for LiteLLM' }; + if (!this.apiKey) return { state: 'misconfigured', reason: 'LLM_API_KEY is required for LiteLLM' }; + if (!this.model) return { state: 'misconfigured', reason: 'LLM_MODEL is required for LiteLLM' }; + return { state: 'configured', provider: this.name, model: this.model, baseUrl: this.baseUrl }; + } +} diff --git a/package.json b/package.json index c87b141..0dda791 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "brief:save": "node apis/save-briefing.mjs", "diag": "node diag.mjs", "test": "npm run test:unit", - "test:unit": "node --test test/llm-openrouter.test.mjs test/llm-ollama.test.mjs test/llm-openai-compatible.test.mjs test/fetch-utils.test.mjs test/reddit-source.test.mjs test/acled-source.test.mjs test/mojibake-text.test.mjs test/adsb.test.mjs test/dashboard-geotagging.test.mjs", + "test:unit": "node --test test/llm-openrouter.test.mjs test/llm-ollama.test.mjs test/llm-openai-compatible.test.mjs test/llm-litellm.test.mjs test/fetch-utils.test.mjs test/reddit-source.test.mjs test/acled-source.test.mjs test/mojibake-text.test.mjs test/adsb.test.mjs test/dashboard-geotagging.test.mjs", "compose:config": "docker compose config", "clean": "node scripts/clean.mjs", "fresh-start": "npm run clean && npm start" diff --git a/test/llm-litellm.test.mjs b/test/llm-litellm.test.mjs new file mode 100644 index 0000000..48d544d --- /dev/null +++ b/test/llm-litellm.test.mjs @@ -0,0 +1,76 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { LiteLLMProvider } from '../lib/llm/litellm.mjs'; +import { createLLMProvider } from '../lib/llm/index.mjs'; + +test('factory creates a configured LiteLLM provider', () => { + const provider = createLLMProvider({ + provider: 'litellm', + baseUrl: 'https://llm.example.test/v1/', + apiKey: 'proxy-key', + model: 'private-model', + }); + + assert.ok(provider instanceof LiteLLMProvider); + assert.equal(provider.baseUrl, 'https://llm.example.test/v1'); + assert.equal(provider.isConfigured, true); + assert.deepEqual(provider.status, { + state: 'configured', + provider: 'litellm', + model: 'private-model', + baseUrl: 'https://llm.example.test/v1', + }); +}); + +test('LiteLLM requires base URL, API key, and model', () => { + const missingBaseUrl = new LiteLLMProvider({ apiKey: 'key', model: 'model' }); + assert.equal(missingBaseUrl.isConfigured, false); + assert.equal(missingBaseUrl.status.reason, 'LLM_BASE_URL is required for LiteLLM'); + + const missingKey = new LiteLLMProvider({ baseUrl: 'https://llm.example.test/v1', model: 'model' }); + assert.equal(missingKey.status.reason, 'LLM_API_KEY is required for LiteLLM'); + + const missingModel = new LiteLLMProvider({ baseUrl: 'https://llm.example.test/v1', apiKey: 'key' }); + assert.equal(missingModel.status.reason, 'LLM_MODEL is required for LiteLLM'); +}); + +test('LiteLLM sends bearer-authenticated OpenAI-compatible requests', async () => { + const provider = new LiteLLMProvider({ + baseUrl: 'https://llm.example.test/v1', + apiKey: 'proxy-key', + model: 'private-model', + temperature: 0.15, + maxTokens: 512, + }); + const originalFetch = globalThis.fetch; + + globalThis.fetch = async (url, options) => { + assert.equal(url, 'https://llm.example.test/v1/chat/completions'); + assert.equal(options.headers.Authorization, 'Bearer proxy-key'); + assert.deepEqual(JSON.parse(options.body), { + model: 'private-model', + temperature: 0.15, + messages: [ + { role: 'system', content: 'system' }, + { role: 'user', content: 'user' }, + ], + max_tokens: 512, + }); + return { + ok: true, + json: async () => ({ + choices: [{ message: { content: 'response' } }], + usage: { prompt_tokens: 7, completion_tokens: 11 }, + model: 'private-model', + }), + }; + }; + + try { + const result = await provider.complete('system', 'user'); + assert.equal(result.text, 'response'); + assert.deepEqual(result.usage, { inputTokens: 7, outputTokens: 11 }); + } finally { + globalThis.fetch = originalFetch; + } +}); -- 2.49.1