15 Commits

Author SHA1 Message Date
e0e408d1eb feat: add LiteLLM provider and Code-Inc image target
All checks were successful
Codex Template Compliance / template-compliance (pull_request) Successful in 6s
Build / test-and-image (pull_request) Successful in 1m12s
2026-07-03 23:06:33 +02:00
c159c83a07 Merge pull request 'revert(ui): remove app-style dashboard redesign' (#44) from codex/revert-app-style-design into codex/production-intelligence-terminal
Some checks failed
Release Dry Run / release-dry-run (push) Successful in 14s
Codex Template Compliance / template-compliance (push) Successful in 6s
Build / test-and-image (push) Successful in 25s
Scheduled Security Scan / security-scan (push) Failing after 9s
Scheduled Repository Cleanup Check / cleanup-check (push) Successful in 8s
Scheduled Dependency Check / dependency-check (push) Failing after 9s
Reviewed-on: MrSphay/intelligence-terminal#44
2026-05-17 20:45:24 +00:00
MrSphay
a1d415e449 Revert "Merge pull request 'feat(ui): redesign dashboard in app-style shell' (#42) from codex/modrinth-app-redesign into codex/production-intelligence-terminal"
All checks were successful
Build / test-and-image (pull_request) Successful in 25s
Codex Template Compliance / template-compliance (pull_request) Successful in 4s
This reverts commit 9f2083a324, reversing
changes made to 1c2b48f588.
2026-05-17 22:35:12 +02:00
MrSphay
0f5f9c5f91 Revert "Merge pull request 'feat(ui): finalize app-style dashboard motion and QA' (#43) from codex/modrinth-app-finalization into codex/production-intelligence-terminal"
This reverts commit 096544f6e6, reversing
changes made to 9f2083a324.
2026-05-17 22:35:07 +02:00
096544f6e6 Merge pull request 'feat(ui): finalize app-style dashboard motion and QA' (#43) from codex/modrinth-app-finalization into codex/production-intelligence-terminal
All checks were successful
Release Dry Run / release-dry-run (push) Successful in 14s
Codex Template Compliance / template-compliance (push) Successful in 6s
Build / test-and-image (push) Successful in 26s
Reviewed-on: MrSphay/intelligence-terminal#43
2026-05-17 20:08:03 +00:00
MrSphay
5a3dbc6252 feat(ui): redesign dashboard in app-style shell
All checks were successful
Codex Template Compliance / template-compliance (pull_request) Successful in 5s
Build / test-and-image (pull_request) Successful in 1m4s
2026-05-17 21:54:36 +02:00
9f2083a324 Merge pull request 'feat(ui): redesign dashboard in app-style shell' (#42) from codex/modrinth-app-redesign into codex/production-intelligence-terminal
All checks were successful
Release Dry Run / release-dry-run (push) Successful in 14s
Codex Template Compliance / template-compliance (push) Successful in 5s
Build / test-and-image (push) Successful in 24s
Reviewed-on: MrSphay/intelligence-terminal#42
2026-05-17 19:04:46 +00:00
dd08ecaf27 Merge branch 'codex/production-intelligence-terminal' into codex/modrinth-app-redesign
All checks were successful
Codex Template Compliance / template-compliance (pull_request) Successful in 5s
Build / test-and-image (pull_request) Successful in 57s
2026-05-17 19:03:03 +00:00
MrSphay
bc354e7bc5 feat(ui): redesign dashboard in app-style shell
All checks were successful
Codex Template Compliance / template-compliance (pull_request) Successful in 5s
Build / test-and-image (pull_request) Successful in 59s
2026-05-17 20:57:54 +02:00
1c2b48f588 Merge pull request 'fix: infer source fetch metrics' (#35) from codex/issue-22-source-fetch-instrumentation into codex/production-intelligence-terminal
All checks were successful
Release Dry Run / release-dry-run (push) Successful in 14s
Codex Template Compliance / template-compliance (push) Successful in 5s
Build / test-and-image (push) Successful in 27s
2026-05-17 18:53:45 +00:00
MrSphay
a590bf62c2 Merge remote-tracking branch 'origin/codex/production-intelligence-terminal' into codex/issue-22-source-fetch-instrumentation
All checks were successful
Codex Template Compliance / template-compliance (pull_request) Successful in 5s
Build / test-and-image (pull_request) Successful in 57s
# Conflicts:
#	test/fetch-utils.test.mjs
2026-05-17 20:52:27 +02:00
6a9918bc98 Merge pull request 'fix: keep sse streams alive behind proxies' (#34) from codex/issue-17-sse-heartbeat into codex/production-intelligence-terminal
All checks were successful
Build / test-and-image (push) Successful in 25s
Release Dry Run / release-dry-run (push) Successful in 13s
Codex Template Compliance / template-compliance (push) Successful in 5s
2026-05-17 18:51:04 +00:00
MrSphay
e4834cd3cd Merge remote-tracking branch 'origin/codex/production-intelligence-terminal' into codex/issue-22-source-fetch-instrumentation
All checks were successful
Codex Template Compliance / template-compliance (pull_request) Successful in 5s
Build / test-and-image (pull_request) Successful in 57s
# Conflicts:
#	test/fetch-utils.test.mjs
2026-05-17 20:40:44 +02:00
MrSphay
c102017b16 Merge remote-tracking branch 'origin/codex/production-intelligence-terminal' into codex/issue-22-source-fetch-instrumentation
All checks were successful
Codex Template Compliance / template-compliance (pull_request) Successful in 5s
Build / test-and-image (pull_request) Successful in 55s
# Conflicts:
#	README.md
#	test/fetch-utils.test.mjs
2026-05-17 20:37:21 +02:00
MrSphay
2025ae09db fix: infer source fetch metrics
All checks were successful
Codex Template Compliance / template-compliance (pull_request) Successful in 4s
Build / test-and-image (pull_request) Successful in 53s
2026-05-17 14:44:21 +02:00
16 changed files with 265 additions and 54 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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}"

View File

@@ -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`.

View File

@@ -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
@@ -249,6 +255,8 @@ Recommended proxy settings:
If you raise the heartbeat interval, keep it shorter than the lowest idle timeout in the proxy chain.
`/api/metrics` includes network health grouped by host and source/provider. Source modules should use `safeFetch(url, { source: 'SourceName' })`; when omitted, the shared helper infers a stable provider bucket from the URL host instead of grouping normal source traffic under `unknown`. Raw fetch exceptions are documented in [Source Fetch Instrumentation](docs/source-fetch-instrumentation.md).
#### Scenario Watchlist
Intelligence Terminal can track operator hypotheses across sweeps with a runtime scenario file at `runs/scenarios.json`. On first run, the server creates three disabled starter examples:
@@ -286,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.
@@ -378,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:
@@ -623,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 |
@@ -749,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

View File

@@ -10,6 +10,44 @@ const fetchMetrics = {
recent: [],
};
const SOURCE_BY_HOST = [
[/api\.bls\.gov$/i, 'BLS'],
[/api\.fred\.stlouisfed\.org$/i, 'FRED'],
[/api\.eia\.gov$/i, 'EIA'],
[/api\.gdeltproject\.org$/i, 'GDELT'],
[/api\.weather\.gov$/i, 'NOAA'],
[/api\.open-notify\.org$/i, 'OpenNotify'],
[/opensky-network\.org$/i, 'OpenSky'],
[/firms\.modaps\.eosdis\.nasa\.gov$/i, 'FIRMS'],
[/api\.acleddata\.com$/i, 'ACLED'],
[/api\.reliefweb\.int$/i, 'ReliefWeb'],
[/receiverbook\.de$/i, 'KiwiSDR'],
[/safecast\.org$/i, 'Safecast'],
[/api\.patentsview\.org$/i, 'PatentsView'],
[/api\.trade\.gov$/i, 'Comtrade'],
[/api\.usaspending\.gov$/i, 'USASpending'],
[/api\.telegram\.org$/i, 'Telegram'],
[/oauth\.reddit\.com$/i, 'Reddit'],
[/reddit\.com$/i, 'Reddit'],
[/api\.bsky\.app$/i, 'Bluesky'],
[/api\.yahoo\.com$/i, 'YahooFinance'],
[/query\d?\.finance\.yahoo\.com$/i, 'YahooFinance'],
[/api\.cloudflare\.com$/i, 'CloudflareRadar'],
[/api\.opensanctions\.org$/i, 'OpenSanctions'],
[/home\.treasury\.gov$/i, 'Treasury'],
[/fiscaldata\.treasury\.gov$/i, 'Treasury'],
[/who\.int$/i, 'WHO'],
];
export function inferFetchSource(url) {
let host = 'unknown';
try { host = new URL(url).host.toLowerCase(); } catch { return 'unknown'; }
for (const [pattern, source] of SOURCE_BY_HOST) {
if (pattern.test(host)) return source;
}
return host;
}
function metricBucket(map, key) {
if (!map[key]) map[key] = { requests: 0, ok: 0, failed: 0, bytes: 0, lastStatus: null, lastError: null, lastMs: 0 };
return map[key];
@@ -38,7 +76,7 @@ export function getFetchMetrics() {
}
export async function safeFetch(url, opts = {}) {
const { timeout = 15000, retries = 1, headers = {}, source = undefined } = opts;
const { timeout = 15000, retries = 1, headers = {}, source = inferFetchSource(url) } = opts;
let lastError;
for (let i = 0; i <= retries; i++) {
const started = Date.now();
@@ -79,11 +117,11 @@ export async function safeFetch(url, opts = {}) {
if (i < retries) await new Promise(r => setTimeout(r, 2000 * (i + 1)));
}
}
return { error: lastError?.message || 'Unknown error', source: url };
return { error: lastError?.message || 'Unknown error', source };
}
export async function safeFetchText(url, opts = {}) {
const { timeout = 15000, retries = 1, headers = {}, source = undefined } = opts;
const { timeout = 15000, retries = 1, headers = {}, source = inferFetchSource(url) } = opts;
let lastError;
for (let i = 0; i <= retries; i++) {
const started = Date.now();

View File

@@ -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',
},

View File

@@ -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

View File

@@ -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
<production-commit-sha>
```
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
```

View File

@@ -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.

View File

@@ -0,0 +1,21 @@
# Source Fetch Instrumentation
`safeFetch()` and `safeFetchText()` attribute requests to `/api/metrics.fetch.bySource`.
Rules:
- Prefer passing an explicit `source` option from source modules when the call has a clear Crucix source name.
- If `source` is omitted, the shared helper infers a stable provider name from the request host.
- Unknown hosts fall back to the lowercase host instead of the old `unknown` bucket.
- Raw `fetch()` calls should be limited to cases where the shared helper cannot represent the protocol cleanly.
Current raw-fetch exceptions:
| Area | Reason |
| --- | --- |
| OAuth/session handshakes | Token exchange calls often need custom form bodies, credential headers, or status-specific diagnostics. |
| Bot and alert delivery | Telegram/Discord alert calls are outbound operator notifications, not intelligence source health. |
| LLM providers | Provider clients already track model/provider status separately from source fetch health. |
| Dashboard browser calls | Browser-side `/api/*` and asset fetches are UI behavior, not source provider health. |
When adding a new intelligence source, use `safeFetch(url, { source: 'SourceName' })` unless there is a documented exception.

View File

@@ -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":

32
lib/llm/litellm.mjs Normal file
View File

@@ -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 };
}
}

View File

@@ -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"

View File

@@ -1,7 +1,7 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { safeFetch, safeFetchText, getFetchMetrics } from '../apis/utils/fetch.mjs';
import { safeFetch, safeFetchText, getFetchMetrics, inferFetchSource } from '../apis/utils/fetch.mjs';
import { formatStaleAlert, shouldSendStaleAlert } from '../lib/stale-alerts.mjs';
test('safeFetch reports HTML as degraded JSON response', async () => {
@@ -101,6 +101,31 @@ test('safeFetchText returns text and byte count', async () => {
}
});
test('safeFetch attributes unlabelled requests to a stable provider source', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => ({
ok: true,
status: 200,
headers: { get: () => 'application/json' },
text: async () => '{"observations":[]}',
});
try {
const data = await safeFetch('https://api.fred.stlouisfed.org/fred/series/observations?series_id=VIXCLS', { retries: 0 });
assert.deepEqual(data, { observations: [] });
const bucket = getFetchMetrics().bySource.FRED;
assert.ok(bucket.requests >= 1);
assert.equal(bucket.lastStatus, 200);
} finally {
globalThis.fetch = originalFetch;
}
});
test('inferFetchSource returns provider names and host fallback', () => {
assert.equal(inferFetchSource('https://api.bls.gov/publicAPI/v2/timeseries/data/CPI'), 'BLS');
assert.equal(inferFetchSource('https://query1.finance.yahoo.com/v8/finance/chart/%5EGSPC'), 'YahooFinance');
assert.equal(inferFetchSource('https://unknown.example.test/path'), 'unknown.example.test');
});
test('SSE endpoint sends reconnect guidance and clears heartbeat timer', () => {
const server = readFileSync(new URL('../server.mjs', import.meta.url), 'utf8');
const config = readFileSync(new URL('../crucix.config.mjs', import.meta.url), 'utf8');

76
test/llm-litellm.test.mjs Normal file
View File

@@ -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;
}
});