AI Consensus Platform — Multi-LLM deliberative intelligence engine.
Version: 1.8.18 | PHP 8.4 | MySQL 8.x | BotHub + OpenRouter
- Refreshed fresh-install default model selection. A brand-new
install now pre-selects, in order,
openai/gpt-chat-latest,anthropic/claude-sonnet-4.6,google/gemini-3.1-pro-previewanddeepseek/deepseek-v4-proon OpenRouter.openai/gpt-5.5is still shipped in the OpenRouter catalogue and can be enabled from Settings, but is no longer one of the four pre-selected defaults. Any operator-supplied selection (viapipeline.default_selected_modelsinconfig.php/.config.php, or saved through the Settings UI) wins unchanged.
- Semantic-component consensus scoring. L3 no longer compares model
answers as raw token bags. The new
SemanticConsensusEngineextracts semantic components (claims / bullets / sentences) from each model's reply, clusters them across models using keyword stems, character 3-grams and numeric / named-entity hints, and scores consensus as the fraction of cross-model semantic content covered by shared clusters. Russian and English paraphrases of the same idea now score in the partial / agreement band rather than collapsing to ≈ 0.07. Legacylexical_jaccardscoring is preserved behindpipeline.consensus.scoring = lexical_jaccardand a per-skillconsensus_scoringoverride. Seedocs/consensus-research.mdfor the methodology and citations. - Selectable consensus skill on the main page. The chat form now
exposes a
Consensus skilldropdown driven by/api/consensus/skills. The new bundledsemantic-consensus-reasoningskill is the default and pairs with the semantic scorer; the legacyconsensus-reasoningskill remains available. The selection is honoured per request — the pipeline picks the round-2 system prompt and the scoring backend from the active skill. - Share button + read-only public share links. A completed pipeline
result can be shared via
POST /api/share/session(used by the Chat tab's new Share button). The endpoint mints a 64-character opaque hex token (256 bits of entropy fromrandom_bytes) and the share URL/shared?token=…renders a read-only view of the query, final answer and full consensus / audit history. The shared page never exposes model selection controls, Settings, Logs, Security, History/Statistic admin panels or the ability to run pipelines, and all rendered text is passed through the secret-redaction filter. Tokens are persisted inshared_sessions(migration006_shared_sessions.sql).
- Round-2 consensus with peer answers. When the L3 verdict after
round 1 is not
agreement, the pipeline now re-runs every L1 model call with peer context: each model receives the original query, its own previous answer, every OTHER model's answer (clearly labelled by role / provider / model — its own answer is NOT in the peer block), the round-1 consensus score, and a new lateral-thinking system prompt (round2_system_prompton theconsensus-reasoningskill). Models are asked to revise OR defend their position using peer evidence rather than blindly copying. Round budget is governed bypipeline.max_consensus_rounds(default2). UI groups rounds under the existing "Consensus Round N" headers automatically.
- Pipeline UX overhaul. A new "Current activity" card pinned to
the bottom of the pipeline always shows what the backend is doing
right now (live spinner, stage, elapsed time, last-update timestamp);
on completion it switches to a green
completedstate and to a redfailedstate on aborted runs. Consensus rounds are now visually grouped by "Consensus Round N" headers. The L3 (consensus) step gets a dedicated accent-bordered card with a verdict badge, score gauge, participants/pairs tiles and structured sections. The Final Answer is rendered in its own visually-distinct section outside the pipeline so it is never confused with a regular step card.
- Robust admin token detection —
.admin.keyis now resolved relative to the project root (the directory holdingconfig.php/index.php), independently ofConfig::rootDir()and the process cwd. It works under php-fpm,php -S, CLI, and tests. When the file exists but the PHP process cannot read it, the admin tabs now surface a permissions hint instead of the generic "Admin token not configured" message./api/tabsand the per-tab*/statusendpoints expose a non-secretadmin_diagnosticsblock (source,env,file.{exists,readable,empty}) — the token value itself is never returned. Operators: place.admin.keyin the project root next toconfig.php,chmod 600, and ensure it is owned by (or readable by) the php-fpm user (e.g.www-data,nginx). Do NOT putREVOAP_ADMIN_TOKENinconfig.php— that variable is read from the environment only.
- Admin Statistic tab — aggregated usage of the council pipeline over selectable windows (
All/Year/Quarter/Week/Day/Hour): session totals (completed/failed/running), prompt/completion/total tokens, cached and reasoning tokens, cache-hit counts, total and average latency, total and average duration, and provider-reported cost (nullwhen not available — never fabricated as$0.00). A per-LLM table breaks every metric down per(provider, model). Gated byStatisticTab=trueAND the admin token AND a configured database. Defaultfalsebecause the metrics describe operational and per-user usage. Adds migration005_statistics_indexes.sql(idempotent, indexes only). Seedocs/Statistic.md. - Tab probe extended —
/api/tabsnow returns{settings, logs, security, history, statistic}.
- Admin History tab — browse every past council session (prompt, final answer, every L0/L1/L2/L3/L4 step, every per-step model response, every event, consensus result and the runtime options snapshot) from the UI. Server-paginated list with year/month grouping, search across query / answer / skill slug, status filter. Gated by
HistoryTab=trueAND the admin token AND a configured database. Defaultfalsebecause every prompt and model response is stored in plaintext (subject to the standard secret-redaction filter). Adds migration004_history_session_metadata.sql(idempotent) which introducescouncil_sessions.skill_slug,council_sessions.options_jsonand supporting indexes. Seedocs/History.md. - Tab probe extended —
/api/tabsnow returns{settings, logs, security, history}.
- Admin Security tab — manage the runtime IP firewall from the browser: enable/disable whitelist or blacklist enforcement, edit IP/CIDR rules (IPv4 + IPv6), configure trusted reverse proxies for safe
X-Forwarded-Forhandling, and run an ad-hoc IP rule test. Loopback (127.0.0.0/8,::1/128) is always allowed, the current admin IP is auto-added on every visit, and any loopback rule pasted into the blacklist is silently stripped — operators cannot lock themselves out. Gated bySecurityTab=trueAND the admin token. Enforcement runs at the front of every request (IpFirewall::enforce) and denied attempts are logged sanitized. Seedocs/Security.md. - Tab probe extended —
/api/tabsnow returns{settings, logs, security}so the SPA can hide tabs that are off inconfig.phpwithout a round-trip per probe.
- Config-driven tab visibility —
SettingsTab,LogsTaband (since 1.6.0)SecurityTabflags inconfig.php(top-level or underapp.*) decide which top-nav tabs render. Strict comparison: only booleantrueenables a tab. Whenfalse, the matching API surface returns404 *_disabled. - Admin Logs tab — view the last N lines of whitelisted log files (
revoap-debug.log/revoap-error.log/php-error.log/access.log), refresh on demand, optionally auto-refresh every 5 s, and (with confirmation) truncate a file. Output is sanitized through the same redactor as the structured debug logger — Bearer tokens,sk-…API keys, DSN passwords and absolute paths never reach the wire. Gated byLogsTab=trueAND the admin token. Seedocs/Logs.md.
- Admin Settings tab — manage OpenRouter / BotHub keys, the SQL DSN, the active model list, migrations, runtime flags and the model-response cache (statistics + purge) from the browser. Gated by
REVOAP_ADMIN_TOKENenv or a.admin.keyfile at the project root. Secrets are stored in.{provider}.keyfiles (0600) and never echoed back. Seedocs/Settings.md. - Runtime overrides —
var/config/runtime.jsonis merged on top ofconfig.phpso every Settings change takes effect on the next request, without editing the static config and without a PHP-FPM reload. - Migration tracker —
db/migrations/*.sqlfiles are inspected and applied through aschema_migrationstable; "already exists" errors are treated as benign so re-runs are safe.
- Resilient MySQL — repository writes survive idle disconnects (SQLSTATE HY000 2006/2013, "MySQL server has gone away") via a one-shot reconnect-and-retry wrapper. Long parallel L1 + L4 calls no longer abort with
PIPELINE_FATALafter a wait_timeout drop. - Fatal finalisation — when the pipeline aborts mid-flight, every step still in
runningis marked failed and a*_FAILEDevent is emitted so the UI replaces stale RUNNING cards with a readable failure. - Single-stream SSE — the front-end closes any active
EventSourcebefore opening a new one and on every terminalPIPELINE_DONE/PIPELINE_FATAL, so duplicate runs cannot occur. - Model response cache — successful chat responses are cached by sha256(provider, model, messages, reasoning, options). Hits skip the network call entirely and surface as
cache_hit=trueinL1_RESPONSE/L4_SYNTHESISevents. - Diagnostics —
/api/debugnow lists expected routes (including/api/council/stream), the cache backend status, and a build_id so operators can confirm a deploy has actually rolled out.
- MySQL
wait_timeout— a low value (default 28800s, but some hosts set 60s) will trip the gone-away path. Either raise it for the RevoAp schema's user, or rely on the new auto-retry. Seedocs/Storage.md. - nginx SSE buffering — for
/api/council/stream, setproxy_buffering off; proxy_cache off; gzip off;(Apache:SetEnv no-gzip 1). Without this, the EventSource looks frozen and the client may auto-reconnect. - Stale deploy — if
curl -N 'https://<host>/api/council/stream?query=test'returns{"error":"not found"}, the deployed code predates v1.1.0. Pull, restart PHP-FPM (or runopcache_reset()), and verify the build_id at/api/debug. - Cache invalidation — set
cache.model_responses.ttl_secondsinconfig.php, drop rows frommodel_response_cache, or remove files undervar/cache/model-responses/.
RevoAp accepts a natural-language query, routes it through a 4-layer AI council pipeline (Distribution → Normalization → Consensus → Synthesis), and delivers a synthesized answer with full transparency — every model response, token count, and consensus score is visible.
- Multi-model, multi-role council sessions via BotHub.chat and OpenRouter.ai
- Per-query consensus pool — pick 3+ models from the sidebar; selection persists in localStorage
- Parallel L1 dispatch via
curl_multi— every selected model is invoked concurrently - Live SSE pipeline updates — each L0/L1/L2/L3/L4 step is rendered as soon as it completes
- Reasoning / thinking modes (Off / Low / Medium / High) forwarded to OpenRouter chat completions
- Hover tooltips on every layer / event chip explaining the stage and its purpose
- Universal connector architecture — add any new LLM by dropping one file
- SKILLS system: declarative JSON workflows defining agent roles and consensus rules
- Forward-only pipeline — append-only event log, no history rewriting
- Per-step statistics tabs: tokens, latency, model, provider, cost, cached & reasoning tokens
- Session statistics: total tokens per model, efficiency metrics
- Light/dark theme with system preference detection
- PHP 8.4 strict types, readonly properties, enums throughout
- MySQL 8.x with PDO, prepared statements, explicit transactions
git clone https://github.com/paulmann/AI-RevoAp.git
cd AI-RevoAp
composer install # optional; src/autoload.php is used as a fallback
cp .env.example .env # or set keys in config.php / .{provider}.key files
mysql -u root opencorum < db/schema.sql # optional; in-memory fallback works without DB
php -S localhost:8080 -t .RevoAp boots without MySQL — the in-memory fallback is fine for trying
the council pipeline locally. To persist sessions, the History tab and
the Statistic tab, point database.dsn (in config.php or via the
admin Settings tab) at a MySQL 8.x database and load the consolidated
schema:
-
Fresh install (empty database) — apply the consolidated schema in one shot:
mysql -u root opencorum < db/schema.sqldb/schema.sqlis the single source of truth: it contains every column, table and index the app needs — including the job subsystem (revoap_jobs,revoap_job_events) and the memory store (memory_items) — as idempotentCREATE TABLE IF NOT EXISTSstatements. Thedb/migrations/directory holds the incremental migrations014…017added after the001…013chain was consolidated (seedb/migrations/README.md), so there is nothing to apply on top of a fresh import. -
Upgrade an existing RevoAp database —
db/schema.sqlis safe to re-import against an already-populated database (all statements are idempotent). The usual upgrade path is a new migration shipped underdb/migrations/NNN_*.sqlwhen a schema change must reach existing deployments; apply it through the admin Settings tab → Database maintenance → Apply Migrations (or the per-file Apply button). TheMigrationManagerskips any migration whose name already lives inschema_migrationsand treatsalready exists/duplicate columnerrors as benign so re-runs are safe.
Append ?creator to any URL to view author info (no configuration data is exposed there).
RevoAp ships a REST API for launching consensus runs, polling results
and receiving webhooks. Authentication is by API keys (rv_*), which
operators create on the Settings → API tab (SHA-256-hashed at
rest, scopes full/read, per-key rpm/daily quotas plus a
per-IP budget of 120 req/min).
# Launch (async 202), then poll to completion.
curl -fsS -X POST 'https://your-host/v1/runs' \
-H 'X-API-Key: rv_…' -H 'Content-Type: application/json' \
-d '{"query":"Compare MySQL and PostgreSQL",
"models":[{"provider":"openrouter","model":"openai/gpt-5.5"}]}'
curl -fsS 'https://your-host/v1/runs/123/result' -H 'X-API-Key: rv_…'
# Same API through the OpenAI / Anthropic dialect (?format=openai|anthropic).
curl -fsS -X POST 'https://your-host/v1/runs?format=openai' \
-H 'X-API-Key: rv_…' -H 'Content-Type: application/json' \
-d '{"model":"openai/gpt-5.5","provider":"openrouter",
"messages":[{"role":"user","content":"Hello"}],"async":false}'Full reference (launch modes sync/202/SSE-stream, read endpoints,
errors, HMAC-signed webhooks with retries, examples):
docs/api-rest.md.
Set your API keys in config.php or create key files:
echo 'your-bothub-key' > .bothub.key
echo 'your-openrouter-key' > .openrouter.key
# Direct providers (v1.8.15+) — flip connectors.{provider}.active=true
# in config.php first, then drop the matching key file. Alias names
# (.claude.key, .gemini.key, .google.key, .grok.key) and per-model
# files (.gpt-5.5.key, .claude-sonnet-4.6.key) are also recognised.
echo 'sk-...' > .openai.key
echo 'sk-ant-...' > .anthropic.key # or .claude.key
echo 'AIza...' > .gemini.key # or .google.key
echo 'sk-...' > .deepseek.key
echo 'xai-...' > .xai.key # or .grok.keyThe shipped model list lives in three files under config/models/:
config/models/openrouter.php— top 10 OpenRouter slugs (latest GPT, Claude, Gemini, DeepSeek, Grok, Qwen).config/models/bothub.php— top 10 BotHub slugs (no vendor prefix).config/models/direct.php— top 5 direct providers (OpenAI, Anthropic, Google/Gemini, DeepSeek, xAI/Grok).
Each row carries provider, model, label, rank, category,
active, and default. Config::load() walks the model_files key
in config.php and merges these files into the runtime models
list. An operator-supplied models list (in config.php,
.config.php, or runtime.json) wins over the shipped catalogue —
all existing deployments keep working unchanged.
TECHNICAL_SPECIFICATION.md— Full architecture and requirementsSKILL.md— Perplexity Computer skill for AI-assisted developmentsrc/Connectors/HOW_TO_CREATE_CONNECTOR.md— Guide to adding new LLM providerssrc/[Module]/[Module].md— Per-module documentation for each component
See TECHNICAL_SPECIFICATION.md Section 2 for the full directory tree.
RevoAp has no api/ directory on disk. Every URL beginning with
/api/... is a virtual route registered with the in-process router
(src/Core/Router.php) and dispatched by the front controller index.php.
Your web server must therefore forward every request that does not match
a real file or directory to index.php.
The bundled .htaccess already does this:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]Make sure AllowOverride All (or AllowOverride FileInfo) is set for the
project directory in your vhost so .htaccess is honoured, and that
mod_rewrite is loaded (a2enmod rewrite on Debian/Ubuntu).
server {
listen 443 ssl http2;
server_name bank.revoap.com;
root /var/www/revoap;
index index.php;
# Static assets
location /assets/ {
try_files $uri =404;
access_log off;
expires 1h;
}
# Front-controller fallback — every non-existent path goes through index.php.
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
# PHP execution (split path-info so /index.php/api/... still works).
location ~ ^/index\.php(/|$) {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
# Block direct access to sensitive files.
location ~* /\.(env|.+\.key)$ { deny all; }
location ~* /(config\.php|composer\.(json|lock))$ { deny all; }
}If /api/health works but /api/council/session/<id> 404s, routing is
healthy and the issue is application-level (typically the in-memory
storage mode, see Debug below).
RevoAp is a single-front-controller PHP app. After pulling a new version on the server, you must clear caches that may still reference the previous build, otherwise stale code or stale browser assets will mask the fix:
git pull --ff-only
# Reload PHP-FPM so the new sources are read (OPcache reset).
sudo systemctl reload php8.4-fpm # or: kill -USR2 $(pgrep -f php-fpm)
# If OPcache is not invalidated by reload (validate_timestamps=0):
php -r 'opcache_reset();' || true
# Hard-refresh the browser (Ctrl+Shift+R / Cmd+Shift+R) — the asset URLs
# now include ?v={app_version}.{mtime} so a new deploy auto-busts caches,
# but a tab opened before the deploy can still hold the old JS module./api/debug returns app.build_id (the short git SHA when .git/ is
present, otherwise the asset-mtime token). Compare it with git rev-parse --short=12 HEAD on the server to confirm the deployed code matches the
commit you expect:
curl -fsS https://your-host/api/debug | jq .app
# { "name": "RevoAp", "version": "1.0.4", "debug": false, "build_id": "abcdef012345" }If app.build_id is older than HEAD, your reload/OPcache reset did not
take effect. Confirm L1 routes through the available provider (typically
openrouter when no bothub key is configured):
curl -fsS -X POST 'https://your-host/api/council/run?debug=1' \
-H 'Content-Type: application/json' \
-d '{"query":"hello"}' \
| jq '.debug.trace[] | select(.type=="L1_ROLE_ASSIGNED") | .payload.provider'The session response also exposes the per-LLM cost-and-token breakdown
under stats.per_model. Each model row includes prompt_tokens,
completion_tokens, total_tokens, cached_tokens, reasoning_tokens,
calls, latency_ms, avg_latency_ms, cost, cost_available and
tokens_estimated. The UI renders the same data as a "Detailed per-LLM
breakdown" table beneath the session summary.
curl -fsS -X POST 'https://your-host/api/council/run' \
-H 'Content-Type: application/json' \
-d '{"query":"hello"}' \
| jq '.stats | {total_tokens, total_cost, cost_available, per_model}'If the response still shows the synthesis model as synthesis / provider
—, or L1 still references bothub despite bothub.api_key being empty,
the deployed build is older than commit bc4f792 (L1 silent-skip fix) and
d8e6a70 (per-step Statistics fix). Repeat the steps above — all three
caches (OPcache, in-memory routes, browser modules) must be cleared
together.
Two equivalent triggers expose a sanitized diagnostic snapshot:
GET /api/debug— dedicated route, always on.?debug=1(or?DEBUG=1,?debug) — query-string override; works on any URL, includingPOST /api/council/run.
Setting app.debug=true in config.php enables debug output on every
request without the query parameter, plus per-request structured logs to
logs/revoap-debug.log. The diagnostic payload contains:
app,request.resolved_path(post-normalization),database.{configured,connected,storage_mode,error_class,error_message}—storage_modeismemorywhen no DSN is configured or the connection failed,persistentotherwise,connectors.{discovered,available,skipped,detailed[].reason}— every unavailable connector explains why (key missing / inactive / not configured),pipeline.active_models[].available— which active models point at a provider whose connector is currently usable,pipeline.stage_plan— the L0–L4 layers as a static plan,hints— actionable next steps including the silent-skip detector: if every active model belongs to an unavailable provider, this hint fires.
POST /api/council/run with debug enabled also returns a debug
envelope containing the pipeline trace (L1_MODEL_SELECTION,
L1_ROLE_ASSIGNED, L4_SELECTED/L4_FAILED) and a snapshot of the
diagnostic report. GET /api/council/session/{id} returns a debug
section explaining storage mode and the last DB error class on 404.
API keys, DSN passwords and absolute filesystem paths are never written
to the response or to the log file — every payload is passed through
Logger::sanitize() first.
User Query
↓
[L0] Skill Router → Auto-create Skill if needed
↓
[L1] Distribution → Parallel calls to all roles across models
↓
[L2] Normalization → Internal: reshape each L1 answer into discrete claims
(no LLM call; output is "Normalized answer from role X
into N claims for consensus")
↓
[L3] Consensus → Agreement scoring + conflict detection
↓
[L4] Synthesis → Final answer + full audit trail
L1 and L4 step cards render the complete model response in the Response
tab — the historical 240-character preview was promoted to the full content
in v1.0.4 / persisted as MEDIUMTEXT (council_steps.summary in db/schema.sql).
A short preview is still kept under meta.summary_preview for compact list
views.
Provider catalogs change. A retired model id returns HTTP 404 ("No endpoints
found" on OpenRouter); since v1.0.5 the L1_FAILED event includes a hint
pointing at the provider's /models endpoint. The default config.php
ships only model ids verified at release time — see
docs/Connectors.md for the
verification commands and the current default list.
PolyForm Noncommercial 1.0.0