Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5283853
feat(openai): OpenAI-compatible protocol DTOs and configuration
ginccc Jul 27, 2026
2575f0e
Merge origin/main into feat/openai-api-adapter
ginccc Jul 27, 2026
9cf41af
feat(openai): expose deployed agents via GET /v1/models, with authent…
ginccc Jul 27, 2026
653be68
feat(openai): map OpenAI messages and image content to EDDI InputData
ginccc Jul 27, 2026
1e37f1c
feat(openai): stateful chat completions, sync and streaming
ginccc Jul 27, 2026
3b384d9
docs(openai): Open WebUI integration guide, changelog and roadmap entry
ginccc Jul 27, 2026
5b8c471
fix(openai): six defects found in self-review of the adapter
ginccc Jul 27, 2026
8046183
feat(openai): accept PDF/document and audio attachments, not just images
ginccc Jul 28, 2026
16165cd
feat(openai): accept 'stateless' as a request body field, not only a …
ginccc Jul 28, 2026
54d4813
docs(openai): recipe for using EDDI as a thin LLM gateway
ginccc Jul 28, 2026
0ae2939
test(openai): HTTP-level integration test for the stream dispatch
ginccc Jul 28, 2026
2c43db5
fix(docs): stop gitleaks flagging the adapter's curl examples
ginccc Jul 28, 2026
36e7418
chore(security): suppress the adapter docs' placeholder-credential fi…
ginccc Jul 28, 2026
3ff4fa8
chore(security): stop the ignore file's own comment tripping the scanner
ginccc Jul 28, 2026
b0a8f6a
fix(openai): wait for the streaming turn before closing the response
ginccc Jul 28, 2026
a68f9d1
feat(openai): runnable Open WebUI demo, and two defects it caught
ginccc Jul 28, 2026
d72d5ab
fix(openai): make the demo agent demonstrate state, and stop Open Web…
ginccc Jul 28, 2026
a73e577
docs(openai): correct where Open WebUI injects RAG context, and cover…
ginccc Jul 28, 2026
fc43606
feat(openai): document the builtin_tools injection, add an optional L…
ginccc Jul 28, 2026
09a55b3
feat(openai): hold the demo LLM key in the Secrets Vault, not in the …
ginccc Jul 28, 2026
84606fa
docs(openai): explain the client-side tool gap instead of just instru…
ginccc Jul 28, 2026
384bdd1
feat(openai): render structured outputs and report token usage
ginccc Jul 28, 2026
9075010
Merge remote-tracking branch 'origin/main' into feat/openai-api-adapter
ginccc Jul 28, 2026
a8501b1
docs(openai): explain why quick replies round-trip, not just that the…
ginccc Jul 28, 2026
05f893f
fix(openai): sanitize logged intent, drop the regex CodeQL flags as R…
ginccc Jul 28, 2026
34b9ae6
docs(openai): justify the auth-filter CodeQL dismissal, and pin it wi…
ginccc Jul 28, 2026
2c543db
Merge remote-tracking branch 'origin/main' into feat/openai-api-adapter
ginccc Jul 29, 2026
8e35aa0
docs(changelog): restore date order after the main merge
ginccc Jul 29, 2026
0705fdb
Merge remote-tracking branch 'origin/main' into feat/openai-api-adapter
ginccc Jul 29, 2026
66ec461
fix(demo): make the Open WebUI seeder correct on a second run
ginccc Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,31 @@ MONGO_PORT=27017
# QUARKUS_OIDC_TENANT_ENABLED=true
# QUARKUS_OIDC_AUTH_SERVER_URL=http://keycloak:8080/realms/eddi
# QUARKUS_OIDC_CLIENT_ID=eddi-backend

# ──────────────────────────────────────────────────
# Open WebUI demo — docker-compose.openwebui.yml only
# See docs/open-webui-integration.md §1. Every value below has a working
# default, so the demo runs with none of them set.
# ──────────────────────────────────────────────────

# Shared bearer secret between Open WebUI and EDDI's /v1 adapter.
# EDDI_OPENAI_COMPAT_API_KEY=sk-eddi-demo

# Host ports. Override when something already owns 7070 or 3000 — otherwise
# Docker fails with "Bind for 0.0.0.0:7070 failed: port is already allocated".
# EDDI_PORT and OPEN_WEBUI_PORT change only the HOST side; the containers keep
# talking to each other on 7070/8080.
# OPEN_WEBUI_PORT=3000

# Optional LLM-backed demo agent. Without a key the demo still runs, but its
# agent is rule-based and cannot answer questions about anything.
#
# The seeder puts this in EDDI's Secrets Vault and writes only
# ${vault:demo-llm-api-key} into the agent config — so it needs
# EDDI_VAULT_MASTER_KEY set above (the compose file supplies a demo default).
#
# Safe to add later: re-running the stack adds the LLM agent without disturbing
# the rule-based one, and changing the key here refreshes the vault entry.
# EDDI_DEMO_LLM_API_KEY=sk-...
# EDDI_DEMO_LLM_TYPE=openai
# EDDI_DEMO_LLM_MODEL=gpt-4o-mini
21 changes: 21 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,24 @@
06ef33aafb04c721429fef1998eb4727308bae7b:src/test/java/ai/labs/eddi/secrets/rest/RestSecretStoreTest.java:generic-api-key:423
06ef33aafb04c721429fef1998eb4727308bae7b:src/test/java/ai/labs/eddi/secrets/rest/RestSecretStoreTest.java:generic-api-key:427
06ef33aafb04c721429fef1998eb4727308bae7b:src/test/java/ai/labs/eddi/secrets/rest/RestSecretStoreTest.java:generic-api-key:445

# OpenAI-compatible adapter docs — placeholder credentials in examples, never real.
# Gitleaks scans the PR's commit history, so fixing the working tree in a later
# commit cannot clear a finding from an earlier one; these entries cover the
# commits that introduced them. The examples themselves have since been changed
# to use $EDDI_API_KEY / os.environ so no future commit re-triggers the rule.
#
# docs/open-webui-integration.md: two curl examples that passed a placeholder
# bearer token inline. (Not quoted here -- the rule matches this file too.)
3b384d99d6c6a5ef652a4294618fa9df16b753df:docs/open-webui-integration.md:curl-auth-header:56
3b384d99d6c6a5ef652a4294618fa9df16b753df:docs/open-webui-integration.md:curl-auth-header:60
# planning/openai-api-adapter-plan.md: the same two curl examples, plus a
# generic-api-key hit on the literal "<api-key>" placeholder in an ASCII
# request diagram.
5283853e511b20937a13ab8a9dc13698d3ef6496:planning/openai-api-adapter-plan.md:generic-api-key:138
5283853e511b20937a13ab8a9dc13698d3ef6496:planning/openai-api-adapter-plan.md:curl-auth-header:791
5283853e511b20937a13ab8a9dc13698d3ef6496:planning/openai-api-adapter-plan.md:curl-auth-header:795
# The justification comment above originally quoted the offending example
# verbatim, so the rule matched this file too. The quote is gone now, but the
# commit that carried it stays in history and still needs an entry.
36e741846f2fcf3e25aa3bcb6efca0feeba69dd0:.gitleaksignore:curl-auth-header:38
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ Follow this order unless the user explicitly requests something different.
| — | Test Coverage | 12,000+ tests, >90% instruction / >80% branch coverage, OpenSSF Gold compliance |
| — | Security Hardening v6.0.2 | SSRF prevention, SafeHttpClient, auth guard, vault salt, security headers, CodeQL + Trivy CI |
| 9b | HITL Framework | Two human-approval gates (turn-level `PAUSE_CONVERSATION` + per-tool-call gating), timeout/no-progress policies, audit ledger, Slack + MCP approval surfaces, crash recovery — see [`docs/hitl.md`](docs/hitl.md) |
| — | OpenAI-Compatible API | `/v1` adapter presenting deployed agents as OpenAI models for Open WebUI and OpenAI SDK clients; per-chat conversation isolation, streaming, multimodal, HITL-aware — see [`docs/open-webui-integration.md`](docs/open-webui-integration.md) |

### In Progress / Upcoming

Expand Down Expand Up @@ -921,7 +922,7 @@ Always use v6 canonical URIs in new configs:
| `eddi://ai.labs.property` | `eddi://ai.labs.property/...` | Optional — slot-filling |
| `eddi://ai.labs.httpcalls` | `eddi://ai.labs.apicalls/...` | Optional — API calls |
| `eddi://ai.labs.output` | `eddi://ai.labs.output/...` | Usually yes — user messages |
| `eddi://ai.labs.langchain` | `eddi://ai.labs.llm/...` | Optional — LLM interaction |
| `eddi://ai.labs.llm` | `eddi://ai.labs.llm/...` | Optional — LLM interaction |

### 5.6 Reference Implementation

Expand Down
153 changes: 153 additions & 0 deletions docker-compose.openwebui.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# ─────────────────────────────────────────────────────────────────────────────
# EDDI + Open WebUI — runnable demo of the OpenAI-compatible adapter (/v1)
#
# docker compose -f docker-compose.openwebui.yml up --build
#
# Then open http://localhost:3000 and pick "agent-father-…" from the model list.
#
# EDDI is BUILT FROM THIS WORKING TREE, not pulled from Docker Hub: the adapter
# is not in any published image yet, so `labsai/eddi:latest` would start fine
# and then 404 on /v1. Dockerfile.demo compiles inside the container, so no
# local JDK or Maven is needed — the first build is slow, later ones are cached.
#
# Self-contained rather than an overlay on docker-compose.yml, so it is one
# file to read and one command to run, and cannot break on compose's
# environment-merge semantics. It therefore repeats the MongoDB definition.
#
# NOT A PRODUCTION CONFIGURATION. See docs/open-webui-integration.md §4 for the
# security model; the demo-only choices are called out inline below.
# ─────────────────────────────────────────────────────────────────────────────

services:
eddi:
build:
context: .
dockerfile: src/main/docker/Dockerfile.demo
environment:
- "MONGODB_CONNECTIONSTRING=mongodb://eddi:changeme@mongodb:27017/eddi?authSource=admin&retryWrites=true&w=majority&connectTimeoutMS=10000&socketTimeoutMS=30000"
# DEMO ONLY: no OIDC, so EDDI's own REST API is wide open. This is what
# lets the seeder below call the (normally role-gated) import endpoint.
- "EDDI_SECURITY_ALLOW_UNAUTHENTICATED=true"

# ── The OpenAI-compatible adapter ──
- "EDDI_OPENAI_COMPAT_ENABLED=true"
- "EDDI_OPENAI_COMPAT_API_KEY=${EDDI_OPENAI_COMPAT_API_KEY:-sk-eddi-demo}"
# DEMO ONLY. Open WebUI runs here with authentication disabled, so it may
# not forward X-OpenWebUI-User-Id; without this the adapter would refuse
# the request rather than guess an identity. In a real multi-user
# deployment leave this false — every anonymous caller would otherwise
# share one conversation, and its memory.
- "EDDI_OPENAI_COMPAT_ALLOW_ANONYMOUS=true"

# Secrets Vault — required for the optional LLM agent, whose provider key
# is stored here and referenced from the agent config as ${vault:...}
# rather than written into it.
#
# DEMO ONLY, obviously: a master key committed to a compose file protects
# nothing. Generate a real one per deployment: openssl rand -base64 32
- "EDDI_VAULT_MASTER_KEY=${EDDI_VAULT_MASTER_KEY:-demo-only-vault-master-key-not-for-production-use}"
ports:
- "${EDDI_PORT:-7070}:7070"
depends_on:
mongodb:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:7070/q/health/ready"]
interval: 10s
timeout: 5s
retries: 12
start_period: 30s

mongodb:
image: mongo:7.0.14
environment:
- "MONGO_INITDB_ROOT_USERNAME=eddi"
- "MONGO_INITDB_ROOT_PASSWORD=changeme"
volumes:
- openwebui-demo-mongodb:/data/db
healthcheck:
test: ["CMD", "mongosh", "-u", "eddi", "-p", "changeme", "--authenticationDatabase", "admin", "--eval", "db.adminCommand('ping')"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s

# One-shot: creates and deploys a small rule-based agent so the model list is
# not empty on first start. EDDI seeds nothing automatically.
#
# It does NOT use POST /backup/import/initialAgents — see the note in
# seed-demo-agent.sh for why that path cannot import the bundled Agent Father
# on Linux today.
eddi-seed:
image: curlimages/curl:8.11.1
restart: "no"
depends_on:
eddi:
condition: service_healthy
environment:
- "EDDI_URL=http://eddi:7070"
- "EDDI_API_KEY=${EDDI_OPENAI_COMPAT_API_KEY:-sk-eddi-demo}"
# So the closing "open this URL" line is right when the ports are remapped.
- "OPEN_WEBUI_URL=http://localhost:${OPEN_WEBUI_PORT:-3000}"
# Optional. The rule-based agent proves the transport and the state
# bridge, but it has no model, so it cannot answer questions ABOUT
# anything — an uploaded PDF included. Set this and a second, LLM-backed
# agent is deployed alongside it:
# EDDI_DEMO_LLM_API_KEY=sk-... docker compose -f docker-compose.openwebui.yml up
- "EDDI_DEMO_LLM_API_KEY=${EDDI_DEMO_LLM_API_KEY:-}"
- "EDDI_DEMO_LLM_TYPE=${EDDI_DEMO_LLM_TYPE:-openai}"
- "EDDI_DEMO_LLM_MODEL=${EDDI_DEMO_LLM_MODEL:-gpt-4o-mini}"
volumes:
- ./src/main/docker/seed-demo-agent.sh:/seed.sh:ro
entrypoint: ["/bin/sh", "/seed.sh"]

open-webui:
image: ghcr.io/open-webui/open-webui:main
ports:
- "${OPEN_WEBUI_PORT:-3000}:8080"
environment:
- "OPENAI_API_BASE_URL=http://eddi:7070/v1"
- "OPENAI_API_KEY=${EDDI_OPENAI_COMPAT_API_KEY:-sk-eddi-demo}"
# Nothing else to talk to; without this the model list mixes in Ollama.
- "ENABLE_OLLAMA_API=false"
# LOAD-BEARING. Supplies X-OpenWebUI-Chat-Id, which is what gives each
# chat window its own EDDI conversation. Without it every chat you open
# against one agent shares a single conversation, and its memory.
- "ENABLE_FORWARD_USER_INFO_HEADERS=true"
# DEMO ONLY: skips account creation so the UI is usable immediately.
- "WEBUI_AUTH=false"
# Open WebUI drives several auxiliary LLM calls of its own — chat titles,
# tags, follow-up suggestions, search queries — and by default they all go
# to the selected model. Against an EDDI agent that means utility prompts
# land in the user's real conversation and advance its behaviour rules,
# and the parsed-out junk shows up in the UI as bogus follow-ups. Turned
# off here so the demo shows the agent and nothing else.
#
# In a real deployment, point these at a separate connection or at an
# "<agent>:stateless" model instead of disabling them — see
# docs/open-webui-integration.md §5.
- "ENABLE_TITLE_GENERATION=false"
- "ENABLE_FOLLOW_UP_GENERATION=false"
- "ENABLE_TAGS_GENERATION=false"
- "ENABLE_AUTOCOMPLETE_GENERATION=false"
- "ENABLE_SEARCH_QUERY_GENERATION=false"
- "ENABLE_RETRIEVAL_QUERY_GENERATION=false"
# LOAD-BEARING for any agent that inspects its input. Open WebUI's RAG
# wraps the retrieved chunks AND its own instruction template around the
# user's message by default (env.py: RAG_SYSTEM_CONTEXT defaults to
# False), so an EDDI agent receives a few thousand tokens of <source>
# markup as {memory.current.input} instead of what the user typed —
# which breaks input matchers, property setters and quick replies alike.
# Setting this puts the context in the system message instead, where the
# adapter maps it to the openai_system_message context entry and the
# agent can use or ignore it deliberately.
- "RAG_SYSTEM_CONTEXT=true"
volumes:
- openwebui-demo-data:/app/backend/data
depends_on:
eddi:
condition: service_healthy

volumes:
openwebui-demo-mongodb:
openwebui-demo-data:
Loading
Loading