Skip to content

Commit 12a4569

Browse files
lgarceau768claude
andcommitted
feat: rebase Flexion customizations onto v0.8.10
Applies all Flexion-specific changes from flex (v0.8.0 base) onto upstream v0.8.10. Includes Google Cloud Identity OAuth group-based roles, provider model icons, FlexChat branding, custom functions, and documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e4e69a1 commit 12a4569

27 files changed

Lines changed: 2289 additions & 66 deletions

README_FLEXION.md

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# FlexChat - Flexion's Open WebUI Fork
2+
3+
FlexChat is Flexion's customized deployment of [Open WebUI](https://github.com/open-webui/open-webui), rebranded and configured to integrate with AWS Bedrock via the [Flexion Bedrock Access Gateway](https://github.com/flexion/bedrock-access-gateway).
4+
5+
## Branch Strategy
6+
7+
| Branch | Purpose |
8+
|--------|---------|
9+
| `flex` | **Flexion customizations** - Contains all Flexion-specific branding, configurations, and features. This is the primary branch for Flexion development. |
10+
| `main` | Mirrors the upstream Open WebUI `main` branch. Used for tracking upstream releases. |
11+
| `dev` | Mirrors the upstream Open WebUI `dev` branch. Used for tracking upstream development. |
12+
13+
### Keeping Up with Upstream
14+
15+
To incorporate upstream Open WebUI updates into our `flex` branch:
16+
17+
```bash
18+
# Add upstream remote (one-time setup)
19+
git remote add upstream https://github.com/open-webui/open-webui.git
20+
```
21+
22+
## Local Development Setup
23+
24+
### Prerequisites
25+
26+
- Docker and Docker Compose
27+
- AWS credentials configured (for Bedrock Access Gateway)
28+
- [Flexion Bedrock Access Gateway](https://github.com/flexion/bedrock-access-gateway) running locally (or live connection)
29+
30+
### Architecture Overview
31+
32+
```
33+
┌─────────────────┐ ┌─────────────────────────┐ ┌─────────────────┐
34+
│ │ │ │ │ │
35+
│ FlexChat │────▶│ Bedrock Access Gateway │────▶│ AWS Bedrock │
36+
│ (Port 3000) │ │ (Port 8000) │ │ │
37+
│ │ │ │ │ │
38+
└─────────────────┘ └─────────────────────────┘ └─────────────────┘
39+
```
40+
41+
FlexChat connects to the Bedrock Access Gateway (BAG) which provides an OpenAI-compatible API facade for Amazon Bedrock models.
42+
43+
### Step 1: Start the Bedrock Access Gateway
44+
45+
Before running FlexChat, you need the Bedrock Access Gateway running locally. See the [BAG README_FLEXION.md](https://github.com/flexion/bedrock-access-gateway/blob/main/README_FLEXION.md) for detailed setup instructions.
46+
47+
Quick start for BAG:
48+
49+
```bash
50+
# Clone the Bedrock Access Gateway repo
51+
git clone https://github.com/flexion/bedrock-access-gateway.git
52+
cd bedrock-access-gateway
53+
54+
# Set up virtual environment
55+
python3 -m venv .venv && source .venv/bin/activate
56+
pip install -r src/requirements.txt
57+
58+
# Configure AWS and start the gateway
59+
export AWS_REGION=us-east-1
60+
export API_KEY=bedrock
61+
export ALLOWED_MODEL_IDS='["anthropic.*", "us.anthropic.*", "us.meta.*"]'
62+
63+
# Run on port 8000
64+
uvicorn api.app:app --host 0.0.0.0 --port 8000
65+
```
66+
67+
### Step 2: Configure FlexChat Environment
68+
69+
Create or update your `.env` file in the FlexChat root directory:
70+
71+
```bash
72+
# Core Settings
73+
ENV=dev
74+
WEBUI_AUTH=FALSE
75+
ENABLE_LOGIN_FORM=false
76+
77+
# Model Configuration
78+
BYPASS_MODEL_ACCESS_CONTROL=true
79+
DEFAULT_MODELS=us.meta.llama3-1-8b-instruct-v1:0
80+
81+
# Bedrock Access Gateway Connection
82+
# Points to the locally running BAG instance
83+
OPENAI_API_BASE_URL=http://host.docker.interal:8000/api/v1
84+
OPENAI_API_KEY=bedrock
85+
86+
# Disable Ollama (we're using Bedrock)
87+
ENABLE_OLLAMA_API=false
88+
89+
# User Settings
90+
DEFAULT_USER_ROLE=user
91+
ENABLE_API_KEYS=true
92+
USER_PERMISSIONS_FEATURES_API_KEYS=true
93+
94+
95+
WEBUI_AUTH=false
96+
```
97+
98+
### Step 3: Run FlexChat with Docker Compose
99+
100+
```bash
101+
# Build and start FlexChat
102+
docker-compose up --build
103+
104+
# Or run in detached mode
105+
docker-compose up -d --build
106+
```
107+
108+
FlexChat will be available at `http://localhost:3000`.
109+
110+
### Docker Compose Configuration
111+
112+
The `docker-compose.yaml` is configured to:
113+
114+
- Build FlexChat from the local Dockerfile
115+
- Mount a persistent volume for data storage
116+
- Load environment variables from `.env`
117+
- Expose local port services like the BAG
118+
- Add `host.docker.internal` for accessing host services (like the BAG)
119+
120+
```yaml
121+
services:
122+
open-webui:
123+
build:
124+
context: .
125+
dockerfile: Dockerfile
126+
container_name: open-webui
127+
volumes:
128+
- open-webui:/app/backend/data
129+
network_mode: 'host'
130+
env_file:
131+
- .env
132+
extra_hosts:
133+
- host.docker.internal:host-gateway
134+
restart: unless-stopped
135+
```
136+
137+
**Note:** The Ollama service is included in the compose file but is not required when using Bedrock. You can remove or comment out the Ollama service and its dependency if desired.
138+
139+
## Connecting to Bedrock Access Gateway
140+
141+
The `OPENAI_API_BASE_URL` environment variable is set to `http://host.docker.internal:8000/api/v1` to connect FlexChat to the locally running Bedrock Access Gateway.
142+
143+
### Important Notes
144+
145+
- **API Key:** The `OPENAI_API_KEY=bedrock` matches the default API key used by the BAG in local development mode.
146+
- **Available Models:** The models available in FlexChat depend on the `ALLOWED_MODEL_IDS` configured in the Bedrock Access Gateway.
147+
148+
### Updating OPENAI_API_BASE_URL for Docker
149+
150+
If FlexChat is running in Docker and BAG is running on your host:
151+
152+
```bash
153+
# In .env, use host.docker.internal for Docker-to-host communication
154+
OPENAI_API_BASE_URL=http://host.docker.internal:8000/api/v1
155+
```
156+
157+
## Flexion Customizations
158+
159+
The `flex` branch includes the following Flexion-specific changes:
160+
161+
### Branding
162+
- Application name changed from "Open WebUI" to "FlexChat"
163+
- Custom Flexion logo used for favicons and splash screens
164+
- Updated site manifest and HTML title
165+
166+
### Configuration Defaults
167+
- Default integration with Bedrock Access Gateway
168+
- Ollama disabled by default
169+
- Google OAuth pre-configured (credentials required)
170+
171+
## Troubleshooting
172+
173+
### FlexChat can't connect to models
174+
175+
1. Verify the Bedrock Access Gateway is running on port 8000
176+
2. Check that `OPENAI_API_BASE_URL` is correctly set
177+
3. If using Docker, try using `0.0.0.0` and setting `network_mode: 'host'`
178+
179+
### No models appearing in the UI
180+
181+
1. Check BAG logs for any authentication errors
182+
2. Verify your AWS credentials have Bedrock invoke permissions
183+
3. Confirm `ALLOWED_MODEL_IDS` in BAG includes the models you expect
184+
185+
### Docker build fails
186+
187+
1. Ensure Docker has sufficient resources allocated
188+
2. Try clearing Docker cache: `docker-compose build --no-cache`
189+
190+
## Related Documentation
191+
192+
- [Open WebUI Documentation](https://docs.openwebui.com/)
193+
- [Flexion Bedrock Access Gateway](https://github.com/flexion/bedrock-access-gateway/blob/main/README_FLEXION.md)
194+
- [Amazon Bedrock Documentation](https://docs.aws.amazon.com/bedrock/)

backend/open_webui/routers/models.py

Lines changed: 101 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Optional
22
import io
3+
import os
34
import base64
45
import json
56
import asyncio
@@ -42,6 +43,73 @@
4243

4344
router = APIRouter()
4445

46+
# Provider icon configuration is loaded from providers.json for easier maintenance
47+
# See static/static/providers/providers.json for the configuration
48+
_PROVIDER_CONFIG = None
49+
_PROVIDER_CONFIG_PATH = f"{STATIC_DIR}/providers/providers.json"
50+
51+
52+
def _load_provider_config() -> dict:
53+
"""Load provider icon configuration from JSON file."""
54+
global _PROVIDER_CONFIG
55+
if _PROVIDER_CONFIG is None:
56+
try:
57+
with open(_PROVIDER_CONFIG_PATH, "r") as f:
58+
_PROVIDER_CONFIG = json.load(f)
59+
except Exception as e:
60+
log.warning(f"Failed to load provider config: {e}")
61+
_PROVIDER_CONFIG = {
62+
"providers": {},
63+
"region_prefixes": [],
64+
"model_prefix_aliases": {},
65+
}
66+
return _PROVIDER_CONFIG
67+
68+
69+
def get_provider_from_model_id(model_id: str) -> Optional[str]:
70+
"""
71+
Extract the provider name from a model ID.
72+
73+
Examples:
74+
'anthropic.claude-3-sonnet' → 'anthropic'
75+
'us.anthropic.claude-3-sonnet' → 'anthropic'
76+
'gpt-4o' → 'openai' (via alias)
77+
"""
78+
config = _load_provider_config()
79+
model_id_lower = model_id.lower()
80+
81+
# Check for special aliases first (e.g., 'gpt-' → 'openai')
82+
for alias, provider in config.get("model_prefix_aliases", {}).items():
83+
if model_id_lower.startswith(alias.lower()):
84+
return provider
85+
86+
# Strip known region prefixes (us., eu., global., apac.)
87+
for prefix in config.get("region_prefixes", []):
88+
if model_id_lower.startswith(prefix.lower()):
89+
model_id_lower = model_id_lower[len(prefix):]
90+
break
91+
92+
# Extract provider from first segment (before the first dot)
93+
if "." in model_id_lower:
94+
return model_id_lower.split(".")[0]
95+
96+
return None
97+
98+
99+
def get_provider_icon_path(model_id: str) -> Optional[str]:
100+
"""
101+
Returns the path to a provider-specific icon based on model ID.
102+
Returns None if no matching provider icon is found.
103+
"""
104+
config = _load_provider_config()
105+
provider = get_provider_from_model_id(model_id)
106+
107+
if provider and provider in config.get("providers", {}):
108+
icon_filename = config["providers"][provider]
109+
return f"{STATIC_DIR}/providers/{icon_filename}"
110+
111+
return None
112+
45113

46114
def is_valid_model_id(model_id: str) -> bool:
47115
return model_id and len(model_id) <= 256
@@ -393,32 +461,43 @@ def get_model_profile_image(id: str, user=Depends(get_verified_user)):
393461
etag = f'"{model.updated_at}"' if model.updated_at else None
394462

395463
if model.meta.profile_image_url:
396-
if model.meta.profile_image_url.startswith("http"):
397-
return Response(
398-
status_code=status.HTTP_302_FOUND,
399-
headers={"Location": model.meta.profile_image_url},
400-
)
401-
elif model.meta.profile_image_url.startswith("data:image"):
402-
try:
403-
header, base64_data = model.meta.profile_image_url.split(",", 1)
404-
image_data = base64.b64decode(base64_data)
405-
image_buffer = io.BytesIO(image_data)
406-
media_type = header.split(";")[0].lstrip("data:")
407-
408-
headers = {"Content-Disposition": "inline"}
409-
if etag:
410-
headers["ETag"] = etag
411-
412-
return StreamingResponse(
413-
image_buffer,
414-
media_type=media_type,
415-
headers=headers,
464+
# Check if it's NOT the default favicon (custom image was set)
465+
if not model.meta.profile_image_url.endswith("/favicon.png"):
466+
if model.meta.profile_image_url.startswith("http"):
467+
return Response(
468+
status_code=status.HTTP_302_FOUND,
469+
headers={"Location": model.meta.profile_image_url},
416470
)
417-
except Exception as e:
418-
pass
471+
elif model.meta.profile_image_url.startswith("data:image"):
472+
try:
473+
header, base64_data = model.meta.profile_image_url.split(",", 1)
474+
image_data = base64.b64decode(base64_data)
475+
image_buffer = io.BytesIO(image_data)
476+
media_type = header.split(";")[0].lstrip("data:")
477+
478+
headers = {"Content-Disposition": "inline"}
479+
if etag:
480+
headers["ETag"] = etag
481+
482+
return StreamingResponse(
483+
image_buffer,
484+
media_type=media_type,
485+
headers=headers,
486+
)
487+
except Exception as e:
488+
pass
489+
490+
# Try to find a provider-specific icon based on model ID
491+
provider_icon_path = get_provider_icon_path(id)
492+
if provider_icon_path and os.path.exists(provider_icon_path):
493+
return FileResponse(provider_icon_path, media_type="image/png")
419494

420495
return FileResponse(f"{STATIC_DIR}/favicon.png")
421496
else:
497+
# Model not found in database - check provider icon based on ID
498+
provider_icon_path = get_provider_icon_path(id)
499+
if provider_icon_path and os.path.exists(provider_icon_path):
500+
return FileResponse(provider_icon_path, media_type="image/png")
422501
return FileResponse(f"{STATIC_DIR}/favicon.png")
423502

424503

0 commit comments

Comments
 (0)