Commit f8683a3d authored by Your Name's avatar Your Name

Release 0.99.25

parent 5246c1ae
......@@ -775,7 +775,7 @@ AISBF provides user-specific API endpoints that allow authenticated users to acc
All user-specific endpoints require authentication via Bearer token:
```bash
curl -H "Authorization: Bearer YOUR_USER_TOKEN" http://localhost:17765/api/user/models
curl -H "Authorization: Bearer YOUR_USER_TOKEN" http://localhost:17765/api/u/yourusername/models
```
Generate a user token from the dashboard: **Dashboard > My Account > API Tokens**
......@@ -788,7 +788,7 @@ Returns all models from the user's own providers, rotations, and autoselects:
```bash
# Get all user models
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/models
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/u/yourusername/models
```
Response includes:
......@@ -801,7 +801,7 @@ Response includes:
Returns all user-configured providers:
```bash
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/providers
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/u/yourusername/providers
```
#### List User Rotations
......@@ -809,7 +809,7 @@ curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/provi
Returns all user-configured rotations:
```bash
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/rotations
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/u/yourusername/rotations
```
#### List User Autoselects
......@@ -817,7 +817,7 @@ curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/rotat
Returns all user-configured autoselects:
```bash
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/autoselects
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/u/yourusername/autoselects
```
#### User Chat Completions
......@@ -831,7 +831,7 @@ curl -X POST -H "Authorization: Bearer YOUR_TOKEN" \
"model": "user-rotation/myrotation",
"messages": [{"role": "user", "content": "Hello"}]
}' \
http://localhost:17765/api/user/chat/completions
http://localhost:17765/api/u/yourusername/chat/completions
```
**Model formats for user endpoints:**
......@@ -850,13 +850,13 @@ Get models for a specific user configuration type:
```bash
# Get user provider models
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/providers/models
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/u/yourusername/providers/models
# Get user rotation models
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/rotations/models
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/u/yourusername/rotations/models
# Get user autoselect models
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/autoselects/models
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/u/yourusername/autoselects/models
```
### Python Examples
......@@ -870,16 +870,16 @@ TOKEN = "YOUR_USER_TOKEN"
headers = {"Authorization": f"Bearer {TOKEN}"}
# List user models
response = requests.get(f"{BASE_URL}/api/user/models", headers=headers)
response = requests.get(f"{BASE_URL}/api/u/yourusername/models", headers=headers)
print(response.json())
# List user providers
response = requests.get(f"{BASE_URL}/api/user/providers", headers=headers)
response = requests.get(f"{BASE_URL}/api/u/yourusername/providers", headers=headers)
print(response.json())
# Send chat completion using user rotation
response = requests.post(
f"{BASE_URL}/api/user/chat/completions",
f"{BASE_URL}/api/u/yourusername/chat/completions",
headers=headers,
json={
"model": "user-rotation/myrotation",
......
......@@ -249,12 +249,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- **User-Specific API Endpoints**: New API endpoints for authenticated users to access their own configurations
- `GET /api/user/models` - List user's own models
- `GET /api/user/providers` - List user's provider configurations
- `GET /api/user/rotations` - List user's rotation configurations
- `GET /api/user/autoselects` - List user's autoselect configurations
- `POST /api/user/chat/completions` - Chat completions using user's own models
- `GET /api/user/{config_type}/models` - List models for specific config type
- `GET /api/u/{username}/models` - List user's own models
- `GET /api/u/{username}/providers` - List user's provider configurations
- `GET /api/u/{username}/rotations` - List user's rotation configurations
- `GET /api/u/{username}/autoselects` - List user's autoselect configurations
- `POST /api/u/{username}/chat/completions` - Chat completions using user's own models
- `GET /api/u/{username}/{config_type}/models` - List models for specific config type
- Requires Bearer token or query parameter authentication
- Admin users get access to global + user configs, regular users get user-only configs
- Global tokens (in aisbf.json) have full access to all configurations
......
......@@ -131,9 +131,22 @@ pip install stem
Edit `/etc/tor/torrc` (or `~/.torrc` on macOS):
```
ControlPort 9051
CookieAuthentication 1
HashedControlPassword 16:YOUR_HASHED_PASSWORD_HERE
```
Generate a hashed password:
```bash
tor --hash-password "your_secure_password"
```
**For persistent hidden services, also add:**
```
HiddenServiceDir /home/yourusername/.aisbf/tor_hidden_service
HiddenServicePort 80 127.0.0.1:17765
```
**Important:** Replace `/home/yourusername` with your actual home directory path. Tor does NOT expand `~` in torrc - you must use absolute paths.
Restart TOR:
```bash
sudo systemctl restart tor # Linux
......@@ -151,8 +164,8 @@ TOR hidden service can be configured via the dashboard or configuration file.
"enabled": true,
"control_port": 9051,
"control_host": "127.0.0.1",
"control_password": null,
"hidden_service_dir": null,
"control_password": "your_secure_password",
"hidden_service_dir": "~/.aisbf/tor_hidden_service",
"hidden_service_port": 80,
"socks_port": 9050,
"socks_host": "127.0.0.1"
......@@ -160,6 +173,8 @@ TOR hidden service can be configured via the dashboard or configuration file.
}
```
**Important:** Set `control_password` to match the password you used when generating the HashedControlPassword for torrc.
**Configuration Options:**
- **`enabled`**: Enable/disable TOR hidden service (default: false)
......@@ -186,18 +201,27 @@ TOR hidden service can be configured via the dashboard or configuration file.
- Keys stored in specified directory
- Ideal for production use
- Set `hidden_service_dir` to a path (e.g., `~/.aisbf/tor_hidden_service`)
- **Must be manually configured in torrc** - see configuration instructions above
Example persistent configuration:
```json
{
"tor": {
"enabled": true,
"control_password": "your_secure_password",
"hidden_service_dir": "~/.aisbf/tor_hidden_service",
"hidden_service_port": 80
}
}
```
When you start aisbf with a persistent hidden service directory configured:
1. It checks if the hidden service already exists (looks for hostname file)
2. If found, it uses the existing onion address
3. If not found, it displays manual configuration instructions
4. You must add the HiddenServiceDir and HiddenServicePort to torrc manually
5. After configuring torrc and restarting Tor, start aisbf again
### Dashboard Configuration
1. Navigate to **Dashboard → Settings**
......@@ -258,9 +282,11 @@ All AISBF endpoints are available over TOR:
- Check AISBF logs for detailed error messages
**Authentication Failed:**
- Verify CookieAuthentication is enabled in torrc
- Check control_password matches torrc configuration
- Ensure AISBF has permission to read TOR cookie file
- Use password authentication instead of CookieAuthentication
- Generate hashed password: `tor --hash-password "your_password"`
- Add to torrc: `HashedControlPassword 16:YOUR_HASH`
- Set `control_password` in aisbf.json to match your password
- Restart Tor: `sudo systemctl restart tor`
**Onion Address Not Generated:**
- Check TOR logs: `journalctl -u tor` or `tail -f /var/log/tor/log`
......@@ -585,12 +611,12 @@ Authorization: Bearer YOUR_API_TOKEN
| Endpoint | Description |
|----------|-------------|
| `GET /api/user/models` | List available models from user's own configurations |
| `GET /api/user/providers` | List user's provider configurations |
| `GET /api/user/rotations` | List user's rotation configurations |
| `GET /api/user/autoselects` | List user's autoselect configurations |
| `POST /api/user/chat/completions` | Chat completions using user's own models |
| `GET /api/user/{config_type}/models` | List models for specific config type (provider, rotation, autoselect) |
| `GET /api/u/{username}/models` | List available models from user's own configurations |
| `GET /api/u/{username}/providers` | List user's provider configurations |
| `GET /api/u/{username}/rotations` | List user's rotation configurations |
| `GET /api/u/{username}/autoselects` | List user's autoselect configurations |
| `POST /api/u/{username}/chat/completions` | Chat completions using user's own models |
| `GET /api/u/{username}/{config_type}/models` | List models for specific config type (provider, rotation, autoselect) |
**Access Control:**
- **Admin Users** have access to both global and user configurations when using user API endpoints
......@@ -601,21 +627,28 @@ Authorization: Bearer YOUR_API_TOKEN
```bash
# List user models
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/models
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/u/yourusername/models
# Chat using user's own models
curl -X POST -H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"model": "your-rotation/model", "messages": [{"role": "user", "content": "Hello"}]}' \
http://localhost:17765/api/user/chat/completions
http://localhost:17765/api/u/yourusername/chat/completions
```
### MCP (Model Context Protocol)
AISBF provides an MCP server for remote agent configuration and model access:
**Global MCP Endpoints (Admin-configured tokens):**
- **SSE Endpoint**: `GET /mcp` - Server-Sent Events for MCP communication
- **HTTP Endpoint**: `POST /mcp` - Direct HTTP transport for MCP
- **Tools**: `GET /mcp/tools` - List available MCP tools
- **Call Tool**: `POST /mcp/tools/call` - Call an MCP tool
**User-Specific MCP Endpoints (User API tokens):**
- **Tools**: `GET /mcp/u/{username}/tools` - List user's MCP tools
- **Call Tool**: `POST /mcp/u/{username}/tools/call` - Call user's MCP tool
MCP tools include:
- `list_models` - List available models (user or global depending on auth)
......
......@@ -2,7 +2,11 @@
A modular proxy server for managing multiple AI provider integrations with unified API interface. AISBF provides intelligent routing, load balancing, and AI-assisted model selection to optimize AI service usage across multiple providers.
![AISBF Dashboard](screenshot.png)
## Try AISBF
Try AISBF live at [https://aisbf.cloud](https://aisbf.cloud) or via TOR at [http://aisbfity4ud6nsht53tsh2iauaur2e4dah2gplcprnikyjpkg72vfjad.onion](http://aisbfity4ud6nsht53tsh2iauaur2e4dah2gplcprnikyjpkg72vfjad.onion) - no installation required!
![AISBF Dashboard](https://git.nexlab.net/nexlab/aisbf/raw/master/screenshot.png)
## Web Dashboard
......@@ -394,9 +398,22 @@ brew install tor
Edit `/etc/tor/torrc` (or `~/.torrc` on macOS) and add:
```
ControlPort 9051
CookieAuthentication 1
HashedControlPassword 16:YOUR_HASHED_PASSWORD_HERE
```
Generate a hashed password:
```bash
tor --hash-password "your_secure_password"
```
**For persistent hidden services, also add:**
```
HiddenServiceDir /home/yourusername/.aisbf/tor_hidden_service
HiddenServicePort 80 127.0.0.1:17765
```
**Important:** Replace `/home/yourusername` with your actual home directory path. Tor does NOT expand `~` in torrc - you must use absolute paths.
Then restart TOR:
```bash
sudo systemctl restart tor # Linux
......@@ -427,8 +444,8 @@ Edit `~/.aisbf/aisbf.json`:
"enabled": true,
"control_port": 9051,
"control_host": "127.0.0.1",
"control_password": null,
"hidden_service_dir": null,
"control_password": "your_secure_password",
"hidden_service_dir": "~/.aisbf/tor_hidden_service",
"hidden_service_port": 80,
"socks_port": 9050,
"socks_host": "127.0.0.1"
......@@ -436,6 +453,8 @@ Edit `~/.aisbf/aisbf.json`:
}
```
**Important:** Set `control_password` to match the password you used when generating the HashedControlPassword for torrc.
#### Ephemeral vs Persistent Hidden Services
**Ephemeral (Default):**
......@@ -451,6 +470,7 @@ Edit `~/.aisbf/aisbf.json`:
- Keys stored in specified directory
- Ideal for production use
- Set `hidden_service_dir` to a path (e.g., `~/.aisbf/tor_hidden_service`)
- **Must be manually configured in torrc** - see configuration instructions above
#### Accessing Your Hidden Service
......@@ -1040,15 +1060,7 @@ Authenticated users can access their configurations using their username in the
| `POST /api/u/{username}/chat/completions` | Chat completions using user's own models |
| `GET /api/u/{username}/{config_type}/models` | List models for specific config type (provider, rotation, autoselect) |
Legacy `/api/user/...` endpoints remain fully supported for backward compatibility:
| Legacy Endpoint | Description |
|-----------------|-------------|
| `GET /api/user/models` | Legacy endpoint for authenticated user |
| `GET /api/user/providers` | Legacy endpoint for authenticated user |
| `GET /api/user/rotations` | Legacy endpoint for authenticated user |
| `GET /api/user/autoselects` | Legacy endpoint for authenticated user |
| `POST /api/user/chat/completions` | Legacy chat completions endpoint |
| `GET /api/user/{config_type}/models` | Legacy model listing endpoint |
Legacy `/api/user/...` endpoints have been replaced with `/api/u/{username}/...` endpoints for better clarity and security.
#### Access Control
......@@ -1101,6 +1113,29 @@ All requests are properly authenticated and authorized using the standard Bearer
#### MCP Integration
User tokens also work with MCP (Model Context Protocol) endpoints:
**Global MCP Endpoints (Admin-configured tokens in aisbf.json):**
- `GET /mcp` - SSE endpoint for MCP communication
- `POST /mcp` - HTTP POST endpoint for MCP
- `GET /mcp/tools` - List available global MCP tools
- `POST /mcp/tools/call` - Call global MCP tools
**User-Specific MCP Endpoints (User API tokens):**
- `GET /mcp/u/{username}/tools` - List user's MCP tools
- `POST /mcp/u/{username}/tools/call` - Call user's MCP tools
Example:
```bash
# List user's MCP tools
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/mcp/u/johnsmith/tools
# Call a user MCP tool
curl -X POST -H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "list_user_models", "arguments": {}}' \
http://localhost:17765/mcp/u/johnsmith/tools/call
```
- Admin users get access to both global and user-specific MCP tools
- Regular users get access to user-only MCP tools
- Tools include model access, configuration management, and usage statistics
......
# Tor Hidden Service Setup Guide for AISBF
This guide explains how to configure Tor for use with AISBF, including both ephemeral and persistent hidden services.
## Prerequisites
Install Tor and the stem Python library:
```bash
# Install Tor
sudo apt install tor # Debian/Ubuntu
sudo dnf install tor # Fedora
brew install tor # macOS
# Install stem library
pip install stem
```
## Tor Configuration (torrc)
### Basic Setup (Required for All Configurations)
Edit your torrc file (usually `/etc/tor/torrc` or `~/.torrc` on macOS):
```
ControlPort 9051
HashedControlPassword 16:YOUR_HASHED_PASSWORD_HERE
```
Generate a hashed password:
```bash
tor --hash-password "your_secure_password"
```
Copy the output (starts with `16:`) and paste it after `HashedControlPassword` in torrc.
### For Ephemeral Hidden Services (Default)
No additional torrc configuration needed. AISBF will create a temporary hidden service via the control port.
**AISBF Configuration (`~/.aisbf/aisbf.json`):**
```json
{
"tor": {
"enabled": true,
"control_password": "your_secure_password",
"hidden_service_dir": "",
"hidden_service_port": 80
}
}
```
### For Persistent Hidden Services (Production)
Add these lines to torrc:
```
HiddenServiceDir /home/yourusername/.aisbf/tor_hidden_service
HiddenServicePort 80 127.0.0.1:17765
```
**Important:**
- Replace `/home/yourusername` with your actual home directory path
- **Tor does NOT expand `~` in torrc** - you must use absolute paths like `/home/username`
- The port `17765` should match your AISBF server port
- The directory will be created by Tor with proper permissions
**AISBF Configuration (`~/.aisbf/aisbf.json`):**
```json
{
"tor": {
"enabled": true,
"control_password": "your_secure_password",
"hidden_service_dir": "/home/yourusername/.aisbf/tor_hidden_service",
"hidden_service_port": 80
}
}
```
**Note:** While AISBF config can use `~/.aisbf/tor_hidden_service`, the torrc file must use the full absolute path.
## Complete torrc Example
### For Ephemeral Hidden Service:
```
ControlPort 9051
HashedControlPassword 16:872860B76453A77D60CA2BB8C1A7042072093276A3D701AD684053EC4C
```
### For Persistent Hidden Service:
```
ControlPort 9051
HashedControlPassword 16:872860B76453A77D60CA2BB8C1A7042072093276A3D701AD684053EC4C
HiddenServiceDir /home/yourusername/.aisbf/tor_hidden_service
HiddenServicePort 80 127.0.0.1:17765
```
## Restart Tor
After editing torrc, restart Tor:
```bash
sudo systemctl restart tor # Linux
brew services restart tor # macOS
```
## Verify Tor is Running
```bash
# Check Tor status
sudo systemctl status tor # Linux
brew services list # macOS
# Test control port connection
telnet 127.0.0.1 9051
```
## Get Your Onion Address
### Ephemeral Service:
The onion address is displayed in AISBF logs when the service starts.
### Persistent Service:
```bash
cat ~/.aisbf/tor_hidden_service/hostname
```
## Troubleshooting
### Permission Denied on Cookie File
Use password authentication instead of cookie authentication (as shown above).
### Connection Refused
- Verify Tor is running: `sudo systemctl status tor`
- Check ControlPort is configured in torrc
- Test connection: `telnet 127.0.0.1 9051`
### Authentication Failed
- Verify the password in aisbf.json matches the one used to generate the hash
- Check the HashedControlPassword in torrc is correct
### Hidden Service Not Created
- Check Tor logs: `sudo journalctl -u tor -n 50`
- Verify HiddenServiceDir path is correct and accessible
- Ensure AISBF server port matches the one in HiddenServicePort
### Onion Address Not Accessible
- Verify AISBF is running: `curl http://localhost:17765`
- Check Tor Browser is configured correctly
- Wait a few minutes for the hidden service to propagate
## Security Considerations
1. **Use strong passwords** for Tor control authentication
2. **Enable API authentication** in AISBF for additional security
3. **Use persistent hidden services** for production deployments
4. **Monitor access logs** for suspicious activity
5. **Keep Tor and AISBF updated** regularly
6. **Consider firewall rules** to restrict clearnet access if only using Tor
## Additional Resources
- Tor Project Documentation: https://www.torproject.org/docs/
- Tor Hidden Service Guide: https://community.torproject.org/onion-services/
- stem Library Documentation: https://stem.torproject.org/
......@@ -54,7 +54,7 @@ from .auth.qwen import QwenOAuth2
from .handlers import RequestHandler, RotationHandler, AutoselectHandler
from .utils import count_messages_tokens, split_messages_into_chunks, get_max_request_tokens_for_model
__version__ = "0.99.21"
__version__ = "0.99.25"
__all__ = [
# Config
"config",
......
......@@ -231,7 +231,7 @@ class KiroAuthManager:
return self._access_token
async def _refresh_kiro_desktop_token(self):
"""Refresh token using Kiro Desktop Auth"""
"""Refresh token using Kiro Desktop Auth with retry logic"""
if not self.refresh_token:
raise ValueError("No refresh token available")
......@@ -242,7 +242,12 @@ class KiroAuthManager:
}
payload = {"refreshToken": self.refresh_token}
async with httpx.AsyncClient() as client:
max_retries = 3
retry_delay = 1.0 # Start with 1 second
for attempt in range(max_retries):
try:
async with httpx.AsyncClient(timeout=httpx.Timeout(30.0, connect=10.0)) as client:
response = await client.post(url, json=payload, headers=headers)
response.raise_for_status()
data = response.json()
......@@ -259,8 +264,26 @@ class KiroAuthManager:
if self.creds_file:
self._save_credentials()
if attempt > 0:
logger.info(f"Token refresh succeeded on attempt {attempt + 1}")
return
except (httpx.ConnectTimeout, httpx.ReadTimeout, httpx.TimeoutException) as e:
logger.warning(f"Token refresh timeout on attempt {attempt + 1}/{max_retries}: {type(e).__name__}")
if attempt < max_retries - 1:
logger.info(f"Retrying in {retry_delay:.1f} seconds...")
await asyncio.sleep(retry_delay)
retry_delay *= 2 # Exponential backoff
else:
logger.error(f"Token refresh failed after {max_retries} attempts")
raise
except Exception as e:
logger.error(f"Token refresh failed with non-timeout error: {type(e).__name__}: {e}")
raise
async def _refresh_aws_sso_token(self):
"""Refresh token using AWS SSO OIDC"""
"""Refresh token using AWS SSO OIDC with retry logic"""
if not all([self.refresh_token, self.client_id, self.client_secret]):
raise ValueError("Missing credentials for AWS SSO OIDC")
......@@ -272,7 +295,12 @@ class KiroAuthManager:
"refresh_token": self.refresh_token
}
async with httpx.AsyncClient() as client:
max_retries = 3
retry_delay = 1.0 # Start with 1 second
for attempt in range(max_retries):
try:
async with httpx.AsyncClient(timeout=httpx.Timeout(30.0, connect=10.0)) as client:
response = await client.post(url, data=payload)
response.raise_for_status()
data = response.json()
......@@ -285,6 +313,24 @@ class KiroAuthManager:
expires_in = data.get('expires_in', 3600)
self._expires_at = datetime.now(timezone.utc) + timedelta(seconds=expires_in)
if attempt > 0:
logger.info(f"AWS SSO token refresh succeeded on attempt {attempt + 1}")
return
except (httpx.ConnectTimeout, httpx.ReadTimeout, httpx.TimeoutException) as e:
logger.warning(f"AWS SSO token refresh timeout on attempt {attempt + 1}/{max_retries}: {type(e).__name__}")
if attempt < max_retries - 1:
logger.info(f"Retrying in {retry_delay:.1f} seconds...")
await asyncio.sleep(retry_delay)
retry_delay *= 2 # Exponential backoff
else:
logger.error(f"AWS SSO token refresh failed after {max_retries} attempts")
raise
except Exception as e:
logger.error(f"AWS SSO token refresh failed with non-timeout error: {type(e).__name__}: {e}")
raise
def _save_credentials(self):
"""Save updated credentials to file"""
if not self.creds_file:
......
......@@ -249,6 +249,18 @@ class SMTPConfig(BaseModel):
from_email: str = ""
from_name: str = "AISBF"
class OAuth2ProviderConfig(BaseModel):
"""Configuration for an OAuth2 provider"""
enabled: bool = False
client_id: str = ""
client_secret: str = ""
scopes: List[str] = []
class OAuth2Config(BaseModel):
"""Configuration for OAuth2 authentication providers"""
google: Optional[OAuth2ProviderConfig] = None
github: Optional[OAuth2ProviderConfig] = None
class AISBFConfig(BaseModel):
"""Global AISBF configuration from aisbf.json"""
classify_nsfw: bool = False
......@@ -267,6 +279,7 @@ class AISBFConfig(BaseModel):
adaptive_rate_limiting: Optional[AdaptiveRateLimitingConfig] = None
signup: Optional[SignupConfig] = None
smtp: Optional[SMTPConfig] = None
oauth2: Optional[OAuth2Config] = None
currency: Optional[CurrencyConfig] = None
payment_gateways: Optional[Dict[str, PaymentGatewayConfig]] = None
......
......@@ -2406,6 +2406,105 @@ class DatabaseManager:
})
return transactions
def get_payment_gateway_settings(self) -> Dict:
"""Get payment gateway settings from admin_settings table."""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
default_gateways = {
"paypal": {"enabled": False, "client_id": "", "client_secret": "", "webhook_secret": "", "sandbox": True},
"stripe": {"enabled": False, "publishable_key": "", "secret_key": "", "webhook_secret": "", "test_mode": True},
"bitcoin": {"enabled": False, "address": "", "confirmations": 3, "expiration_minutes": 120},
"ethereum": {"enabled": False, "address": "", "confirmations": 12, "chain_id": 1},
"usdt": {"enabled": False, "address": "", "network": "erc20", "confirmations": 3},
"usdc": {"enabled": False, "address": "", "network": "erc20", "confirmations": 3}
}
try:
cursor.execute(f'''
SELECT setting_value
FROM admin_settings
WHERE setting_key = {placeholder}
''', ('payment_gateways',))
row = cursor.fetchone()
if row and row[0]:
import json
return json.loads(row[0])
except Exception as e:
logger.warning(f"Error loading payment gateway settings: {e}")
return default_gateways
def save_payment_gateway_settings(self, settings: Dict) -> bool:
"""Save payment gateway settings to admin_settings table."""
with self._get_connection() as conn:
cursor = conn.cursor()
import json
settings_json = json.dumps(settings)
placeholder = '?' if self.db_type == 'sqlite' else '%s'
try:
insert_syntax = 'INSERT OR REPLACE' if self.db_type == 'sqlite' else 'REPLACE'
cursor.execute(f'''
{insert_syntax} INTO admin_settings (setting_key, setting_value, updated_at)
VALUES ({placeholder}, {placeholder}, CURRENT_TIMESTAMP)
''', ('payment_gateways', settings_json))
conn.commit()
logger.info("Payment gateway settings saved to database")
return True
except Exception as e:
logger.error(f"Error saving payment gateway settings: {e}")
return False
def get_currency_settings(self) -> Dict:
"""Get currency settings from admin_settings table."""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
default_currency = {
"currency_code": "USD",
"currency_symbol": "$",
"currency_decimals": 2
}
try:
cursor.execute(f'''
SELECT setting_value
FROM admin_settings
WHERE setting_key = {placeholder}
''', ('currency',))
row = cursor.fetchone()
if row and row[0]:
import json
return json.loads(row[0])
except Exception as e:
logger.warning(f"Error loading currency settings: {e}")
return default_currency
def save_currency_settings(self, settings: Dict) -> bool:
"""Save currency settings to admin_settings table."""
with self._get_connection() as conn:
cursor = conn.cursor()
import json
settings_json = json.dumps(settings)
placeholder = '?' if self.db_type == 'sqlite' else '%s'
try:
insert_syntax = 'INSERT OR REPLACE' if self.db_type == 'sqlite' else 'REPLACE'
cursor.execute(f'''
{insert_syntax} INTO admin_settings (setting_key, setting_value, updated_at)
VALUES ({placeholder}, {placeholder}, CURRENT_TIMESTAMP)
''', ('currency', settings_json))
conn.commit()
logger.info("Currency settings saved to database")
return True
except Exception as e:
logger.error(f"Error saving currency settings: {e}")
return False
# ============================================================
# DATABASE REGISTRY - EXPLICIT NAMED INSTANCES
......@@ -2909,34 +3008,6 @@ def DatabaseManager__initialize_database(self):
''')
conn.commit()
logger.info("✅ Migration: Created missing account_tiers table")
else:
cursor.execute("""
SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = 'account_tiers'
""")
if not cursor.fetchone():
cursor.execute(f'''
CREATE TABLE account_tiers (
id INTEGER PRIMARY KEY {auto_increment},
name VARCHAR(255) UNIQUE NOT NULL,
description TEXT,
price_monthly DECIMAL(10,2) DEFAULT 0.00,
price_yearly DECIMAL(10,2) DEFAULT 0.00,
is_default {boolean_type} DEFAULT 0,
is_active {boolean_type} DEFAULT 1,
max_requests_per_day INTEGER DEFAULT -1,
max_requests_per_month INTEGER DEFAULT -1,
max_providers INTEGER DEFAULT -1,
max_rotations INTEGER DEFAULT -1,
max_autoselections INTEGER DEFAULT -1,
max_rotation_models INTEGER DEFAULT -1,
max_autoselection_models INTEGER DEFAULT -1,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default}
)
''')
conn.commit()
logger.info("✅ Migration: Created missing account_tiers table")
except Exception as e:
logger.warning(f"Migration check for account_tiers table: {e}")
......@@ -3091,6 +3162,14 @@ def DatabaseManager__initialize_database(self):
FOREIGN KEY (user_id) REFERENCES users(id)
)
'''),
('admin_settings', f'''
CREATE TABLE admin_settings (
id INTEGER PRIMARY KEY {auto_increment},
setting_key VARCHAR(255) UNIQUE NOT NULL,
setting_value TEXT,
updated_at TIMESTAMP DEFAULT {timestamp_default}
)
'''),
('user_subscriptions', f'''
CREATE TABLE user_subscriptions (
id INTEGER PRIMARY KEY {auto_increment},
......
......@@ -39,6 +39,7 @@ class MCPAuthLevel:
NONE = 0
AUTOSELECT = 1 # Can access autoselection and autorotation settings
FULLCONFIG = 2 # Can access all configurations (providers, rotations, autoselect, aisbf)
USER = 3 # User-specific access via API token
def load_mcp_config() -> Dict:
......@@ -1598,12 +1599,18 @@ class MCPServer:
from starlette.requests import Request
from main import get_user_handler
# Get username for the path
from .database import get_database
db = get_database()
user = db.get_user_by_id(user_id)
username = user['username'] if user else 'unknown'
scope = {
"type": "http",
"method": "POST",
"headers": [],
"query_string": b"",
"path": "/api/user/chat/completions"
"path": f"/api/u/{username}/chat/completions"
}
dummy_request = Request(scope)
......
......@@ -98,6 +98,7 @@ class AccountTier(BaseModel):
price_yearly: float = 0.0
is_default: bool = False
is_active: bool = True
is_visible: bool = True # If False, tier is hidden from user selection but can be assigned by admin
# Limits
max_requests_per_day: int = -1
......
......@@ -127,8 +127,28 @@ class KiroProviderHandler(BaseProviderHandler):
# Apply rate limiting
await self.apply_rate_limit()
# Get access token and profile ARN
# Get access token and profile ARN with retry logic
max_retries = 3
retry_delay = 1.0
access_token = None
for attempt in range(max_retries):
try:
access_token = await self.auth_manager.get_access_token()
break
except (Exception) as e:
if "ConnectTimeout" in str(type(e).__name__) or "TimeoutException" in str(type(e).__name__):
logging.warning(f"Token retrieval timeout on attempt {attempt + 1}/{max_retries}")
if attempt < max_retries - 1:
logging.info(f"Retrying in {retry_delay:.1f} seconds...")
await asyncio.sleep(retry_delay)
retry_delay *= 2
else:
logging.error(f"Token retrieval failed after {max_retries} attempts")
raise
else:
raise
profile_arn = self.auth_manager.profile_arn
# Determine effective profileArn based on auth type
......@@ -176,12 +196,30 @@ class KiroProviderHandler(BaseProviderHandler):
model=model
)
# Non-streaming request
# Non-streaming request with retry logic
max_api_retries = 2
api_retry_delay = 2.0
response = None
for api_attempt in range(max_api_retries):
try:
response = await self.client.post(
kiro_api_url,
json=payload,
headers=headers
)
break
except (httpx.ConnectTimeout, httpx.ReadTimeout, httpx.TimeoutException) as e:
logging.warning(f"API request timeout on attempt {api_attempt + 1}/{max_api_retries}: {type(e).__name__}")
if api_attempt < max_api_retries - 1:
logging.info(f"Retrying API request in {api_retry_delay:.1f} seconds...")
await asyncio.sleep(api_retry_delay)
api_retry_delay *= 1.5
else:
logging.error(f"API request failed after {max_api_retries} attempts")
self.record_failure()
raise
# Check for 429 rate limit error before raising
if response.status_code == 429:
......@@ -234,6 +272,11 @@ class KiroProviderHandler(BaseProviderHandler):
self.record_success()
return openai_response
except (httpx.ConnectTimeout, httpx.ReadTimeout, httpx.TimeoutException) as e:
logging.error(f"KiroProviderHandler: Timeout error after retries: {type(e).__name__}")
logging.debug(f"KiroProviderHandler: Timeout details", exc_info=True)
self.record_failure()
raise Exception(f"Kiro API timeout after retries: {type(e).__name__}")
except Exception as e:
logging.error(f"KiroProviderHandler: Error: {str(e)}", exc_info=True)
self.record_failure()
......
......@@ -84,7 +84,7 @@ class TorHiddenService:
)
# Authenticate
if self.config.control_password:
if self.config.control_password and self.config.control_password.strip():
logger.info("Authenticating with TOR control port using password")
self.controller.authenticate(password=self.config.control_password)
else:
......@@ -106,6 +106,50 @@ class TorHiddenService:
logger.error(f"Error connecting to TOR: {e}")
return False
def _check_existing_hidden_service(self, hidden_service_dir: Path) -> Optional[str]:
"""
Check if a hidden service already exists in torrc and retrieve its onion address.
Args:
hidden_service_dir: Path to the hidden service directory
Returns:
str: Onion address if found, None otherwise
"""
try:
hostname_file = hidden_service_dir / 'hostname'
if hostname_file.exists() and hostname_file.stat().st_size > 0:
onion_address = hostname_file.read_text().strip()
logger.info(f"Found existing hidden service: {onion_address}")
return onion_address
except Exception as e:
logger.debug(f"Could not read existing hostname file: {e}")
return None
def _print_manual_configuration_instructions(self, hidden_service_dir: Path, local_port: int):
"""
Print manual configuration instructions for persistent hidden service.
Args:
hidden_service_dir: Path to the hidden service directory
local_port: Local port where AISBF is running
"""
logger.error("=" * 80)
logger.error("MANUAL CONFIGURATION REQUIRED")
logger.error("=" * 80)
logger.error("To set up a persistent Tor hidden service, add the following lines")
logger.error("to your torrc file (usually /etc/tor/torrc):")
logger.error("")
logger.error(f" HiddenServiceDir {hidden_service_dir}")
logger.error(f" HiddenServicePort {self.config.hidden_service_port} 127.0.0.1:{local_port}")
logger.error("")
logger.error("Then restart Tor:")
logger.error(" sudo systemctl restart tor")
logger.error("")
logger.error("After Tor restarts, your onion address will be in:")
logger.error(f" {hidden_service_dir}/hostname")
logger.error("=" * 80)
def create_hidden_service(self, local_port: int) -> Optional[str]:
"""
Create a TOR hidden service.
......@@ -122,37 +166,43 @@ class TorHiddenService:
try:
from stem.control import Controller
import time
# Determine if we should use persistent or ephemeral hidden service
if self.config.hidden_service_dir:
if self.config.hidden_service_dir and self.config.hidden_service_dir.strip():
# Persistent hidden service
hidden_service_dir = Path(self.config.hidden_service_dir).expanduser()
hidden_service_dir = Path(self.config.hidden_service_dir).expanduser().resolve()
hidden_service_dir.mkdir(parents=True, exist_ok=True)
logger.info(f"Creating persistent hidden service in {hidden_service_dir}")
logger.info(f"Setting up persistent hidden service in {hidden_service_dir}")
# Check if hidden service already exists
existing_address = self._check_existing_hidden_service(hidden_service_dir)
if existing_address:
self.onion_address = existing_address
logger.info(f"Using existing persistent hidden service: {self.onion_address}")
return self.onion_address
# Persistent hidden services must be configured in torrc
logger.warning("Persistent hidden service not found")
self._print_manual_configuration_instructions(hidden_service_dir, local_port)
# Set hidden service configuration
self.controller.set_options([
('HiddenServiceDir', str(hidden_service_dir)),
('HiddenServicePort', f'{self.config.hidden_service_port} 127.0.0.1:{local_port}')
])
# Wait a bit to see if user configured it
logger.info("Waiting 10 seconds for manual configuration...")
logger.info("(Configure torrc now, or press Ctrl+C to cancel)")
# Wait for Tor daemon to create the hostname file (can take up to 30 seconds)
import time
hostname_file = hidden_service_dir / 'hostname'
# Wait up to 30 seconds with 1 second intervals
for attempt in range(30):
if hostname_file.exists() and hostname_file.stat().st_size > 0:
break
for attempt in range(10):
time.sleep(1)
logger.debug(f"Waiting for hostname file... attempt {attempt + 1}/30")
if hostname_file.exists():
if hostname_file.exists() and hostname_file.stat().st_size > 0:
self.onion_address = hostname_file.read_text().strip()
logger.info(f"Persistent hidden service created: {self.onion_address}")
else:
logger.error("Failed to read onion address from hostname file")
logger.info(f"Persistent hidden service detected: {self.onion_address}")
return self.onion_address
logger.error("Persistent hidden service not configured")
logger.error("Please configure torrc manually and restart aisbf")
return None
else:
# Ephemeral hidden service
......
......@@ -83,8 +83,8 @@
"enabled": false,
"control_port": 9051,
"control_host": "127.0.0.1",
"control_password": null,
"hidden_service_dir": null,
"control_password": "",
"hidden_service_dir": "",
"hidden_service_port": 80,
"socks_port": 9050,
"socks_host": "127.0.0.1"
......
# Username and Display Name Handling Design
## Overview
Implement separate username and display_name fields to handle OAuth signup with full names while maintaining clean usernames for system use.
## Problem Statement
Google OAuth returns display names like "Stefy "nextime" Lanza" which contain spaces and special characters not allowed in usernames. Current system tries to use raw display name as username, causing validation failures.
## Solution
- Add `display_name` VARCHAR(255) column to users table
- Generate clean username from display_name (with email fallback)
- Store raw OAuth display name in display_name field
- Use display_name for UI display, username for system identification
## Database Schema Changes
### Users Table
```sql
ALTER TABLE users ADD COLUMN display_name VARCHAR(255);
UPDATE users SET display_name = username WHERE display_name IS NULL;
```
### Affected Queries
- All SELECT from users: include display_name
- INSERT users: accept display_name parameter
- UPDATE users: allow display_name changes
## Username Generation Logic
### OAuth Signup Flow
1. Get display_name from OAuth provider:
- Google: `user_info.get('name')`
- GitHub: `user_info.get('name')` or `user_info.get('login')`
2. Generate username:
```python
if display_name and display_name.strip():
# Sanitize display_name
username_base = sanitize(display_name)
else:
# Fallback to email
username_base = sanitize(email.split('@')[0])
# Ensure unique
username = find_unique_username(username_base)
```
3. Sanitization rules:
- Lowercase
- Remove invalid characters (keep a-z, A-Z, 0-9, -, _, .)
- Replace spaces with underscores
- Length 3-50 characters
- Trim whitespace
4. Uniqueness handling:
- Check if username exists
- If conflict, append counter: username1, username2, etc.
## Code Changes Required
### Database Layer (database.py)
- Update table creation/migration
- Update user creation functions
- Update user retrieval functions
### OAuth Handlers (main.py)
- Update Google OAuth callback
- Update GitHub OAuth callback
- Pass display_name to user creation
### User Management
- Update admin user creation
- Update user profile editing
- Update signup form handling
### UI Changes
- Display display_name in user lists
- Allow display_name editing in profile
- Show username in technical contexts
## Migration Strategy
1. Add display_name column with NULL allowed initially
2. Populate existing users: display_name = username
3. Make display_name NOT NULL with default ''
4. Update all code to handle display_name
5. Test OAuth signup with display names
## Testing Requirements
- OAuth signup with names containing spaces/quotes
- Username conflict resolution
- Display name editing
- Backward compatibility with existing users
- UI displays display_name correctly
## Success Criteria
- OAuth signup works with any display name format
- Usernames are always valid and unique
- Display names preserve user identity
- Existing functionality unchanged
- UI shows appropriate names in context</content>
<parameter name="filePath">docs/superpowers/specs/2026-04-15-username-display-name-design.md
\ No newline at end of file
This diff is collapsed.
......@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "aisbf"
version = "0.99.21"
version = "0.99.25"
description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations"
readme = "README.md"
license = "GPL-3.0-or-later"
......
......@@ -49,7 +49,7 @@ class InstallCommand(_install):
setup(
name="aisbf",
version="0.99.21",
version="0.99.25",
author="AISBF Contributors",
author_email="stefy@nexlab.net",
description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations",
......
......@@ -461,7 +461,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<body>
<div class="header">
<div class="container">
<h1>AISBF Dashboard</h1>
<h1>AISBF Dashboard <span style="font-size: 0.8em; font-weight: normal; opacity: 0.7;">v{{ __version__ }}</span></h1>
{% if request.session.logged_in %}
<div class="header-actions">
<a href="{{ url_for(request, '/dashboard/docs') }}" class="btn btn-secondary">Docs</a>
......@@ -501,6 +501,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<a href="{{ url_for(request, '/dashboard/autoselect') }}" {% if '/autoselect' in request.path %}class="active"{% endif %}>Autoselect</a>
<a href="{{ url_for(request, '/dashboard/prompts') }}" {% if '/prompts' in request.path %}class="active"{% endif %}>Prompts</a>
<a href="{{ url_for(request, '/dashboard/analytics') }}" {% if '/analytics' in request.path %}class="active"{% endif %}>Analytics</a>
<a href="{{ url_for(request, '/dashboard/user/tokens') }}" {% if '/user/tokens' in request.path %}class="active"{% endif %}>API Tokens</a>
{% if request.session.role == 'admin' %}
<a href="{{ url_for(request, '/dashboard/users') }}" {% if '/users' in request.path %}class="active"{% endif %}>Users</a>
<a href="{{ url_for(request, '/dashboard/settings') }}" {% if '/settings' in request.path %}class="active"{% endif %}>Settings</a>
......@@ -519,6 +520,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% if request.session.user_id %}
{% if request.session.user_id %}
<a href="{{ url_for(request, '/dashboard/profile') }}">Edit Profile</a>
<a href="{{ url_for(request, '/dashboard/user/tokens') }}">API Tokens</a>
<a href="{{ url_for(request, '/dashboard/subscription') }}">Subscription</a>
<a href="{{ url_for(request, '/dashboard/billing') }}">Billing</a>
<a href="{{ url_for(request, '/dashboard/change-password') }}">Change Password</a>
......
......@@ -89,6 +89,13 @@
Tier is active and available for users
</label>
</div>
<div style="margin-bottom: 15px;">
<label style="cursor: pointer; font-weight: 500; color: #e0e0e0;">
<input type="checkbox" name="is_visible" value="1" {{ 'checked' if not tier or tier.is_visible else '' }} style="margin-right: 10px;">
Tier is visible to users (if unchecked, only admin can assign this tier)
</label>
</div>
</div>
<div style="display: flex; gap: 10px;">
......
......@@ -744,9 +744,10 @@ function savePaymentGateways() {
function showToast(message, type) {
// Create and show toast notification
const toastContainer = document.createElement('div');
toastContainer.className = `position-fixed bottom-0 end-0 p-3`;
toastContainer.className = `position-fixed top-0 end-0 p-3`;
toastContainer.style.zIndex = '9999';
toastContainer.innerHTML = `
<div class="toast show" role="alert">
<div class="toast show" role="alert" style="min-width: 300px;">
<div class="toast-header bg-${type} text-white">
<strong class="me-auto">Notification</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button>
......@@ -758,7 +759,12 @@ function showToast(message, type) {
`;
document.body.appendChild(toastContainer);
setTimeout(() => toastContainer.remove(), 3000);
// Auto-dismiss after 5 seconds
setTimeout(() => {
if (toastContainer.parentNode) {
toastContainer.remove();
}
}, 5000);
}
</script>
{% endblock %}
\ No newline at end of file
......@@ -67,15 +67,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% endif %}
</form>
{% if config.oauth2 and (config.oauth2.google.enabled or config.oauth2.github.enabled) %}
{% if config.oauth2 and ((config.oauth2.google and config.oauth2.google.enabled) or (config.oauth2.github and config.oauth2.github.enabled)) %}
<div style="margin: 25px 0; text-align: center; position: relative;">
<hr style="border: none; border-top: 1px solid #ddd; margin: 0;">
<span style="background: white; padding: 0 15px; position: relative; top: -13px; color: #666;">or continue with</span>
<hr style="border: none; border-top: 1px solid #0f3460; margin: 0;">
<span style="background: #16213e; padding: 0 15px; position: relative; top: -13px; color: #a0a0a0;">or continue with</span>
</div>
<div style="display: flex; flex-direction: column; gap: 12px;">
{% if config.oauth2.google.enabled %}
<a href="/auth/oauth2/google" style="display: flex; align-items: center; justify-content: center; gap: 10px; padding: 12px; border: 1px solid #dadce0; border-radius: 4px; background: white; color: #3c4043; font-weight: 500; text-decoration: none; transition: background 0.2s;">
{% if config.oauth2.google and config.oauth2.google.enabled %}
<a href="/auth/oauth2/google" style="display: flex; align-items: center; justify-content: center; gap: 10px; padding: 12px; border: 1px solid #0f3460; border-radius: 4px; background: #1a1a2e; color: #e0e0e0; font-weight: 500; text-decoration: none; transition: background 0.2s;">
<svg viewBox="0 0 24 24" width="20" height="20">
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
......@@ -86,8 +86,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</a>
{% endif %}
{% if config.oauth2.github.enabled %}
<a href="/auth/oauth2/github" style="display: flex; align-items: center; justify-content: center; gap: 10px; padding: 12px; border: 1px solid #d1d5da; border-radius: 4px; background: #24292e; color: #ffffff; font-weight: 500; text-decoration: none;">
{% if config.oauth2.github and config.oauth2.github.enabled %}
<a href="/auth/oauth2/github" style="display: flex; align-items: center; justify-content: center; gap: 10px; padding: 12px; border: 1px solid #0f3460; border-radius: 4px; background: #24292e; color: #ffffff; font-weight: 500; text-decoration: none; transition: background 0.2s;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
......
......@@ -262,9 +262,10 @@ function downgradeToFree() {
function showToast(message, type) {
const toastContainer = document.createElement('div');
toastContainer.className = `position-fixed bottom-0 end-0 p-3`;
toastContainer.className = `position-fixed top-0 end-0 p-3`;
toastContainer.style.zIndex = '9999';
toastContainer.innerHTML = `
<div class="toast show" role="alert">
<div class="toast show" role="alert" style="min-width: 300px;">
<div class="toast-header bg-${type} text-white">
<strong class="me-auto">Notification</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button>
......@@ -276,7 +277,12 @@ function showToast(message, type) {
`;
document.body.appendChild(toastContainer);
setTimeout(() => toastContainer.remove(), 3000);
// Auto-dismiss after 5 seconds
setTimeout(() => {
if (toastContainer.parentNode) {
toastContainer.remove();
}
}, 5000);
}
</script>
{% endblock %}
\ No newline at end of file
......@@ -61,6 +61,16 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</p>
</div>
<div id="new-provider-claude-warning" style="background: #ffdd57; border: 2px solid #ff6b35; border-radius: 8px; padding: 20px; margin-bottom: 15px; display: none;">
<h4 style="margin: 0 0 10px 0; color: #d63031;">⚠️ Important Notice for Claude Provider</h4>
<p style="margin: 0; color: #2d3436;">
Claude.ai policies state that unofficial clients are not allowed. By using AISBF as a client, you acknowledge that:
<br><br>• While we do our best to mimic the claude-code CLI, using an unofficial client can lead to account suspension or cancellation
<br>• You use this software at your own risk and responsibility
<br>• We are not affiliated with Anthropic and cannot guarantee compatibility or continued functionality
</p>
</div>
<div style="display: flex; gap: 10px;">
<button type="button" class="btn" onclick="confirmAddProvider()">Create Provider</button>
<button type="button" class="btn btn-secondary" onclick="cancelAddProvider()">Cancel</button>
......@@ -345,6 +355,16 @@ function renderProviderDetails(key) {
</div>
<div id="claude-upload-status-${key}" style="margin-top: 10px;"></div>
</div>
<div style="background: #ffdd57; border: 2px solid #ff6b35; border-radius: 8px; padding: 20px; margin-bottom: 15px;">
<h4 style="margin: 0 0 10px 0; color: #d63031;">⚠️ Important Notice</h4>
<p style="margin: 0; color: #2d3436;">
Claude.ai policies state that unofficial clients are not allowed. By using AISBF as a client, you acknowledge that:
<br><br>• While we do our best to mimic the claude-code CLI, using an unofficial client can lead to account suspension or cancellation
<br>• You use this software at your own risk and responsibility
<br>• We are not affiliated with Anthropic and cannot guarantee compatibility or continued functionality
</p>
</div>
`;
} else if (isQwenProvider) {
// Qwen authentication fields - supports both API key and OAuth2
......@@ -751,6 +771,14 @@ function updateNewProviderDefaults() {
};
descriptionEl.textContent = descriptions[providerType] || 'Standard provider configuration.';
// Show/hide Claude warning
const claudeWarningEl = document.getElementById('new-provider-claude-warning');
if (providerType === 'claude') {
claudeWarningEl.style.display = 'block';
} else {
claudeWarningEl.style.display = 'none';
}
}
function confirmAddProvider() {
......
This diff is collapsed.
......@@ -79,15 +79,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
</form>
{% if config.oauth2 and (config.oauth2.google.enabled or config.oauth2.github.enabled) %}
{% if config.oauth2 and ((config.oauth2.google and config.oauth2.google.enabled) or (config.oauth2.github and config.oauth2.github.enabled)) %}
<div style="margin: 25px 0; text-align: center; position: relative;">
<hr style="border: none; border-top: 1px solid #ddd; margin: 0;">
<span style="background: white; padding: 0 15px; position: relative; top: -13px; color: #666;">or sign up with</span>
<hr style="border: none; border-top: 1px solid #0f3460; margin: 0;">
<span style="background: #16213e; padding: 0 15px; position: relative; top: -13px; color: #a0a0a0;">or sign up with</span>
</div>
<div style="display: flex; flex-direction: column; gap: 12px;">
{% if config.oauth2.google.enabled %}
<a href="/auth/oauth2/google" style="display: flex; align-items: center; justify-content: center; gap: 10px; padding: 12px; border: 1px solid #dadce0; border-radius: 4px; background: white; color: #3c4043; font-weight: 500; text-decoration: none; transition: background 0.2s;">
{% if config.oauth2.google and config.oauth2.google.enabled %}
<a href="/auth/oauth2/google" style="display: flex; align-items: center; justify-content: center; gap: 10px; padding: 12px; border: 1px solid #0f3460; border-radius: 4px; background: #1a1a2e; color: #e0e0e0; font-weight: 500; text-decoration: none; transition: background 0.2s;">
<svg viewBox="0 0 24 24" width="20" height="20">
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
......@@ -98,8 +98,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</a>
{% endif %}
{% if config.oauth2.github.enabled %}
<a href="/auth/oauth2/github" style="display: flex; align-items: center; justify-content: center; gap: 10px; padding: 12px; border: 1px solid #d1d5da; border-radius: 4px; background: #24292e; color: #ffffff; font-weight: 500; text-decoration: none;">
{% if config.oauth2.github and config.oauth2.github.enabled %}
<a href="/auth/oauth2/github" style="display: flex; align-items: center; justify-content: center; gap: 10px; padding: 12px; border: 1px solid #0f3460; border-radius: 4px; background: #24292e; color: #ffffff; font-weight: 500; text-decoration: none; transition: background 0.2s;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
......
......@@ -66,12 +66,12 @@
<h3>MCP Tools</h3>
<div class="endpoint">
<code class="method GET">GET</code>
<code class="url">{{ get_base_url(request) }}/mcp/tools</code>
<code class="url">{{ get_base_url(request) }}/mcp/u/{{ session.username }}/tools</code>
<p>List available MCP tools for your configurations</p>
</div>
<div class="endpoint">
<code class="method POST">POST</code>
<code class="url">{{ get_base_url(request) }}/mcp/tools/call</code>
<code class="url">{{ get_base_url(request) }}/mcp/u/{{ session.username }}/tools/call</code>
<p>Call MCP tools to manage your configurations</p>
</div>
</div>
......
......@@ -33,37 +33,37 @@
<tbody>
<tr>
<td><code class="method GET">GET</code></td>
<td><code>/api/user/models</code></td>
<td><code>/api/u/{{ session.username }}/models</code></td>
<td>List your models</td>
</tr>
<tr>
<td><code class="method GET">GET</code></td>
<td><code>/api/user/providers</code></td>
<td><code>/api/u/{{ session.username }}/providers</code></td>
<td>List your providers</td>
</tr>
<tr>
<td><code class="method GET">GET</code></td>
<td><code>/api/user/rotations</code></td>
<td><code>/api/u/{{ session.username }}/rotations</code></td>
<td>List your rotations</td>
</tr>
<tr>
<td><code class="method GET">GET</code></td>
<td><code>/api/user/autoselects</code></td>
<td><code>/api/u/{{ session.username }}/autoselects</code></td>
<td>List your autoselects</td>
</tr>
<tr>
<td><code class="method POST">POST</code></td>
<td><code>/api/user/chat/completions</code></td>
<td><code>/api/u/{{ session.username }}/chat/completions</code></td>
<td>Chat using your configs</td>
</tr>
<tr>
<td><code class="method GET">GET</code></td>
<td><code>/mcp/tools</code></td>
<td><code>/mcp/u/{{ session.username }}/tools</code></td>
<td>List MCP tools</td>
</tr>
<tr>
<td><code class="method POST">POST</code></td>
<td><code>/mcp/tools/call</code></td>
<td><code>/mcp/u/{{ session.username }}/tools/call</code></td>
<td>Call MCP tools</td>
</tr>
</tbody>
......@@ -71,16 +71,16 @@
<h3>Example curl commands:</h3>
<pre style="background: #1a1a2e; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code># List your models
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/models
curl -H "Authorization: Bearer YOUR_TOKEN" {{ request.base_url }}api/u/{{ session.username }}/models
# List your providers
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/providers
curl -H "Authorization: Bearer YOUR_TOKEN" {{ request.base_url }}api/u/{{ session.username }}/providers
# Send a chat request
curl -X POST -H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"model": "user-rotation/myrotation", "messages": [{"role": "user", "content": "Hello"}]}' \
http://localhost:17765/api/user/chat/completions</code></pre>
{{ request.base_url }}api/u/{{ session.username }}/chat/completions</code></pre>
</div>
<div class="card">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment