Commit 1c25697e authored by Your Name's avatar Your Name

Correcyt templating

parent 483b2f62
{
"name": ".kilo",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@kilocode/plugin": "7.2.14"
}
},
"node_modules/@kilocode/plugin": {
"version": "7.2.14",
"resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.2.14.tgz",
"integrity": "sha512-mS+WA9HZIBH2qQ9ARA+v0q4MdQTSdfOvKbe4AOSkjP+P5hVA70OM/UVM9DVcvmjSOxU+wuUxmOy+j/EQIrgFmw==",
"license": "MIT",
"dependencies": {
"@kilocode/sdk": "7.2.14",
"zod": "4.1.8"
},
"peerDependencies": {
"@opentui/core": ">=0.1.97",
"@opentui/solid": ">=0.1.97"
},
"peerDependenciesMeta": {
"@opentui/core": {
"optional": true
},
"@opentui/solid": {
"optional": true
}
}
},
"node_modules/@kilocode/sdk": {
"version": "7.2.14",
"resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.2.14.tgz",
"integrity": "sha512-Naz83lFrsbavuDp6UwxRuglOaSNvRBsZfcRNvb7RpWYAwbuJP0dBdhpXj6uO3ta5qxeQ2JzxKNC9Ffz+LCLLDg==",
"license": "MIT",
"dependencies": {
"cross-spawn": "7.0.6"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/zod": {
"version": "4.1.8",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}
...@@ -28,10 +28,11 @@ Access the dashboard at `http://localhost:17765/dashboard` (default credentials: ...@@ -28,10 +28,11 @@ Access the dashboard at `http://localhost:17765/dashboard` (default credentials:
## Key Features ## Key Features
- **Multi-Provider Support**: Unified interface for Google, OpenAI, Anthropic, Ollama, Kiro (Amazon Q Developer), Kiro-cli, Claude Code (OAuth2), Kilocode (OAuth2), Codex (OAuth2), and Qwen (OAuth2) - **Multi-Provider Support**: Unified interface for Google, OpenAI, Anthropic, Ollama, Kiro (Amazon Q Developer), Kiro-cli, Claude Code (OAuth2), Kilocode (OAuth2), Codex (OAuth2), and Qwen (API Key/OAuth2 - OAuth2 discontinued)
- **Claude OAuth2 Authentication**: Full OAuth2 PKCE flow for Claude Code with automatic token refresh and Chrome extension for remote servers - **Claude OAuth2 Authentication**: Full OAuth2 PKCE flow for Claude Code with automatic token refresh and Chrome extension for remote servers
- **Kilocode OAuth2 Authentication**: OAuth2 Device Authorization Grant for Kilo Code with automatic token refresh - **Kilocode OAuth2 Authentication**: OAuth2 Device Authorization Grant for Kilo Code with automatic token refresh
- **Codex OAuth2 Authentication**: OAuth2 Device Authorization Grant for OpenAI Codex with automatic token refresh and API key exchange - **Codex OAuth2 Authentication**: OAuth2 Device Authorization Grant for OpenAI Codex with automatic token refresh and API key exchange
- **Qwen Authentication**: API key authentication (recommended) or OAuth2 (discontinued as of April 2026)
- **Rotation Models**: Weighted load balancing across multiple providers with automatic failover - **Rotation Models**: Weighted load balancing across multiple providers with automatic failover
- **Autoselect Models**: AI-powered model selection based on content analysis and request characteristics - **Autoselect Models**: AI-powered model selection based on content analysis and request characteristics
- **Semantic Classification**: Fast hybrid BM25 + semantic model selection using sentence transformers (optional) - **Semantic Classification**: Fast hybrid BM25 + semantic model selection using sentence transformers (optional)
...@@ -140,7 +141,7 @@ See [`PYPI.md`](PYPI.md) for detailed instructions on publishing to PyPI. ...@@ -140,7 +141,7 @@ See [`PYPI.md`](PYPI.md) for detailed instructions on publishing to PyPI.
- Kiro-cli (Amazon Q Developer CLI authentication) - Kiro-cli (Amazon Q Developer CLI authentication)
- Kilocode (OAuth2 Device Authorization Grant) - Kilocode (OAuth2 Device Authorization Grant)
- Codex (OAuth2 Device Authorization Grant - OpenAI protocol) - Codex (OAuth2 Device Authorization Grant - OpenAI protocol)
- Qwen (OAuth2 Device Authorization Grant with PKCE - DashScope OpenAI-compatible) - Qwen (API Key or OAuth2 - OAuth2 discontinued April 2026)
### Kiro-cli Provider Support ### Kiro-cli Provider Support
...@@ -271,11 +272,18 @@ AISBF supports OpenAI Codex as a provider using OAuth2 Device Authorization Gran ...@@ -271,11 +272,18 @@ AISBF supports OpenAI Codex as a provider using OAuth2 Device Authorization Gran
### Qwen OAuth2 Authentication ### Qwen OAuth2 Authentication
AISBF supports Qwen (Alibaba Cloud) as a provider using OAuth2 Device Authorization Grant with PKCE: > **⚠️ IMPORTANT NOTICE: Qwen OAuth2 Service Discontinued (April 2026)**
>
> Qwen has completely disabled OAuth2 subscriptions for Qwen Code. OAuth2 tokens from `chat.qwen.ai` are no longer accepted by the DashScope API.
>
> **Please use API key authentication instead.** OAuth2 support is maintained in the code for potential future re-enablement, but it is currently non-functional.
AISBF supports Qwen (Alibaba Cloud) as a provider using OAuth2 Device Authorization Grant with PKCE (currently discontinued) or API key authentication:
#### Features #### Features
- Full OAuth2 Device Authorization Grant flow with PKCE (S256) - **API Key Authentication (Recommended)**: Direct API key authentication with DashScope
- Automatic token refresh with cross-process synchronization - Full OAuth2 Device Authorization Grant flow with PKCE (S256) - **DISCONTINUED**
- Automatic token refresh with cross-process synchronization - **DISCONTINUED**
- OpenAI-compatible DashScope API endpoint - OpenAI-compatible DashScope API endpoint
- Dashboard integration with authentication UI - Dashboard integration with authentication UI
- No localhost callback port needed (device code flow) - No localhost callback port needed (device code flow)
...@@ -284,11 +292,21 @@ AISBF supports Qwen (Alibaba Cloud) as a provider using OAuth2 Device Authorizat ...@@ -284,11 +292,21 @@ AISBF supports Qwen (Alibaba Cloud) as a provider using OAuth2 Device Authorizat
- File-based locking for multi-process token management - File-based locking for multi-process token management
#### Setup #### Setup
**Using API Key (Recommended):**
1. Add qwen provider to configuration (via dashboard or `~/.aisbf/providers.json`)
2. Enter your Qwen API key in the dashboard
3. Select your region (China, Singapore, US, etc.)
4. Use qwen models via API: `qwen/<model>`
**Using OAuth2 (Discontinued - Not Working):**
1. Add qwen provider to configuration (via dashboard or `~/.aisbf/providers.json`) 1. Add qwen provider to configuration (via dashboard or `~/.aisbf/providers.json`)
2. Click "Authenticate with Qwen (Device Code)" in dashboard 2. Click "Authenticate with Qwen (Device Code)" in dashboard
3. Complete device authorization flow at `https://chat.qwen.ai` 3. Complete device authorization flow at `https://chat.qwen.ai`
4. Use qwen models via API: `qwen/<model>` 4. Use qwen models via API: `qwen/<model>`
**Note:** OAuth2 authentication will fail with 401 errors. Use API key authentication instead.
#### Configuration Example #### Configuration Example
```json ```json
{ {
......
...@@ -54,7 +54,7 @@ from .auth.qwen import QwenOAuth2 ...@@ -54,7 +54,7 @@ from .auth.qwen import QwenOAuth2
from .handlers import RequestHandler, RotationHandler, AutoselectHandler from .handlers import RequestHandler, RotationHandler, AutoselectHandler
from .utils import count_messages_tokens, split_messages_into_chunks, get_max_request_tokens_for_model from .utils import count_messages_tokens, split_messages_into_chunks, get_max_request_tokens_for_model
__version__ = "0.99.33" __version__ = "0.99.35"
__all__ = [ __all__ = [
# Config # Config
"config", "config",
......
...@@ -5,6 +5,16 @@ AISBF - AI Service Broker Framework || AI Should Be Free ...@@ -5,6 +5,16 @@ AISBF - AI Service Broker Framework || AI Should Be Free
Qwen OAuth2 Device Authorization Grant implementation. Qwen OAuth2 Device Authorization Grant implementation.
⚠️ WARNING: QWEN OAUTH2 SERVICE DISCONTINUED ⚠️
As of April 2026, Qwen has completely disabled OAuth2 subscriptions for Qwen Code.
The OAuth2 tokens obtained from chat.qwen.ai are no longer valid for the DashScope API.
This implementation is maintained in the hope that Qwen will re-enable OAuth2 support
in the future. If the service remains discontinued, this code will eventually be removed.
For now, please use API key authentication instead of OAuth2 for Qwen/DashScope services.
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
...@@ -74,6 +84,14 @@ class QwenOAuth2: ...@@ -74,6 +84,14 @@ class QwenOAuth2:
""" """
OAuth2 Device Authorization Grant implementation for Qwen. OAuth2 Device Authorization Grant implementation for Qwen.
⚠️ WARNING: QWEN OAUTH2 SERVICE DISCONTINUED ⚠️
As of April 2026, Qwen has completely disabled OAuth2 subscriptions for Qwen Code.
OAuth2 tokens from chat.qwen.ai are no longer accepted by the DashScope API.
This implementation is maintained for potential future re-enablement by Qwen.
Please use API key authentication instead.
Implements RFC 8628 device authorization flow with PKCE for CLI/desktop applications. Implements RFC 8628 device authorization flow with PKCE for CLI/desktop applications.
Supports authentication with Qwen's OAuth2 endpoints and automatic token refresh. Supports authentication with Qwen's OAuth2 endpoints and automatic token refresh.
""" """
...@@ -82,9 +100,17 @@ class QwenOAuth2: ...@@ -82,9 +100,17 @@ class QwenOAuth2:
""" """
Initialize Qwen OAuth2 client. Initialize Qwen OAuth2 client.
⚠️ WARNING: OAuth2 authentication for Qwen has been discontinued.
This client will not work with DashScope API. Use API key authentication instead.
Args: Args:
credentials_file: Path to credentials JSON file (default: ~/.aisbf/qwen_credentials.json) credentials_file: Path to credentials JSON file (default: ~/.aisbf/qwen_credentials.json)
""" """
logger.warning(
"⚠️ Qwen OAuth2 service has been discontinued by Qwen. "
"OAuth2 tokens are no longer accepted by DashScope API. "
"Please use API key authentication instead."
)
self.credentials_file = os.path.expanduser(credentials_file) if credentials_file else os.path.expanduser("~/.aisbf/qwen_credentials.json") self.credentials_file = os.path.expanduser(credentials_file) if credentials_file else os.path.expanduser("~/.aisbf/qwen_credentials.json")
self.lock_file = os.path.expanduser("~/.aisbf/qwen_credentials.lock") self.lock_file = os.path.expanduser("~/.aisbf/qwen_credentials.lock")
self.credentials = None self.credentials = None
...@@ -419,11 +445,20 @@ class QwenOAuth2: ...@@ -419,11 +445,20 @@ class QwenOAuth2:
if expires_in_ms < 3600000: if expires_in_ms < 3600000:
expires_in_ms = 3600000 expires_in_ms = 3600000
# WORKAROUND: OAuth2 server returns incorrect resource_url ("portal.qwen.ai")
# Always use the correct DashScope API endpoint
resource_url = token_data.get("resource_url")
if resource_url and "portal.qwen.ai" in resource_url:
logger.warning(f"QwenOAuth2: OAuth2 server returned incorrect resource_url '{resource_url}', using correct endpoint")
resource_url = DEFAULT_DASHSCOPE_BASE_URL
elif not resource_url:
resource_url = DEFAULT_DASHSCOPE_BASE_URL
credentials = { credentials = {
"access_token": token_data["access_token"], "access_token": token_data["access_token"],
"refresh_token": token_data.get("refresh_token"), "refresh_token": token_data.get("refresh_token"),
"token_type": token_data.get("token_type", "Bearer"), "token_type": token_data.get("token_type", "Bearer"),
"resource_url": token_data.get("resource_url"), "resource_url": resource_url,
"expiry_date": int(time.time() * 1000) + expires_in_ms, "expiry_date": int(time.time() * 1000) + expires_in_ms,
"last_refresh": datetime.utcnow().isoformat() + "Z", "last_refresh": datetime.utcnow().isoformat() + "Z",
} }
...@@ -495,12 +530,21 @@ class QwenOAuth2: ...@@ -495,12 +530,21 @@ class QwenOAuth2:
if response.status_code == 200: if response.status_code == 200:
result = response.json() result = response.json()
# WORKAROUND: OAuth2 server returns incorrect resource_url ("portal.qwen.ai")
# Always use the correct DashScope API endpoint
resource_url = result.get("resource_url", self.credentials.get("resource_url"))
if resource_url and "portal.qwen.ai" in resource_url:
logger.warning(f"QwenOAuth2: OAuth2 server returned incorrect resource_url '{resource_url}', using correct endpoint")
resource_url = DEFAULT_DASHSCOPE_BASE_URL
elif not resource_url:
resource_url = DEFAULT_DASHSCOPE_BASE_URL
# Update credentials # Update credentials
credentials = { credentials = {
"access_token": result["access_token"], "access_token": result["access_token"],
"token_type": result.get("token_type", "Bearer"), "token_type": result.get("token_type", "Bearer"),
"refresh_token": result.get("refresh_token", self.credentials["refresh_token"]), "refresh_token": result.get("refresh_token", self.credentials["refresh_token"]),
"resource_url": result.get("resource_url", self.credentials.get("resource_url")), "resource_url": resource_url,
# OAuth2: expires_in is in seconds, convert to ms with minimum 1 hour # OAuth2: expires_in is in seconds, convert to ms with minimum 1 hour
"expiry_date": int(time.time() * 1000) + max(3600000, result.get("expires_in", 7200) * 1000), "expiry_date": int(time.time() * 1000) + max(3600000, result.get("expires_in", 7200) * 1000),
"last_refresh": datetime.utcnow().isoformat() + "Z", "last_refresh": datetime.utcnow().isoformat() + "Z",
......
...@@ -85,7 +85,7 @@ class ProviderConfig(BaseModel): ...@@ -85,7 +85,7 @@ class ProviderConfig(BaseModel):
default_condense_method: Optional[Union[str, List[str]]] = None default_condense_method: Optional[Union[str, List[str]]] = None
default_error_cooldown: Optional[int] = None # Default cooldown period in seconds after 3 consecutive failures (default: 300) default_error_cooldown: Optional[int] = None # Default cooldown period in seconds after 3 consecutive failures (default: 300)
# Provider-native caching configuration # Provider-native caching configuration
enable_native_caching: bool = False # Enable provider-native caching (Anthropic cache_control, Google Context Caching, OpenAI-compatible APIs) enable_native_caching: bool = False # Enable provider-native caching (Anthropic cache_control, Google Context Caching, OpenAI and Kilo-compatible APIs)
cache_ttl: Optional[int] = None # Cache TTL in seconds for Google Context Caching API cache_ttl: Optional[int] = None # Cache TTL in seconds for Google Context Caching API
min_cacheable_tokens: Optional[int] = 1024 # Minimum token count for content to be cacheable (default matches OpenAI) min_cacheable_tokens: Optional[int] = 1024 # Minimum token count for content to be cacheable (default matches OpenAI)
prompt_cache_key: Optional[str] = None # Optional cache key for OpenAI's load balancer routing optimization prompt_cache_key: Optional[str] = None # Optional cache key for OpenAI's load balancer routing optimization
......
...@@ -66,28 +66,70 @@ def get_provider_handler(provider_id: str, api_key: Optional[str] = None, user_i ...@@ -66,28 +66,70 @@ def get_provider_handler(provider_id: str, api_key: Optional[str] = None, user_i
logger.info(f"API key provided: {bool(api_key)}") logger.info(f"API key provided: {bool(api_key)}")
logger.info(f"User ID: {user_id}") logger.info(f"User ID: {user_id}")
provider_config = config.get_provider(provider_id) # First check for user-specific provider configuration if user_id is provided
provider_config = None
if user_id is not None:
try:
from ..database import DatabaseRegistry
db = DatabaseRegistry.get_config_database()
user_provider = db.get_user_provider(user_id, provider_id)
if user_provider:
provider_config = user_provider['config']
logger.info(f"Using user-specific provider configuration for user {user_id}")
except Exception as e:
logger.debug(f"Failed to load user-specific provider config: {e}")
# For authenticated users: NO fallback to global providers
if user_id is not None and not provider_config:
logger.error(f"User {user_id} attempted to access provider '{provider_id}' which does not exist in their configuration")
raise ValueError(f"Provider '{provider_id}' not found in user configuration")
# Fallback to global config only if no user_id is provided
if not provider_config:
provider_config = config.get_provider(provider_id)
logger.info(f"Using global provider configuration")
logger.info(f"Provider config: {provider_config}") logger.info(f"Provider config: {provider_config}")
logger.info(f"Provider type: {provider_config.type}")
logger.info(f"Provider endpoint: {provider_config.endpoint}")
handler_class = PROVIDER_HANDLERS.get(provider_config.type) # Handle both dict (user providers) and object (global providers)
if isinstance(provider_config, dict):
provider_type = provider_config.get('type')
api_key = provider_config.get('api_key')
else:
provider_type = provider_config.type
api_key = provider_config.api_key if hasattr(provider_config, 'api_key') else None
logger.info(f"Provider type: {provider_type}")
handler_class = PROVIDER_HANDLERS.get(provider_type)
logger.info(f"Handler class: {handler_class.__name__ if handler_class else 'None'}") logger.info(f"Handler class: {handler_class.__name__ if handler_class else 'None'}")
logger.info(f"Available handler types: {list(PROVIDER_HANDLERS.keys())}") logger.info(f"Available handler types: {list(PROVIDER_HANDLERS.keys())}")
if not handler_class: if not handler_class:
logger.error(f"Unsupported provider type: {provider_config.type}") logger.error(f"Unsupported provider type: {provider_type}")
raise ValueError(f"Unsupported provider type: {provider_config.type}") raise ValueError(f"Unsupported provider type: {provider_type}")
# Check if handler supports user_id parameter (CodexProviderHandler does) # Check if handler supports user_id parameter
import inspect import inspect
sig = inspect.signature(handler_class.__init__) sig = inspect.signature(handler_class.__init__)
if 'user_id' in sig.parameters: if 'user_id' in sig.parameters:
logger.info(f"Creating handler with provider_id, optional api_key, and user_id") logger.info(f"Creating handler with provider_id, api_key, and user_id")
handler = handler_class(provider_id, api_key, user_id=user_id) handler = handler_class(provider_id, api_key, user_id=user_id)
else: else:
logger.info(f"Creating handler with provider_id and optional api_key") # For older providers that don't accept user_id parameter
logger.info(f"Creating handler with provider_id and api_key (no user_id support)")
# Create a patched instance with user_id for base class initialization
handler = handler_class(provider_id, api_key) handler = handler_class(provider_id, api_key)
# Set user_id manually for base class compatibility
handler.user_id = user_id
# Fix error tracking and rate limit for user providers
if user_id is not None:
handler.error_tracking = {
"enabled": True,
"max_errors": 5,
"cooldown_seconds": 60
}
handler.rate_limit = 60
logger.info(f"Handler created: {handler.__class__.__name__}") logger.info(f"Handler created: {handler.__class__.__name__}")
logger.info(f"=== get_provider_handler END ===") logger.info(f"=== get_provider_handler END ===")
......
...@@ -718,12 +718,34 @@ def get_all_adaptive_rate_limiters() -> Dict[str, AdaptiveRateLimiter]: ...@@ -718,12 +718,34 @@ def get_all_adaptive_rate_limiters() -> Dict[str, AdaptiveRateLimiter]:
class BaseProviderHandler: class BaseProviderHandler:
def __init__(self, provider_id: str, api_key: Optional[str] = None): def __init__(self, provider_id: str, api_key: Optional[str] = None, user_id: Optional[int] = None):
self.provider_id = provider_id self.provider_id = provider_id
self.api_key = api_key self.api_key = api_key
self.error_tracking = config.error_tracking[provider_id] self.user_id = user_id
# Get error tracking config - use user-specific or default to global
if user_id is not None and provider_id in config.error_tracking:
self.error_tracking = config.error_tracking[provider_id]
elif user_id is not None:
# Default error tracking for user providers
self.error_tracking = {
"enabled": True,
"max_errors": 5,
"cooldown_seconds": 60
}
else:
self.error_tracking = config.error_tracking[provider_id]
self.last_request_time = 0 self.last_request_time = 0
self.rate_limit = config.providers[provider_id].rate_limit
# Get rate limit - use user-specific or default to global
if user_id is not None and provider_id in config.providers:
self.rate_limit = config.providers[provider_id].rate_limit
elif user_id is not None:
# Default rate limit for user providers
self.rate_limit = 60
else:
self.rate_limit = config.providers[provider_id].rate_limit
# Add model-level rate limit tracking # Add model-level rate limit tracking
self.model_last_request_time = {} # {model_name: timestamp} self.model_last_request_time = {} # {model_name: timestamp}
# Token usage tracking for rate limits # Token usage tracking for rate limits
......
...@@ -47,8 +47,7 @@ class ClaudeProviderHandler(BaseProviderHandler): ...@@ -47,8 +47,7 @@ class ClaudeProviderHandler(BaseProviderHandler):
# NOTE: OAuth2 API uses its own model naming scheme that differs from standard Anthropic API # NOTE: OAuth2 API uses its own model naming scheme that differs from standard Anthropic API
def __init__(self, provider_id: str, api_key: Optional[str] = None, user_id: Optional[int] = None): def __init__(self, provider_id: str, api_key: Optional[str] = None, user_id: Optional[int] = None):
super().__init__(provider_id, api_key) super().__init__(provider_id, api_key, user_id=user_id)
self.user_id = user_id
self.provider_config = config.get_provider(provider_id) self.provider_config = config.get_provider(provider_id)
# Get credentials file path from config # Get credentials file path from config
......
...@@ -59,8 +59,7 @@ class CodexProviderHandler(BaseProviderHandler): ...@@ -59,8 +59,7 @@ class CodexProviderHandler(BaseProviderHandler):
""" """
def __init__(self, provider_id: str, api_key: Optional[str] = None, user_id: Optional[int] = None): def __init__(self, provider_id: str, api_key: Optional[str] = None, user_id: Optional[int] = None):
super().__init__(provider_id, api_key) super().__init__(provider_id, api_key, user_id=user_id)
self.user_id = user_id
# Get provider config # Get provider config
provider_config = config.providers.get(provider_id) provider_config = config.providers.get(provider_id)
......
...@@ -27,6 +27,7 @@ from typing import Dict, List, Optional, Union ...@@ -27,6 +27,7 @@ from typing import Dict, List, Optional, Union
from openai import OpenAI from openai import OpenAI
from ..models import Model from ..models import Model
from ..config import config from ..config import config
from ..utils import count_messages_tokens
from .base import BaseProviderHandler, AISBF_DEBUG from .base import BaseProviderHandler, AISBF_DEBUG
...@@ -39,8 +40,7 @@ class KiloProviderHandler(BaseProviderHandler): ...@@ -39,8 +40,7 @@ class KiloProviderHandler(BaseProviderHandler):
""" """
def __init__(self, provider_id: str, api_key: Optional[str] = None, user_id: Optional[int] = None): def __init__(self, provider_id: str, api_key: Optional[str] = None, user_id: Optional[int] = None):
super().__init__(provider_id, api_key) super().__init__(provider_id, api_key, user_id=user_id)
self.user_id = user_id
self.provider_config = config.get_provider(provider_id) self.provider_config = config.get_provider(provider_id)
# Unified auth config with backward compatibility # Unified auth config with backward compatibility
...@@ -252,6 +252,16 @@ class KiloProviderHandler(BaseProviderHandler): ...@@ -252,6 +252,16 @@ class KiloProviderHandler(BaseProviderHandler):
await self.apply_rate_limit() await self.apply_rate_limit()
# Check if native caching is enabled for this provider
provider_config = config.providers.get(self.provider_id)
enable_native_caching = getattr(provider_config, 'enable_native_caching', False)
min_cacheable_tokens = getattr(provider_config, 'min_cacheable_tokens', 1024)
prompt_cache_key = getattr(provider_config, 'prompt_cache_key', None)
logging.info(f"KiloProviderHandler: Native caching enabled: {enable_native_caching}")
if enable_native_caching:
logging.info(f"KiloProviderHandler: Min cacheable tokens: {min_cacheable_tokens}, prompt_cache_key: {prompt_cache_key}")
request_params = { request_params = {
"model": model, "model": model,
"messages": [], "messages": [],
...@@ -262,24 +272,65 @@ class KiloProviderHandler(BaseProviderHandler): ...@@ -262,24 +272,65 @@ class KiloProviderHandler(BaseProviderHandler):
if max_tokens is not None: if max_tokens is not None:
request_params["max_tokens"] = max_tokens request_params["max_tokens"] = max_tokens
for msg in messages: # Add prompt_cache_key if provided (for OpenAI-compatible load balancer routing optimization)
message = {"role": msg["role"]} if enable_native_caching and prompt_cache_key:
request_params["prompt_cache_key"] = prompt_cache_key
if msg["role"] == "tool": logging.info(f"KiloProviderHandler: Added prompt_cache_key to request")
if "tool_call_id" in msg and msg["tool_call_id"] is not None:
message["tool_call_id"] = msg["tool_call_id"] # Build messages with all fields (including tool_calls, tool_call_id, and cache_control)
else: if enable_native_caching:
logging.warning(f"Skipping tool message without tool_call_id: {msg}") # Count cumulative tokens for cache decision
continue cumulative_tokens = 0
for i, msg in enumerate(messages):
if "content" in msg and msg["content"] is not None: # Count tokens in this message
message["content"] = msg["content"] message_tokens = count_messages_tokens([msg], model)
if "tool_calls" in msg and msg["tool_calls"] is not None: cumulative_tokens += message_tokens
message["tool_calls"] = msg["tool_calls"]
if "name" in msg and msg["name"] is not None: message = {"role": msg["role"]}
message["name"] = msg["name"]
# For tool role, tool_call_id is required
request_params["messages"].append(message) if msg["role"] == "tool":
if "tool_call_id" in msg and msg["tool_call_id"] is not None:
message["tool_call_id"] = msg["tool_call_id"]
else:
# Skip tool messages without tool_call_id
logging.warning(f"Skipping tool message without tool_call_id: {msg}")
continue
if "content" in msg and msg["content"] is not None:
message["content"] = msg["content"]
if "tool_calls" in msg and msg["tool_calls"] is not None:
message["tool_calls"] = msg["tool_calls"]
if "name" in msg and msg["name"] is not None:
message["name"] = msg["name"]
# Apply cache_control based on position and token count
if (msg["role"] == "system" or
(i < len(messages) - 2 and cumulative_tokens >= min_cacheable_tokens)):
message["cache_control"] = {"type": "ephemeral"}
logging.info(f"KiloProviderHandler: Applied cache_control to message {i} ({message_tokens} tokens, cumulative: {cumulative_tokens})")
request_params["messages"].append(message)
else:
# Native caching disabled - build messages without cache_control
for msg in messages:
message = {"role": msg["role"]}
if msg["role"] == "tool":
if "tool_call_id" in msg and msg["tool_call_id"] is not None:
message["tool_call_id"] = msg["tool_call_id"]
else:
logging.warning(f"Skipping tool message without tool_call_id: {msg}")
continue
if "content" in msg and msg["content"] is not None:
message["content"] = msg["content"]
if "tool_calls" in msg and msg["tool_calls"] is not None:
message["tool_calls"] = msg["tool_calls"]
if "name" in msg and msg["name"] is not None:
message["name"] = msg["name"]
request_params["messages"].append(message)
if tools is not None: if tools is not None:
request_params["tools"] = tools request_params["tools"] = tools
......
...@@ -5,6 +5,16 @@ AISBF - AI Service Broker Framework || AI Should Be Free ...@@ -5,6 +5,16 @@ AISBF - AI Service Broker Framework || AI Should Be Free
Qwen OAuth2 provider handler. Qwen OAuth2 provider handler.
⚠️ WARNING: QWEN OAUTH2 SERVICE DISCONTINUED ⚠️
As of April 2026, Qwen has completely disabled OAuth2 subscriptions for Qwen Code.
The OAuth2 tokens obtained from chat.qwen.ai are no longer valid for the DashScope API.
This implementation is maintained in the hope that Qwen will re-enable OAuth2 support
in the future. If the service remains discontinued, this code will eventually be removed.
For now, please use API key authentication instead of OAuth2 for Qwen/DashScope services.
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
...@@ -37,6 +47,14 @@ class QwenProviderHandler(BaseProviderHandler): ...@@ -37,6 +47,14 @@ class QwenProviderHandler(BaseProviderHandler):
""" """
Handler for Qwen OAuth2 integration using OpenAI-compatible API. Handler for Qwen OAuth2 integration using OpenAI-compatible API.
⚠️ WARNING: QWEN OAUTH2 SERVICE DISCONTINUED ⚠️
As of April 2026, Qwen has completely disabled OAuth2 subscriptions for Qwen Code.
OAuth2 tokens from chat.qwen.ai are no longer accepted by the DashScope API.
This implementation is maintained for potential future re-enablement by Qwen.
Please use API key authentication instead.
This handler uses OAuth2 authentication to access Qwen models through This handler uses OAuth2 authentication to access Qwen models through
the DashScope OpenAI-compatible endpoint. OAuth2 access tokens are passed the DashScope OpenAI-compatible endpoint. OAuth2 access tokens are passed
as the api_key parameter to the OpenAI SDK. as the api_key parameter to the OpenAI SDK.
...@@ -46,8 +64,15 @@ class QwenProviderHandler(BaseProviderHandler): ...@@ -46,8 +64,15 @@ class QwenProviderHandler(BaseProviderHandler):
""" """
def __init__(self, provider_id: str, api_key: Optional[str] = None, user_id: Optional[int] = None): def __init__(self, provider_id: str, api_key: Optional[str] = None, user_id: Optional[int] = None):
super().__init__(provider_id, api_key) super().__init__(provider_id, api_key, user_id=user_id)
self.user_id = user_id
import logging
logger = logging.getLogger(__name__)
logger.warning(
"⚠️ Qwen OAuth2 service has been discontinued by Qwen. "
"OAuth2 tokens are no longer accepted by DashScope API. "
"Please use API key authentication instead."
)
self.provider_config = config.get_provider(provider_id) self.provider_config = config.get_provider(provider_id)
# Get credentials file path from config # Get credentials file path from config
......
...@@ -136,10 +136,15 @@ else ...@@ -136,10 +136,15 @@ else
echo " - htmlcov/ not found (skipping)" echo " - htmlcov/ not found (skipping)"
fi fi
# Remove additional files:
rm -f debug.log || true
rm -f *.db || true
rm -f *.sqlite3 || true
echo "" echo ""
echo "==========================================" echo "=========================================="
echo "Clean completed successfully!" echo "Clean completed successfully!"
echo "==========================================" echo "=========================================="
echo "" echo ""
echo "All build artifacts and temporary files have been removed." echo "All build artifacts and temporary files have been removed."
echo "" echo ""
\ No newline at end of file
This diff is collapsed.
...@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" ...@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "aisbf" name = "aisbf"
version = "0.99.33" version = "0.99.35"
description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations" description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations"
readme = "README.md" readme = "README.md"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
......
...@@ -49,7 +49,7 @@ class InstallCommand(_install): ...@@ -49,7 +49,7 @@ class InstallCommand(_install):
setup( setup(
name="aisbf", name="aisbf",
version="0.99.33", version="0.99.35",
author="AISBF Contributors", author="AISBF Contributors",
author_email="stefy@nexlab.net", 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", description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations",
......
...@@ -120,16 +120,22 @@ function openOAuthPopup(url) { ...@@ -120,16 +120,22 @@ function openOAuthPopup(url) {
// Listen for popup messages // Listen for popup messages
const messageListener = function(event) { const messageListener = function(event) {
if (event.data.type === 'oauth2_complete') { console.log('Received OAuth message:', event.data, event.origin);
if (event.data && event.data.type === 'oauth2_complete') {
console.log('OAuth complete, redirecting to:', event.data.redirect_url || '/dashboard');
// Close popup // Close popup
popup.close(); popup.close();
// Remove listener // Remove listener
window.removeEventListener('message', messageListener); window.removeEventListener('message', messageListener);
// Redirect to dashboard // Force redirect to dashboard - use relative path for reliability
window.location.href = event.data.redirect_url || '/dashboard'; window.location.replace('/dashboard');
} else if (event.data.type === 'oauth2_error') { } else if (event.data && event.data.type === 'oauth2_error') {
console.log('OAuth error:', event.data.error);
// Close popup // Close popup
popup.close(); popup.close();
...@@ -148,6 +154,24 @@ function openOAuthPopup(url) { ...@@ -148,6 +154,24 @@ function openOAuthPopup(url) {
if (popup.closed) { if (popup.closed) {
clearInterval(popupCheck); clearInterval(popupCheck);
window.removeEventListener('message', messageListener); window.removeEventListener('message', messageListener);
// Check if authentication was successful by trying to load dashboard
// Use fetch with credentials: 'same-origin' to send cookies
fetch('/dashboard', {
method: 'GET',
credentials: 'same-origin',
redirect: 'follow'
}).then(response => {
// If we get a 200 OK (HTML) or redirect to dashboard, auth was successful
if (response.ok || response.redirected) {
console.log('Auth detected on popup close, redirecting to dashboard');
window.location.replace('/dashboard');
}
}).catch(error => {
console.log('Dashboard check failed:', error);
// Fallback: try direct redirect anyway for authenticated users
window.location.replace('/dashboard');
});
} }
}, 500); }, 500);
} }
......
...@@ -524,13 +524,23 @@ function renderProviderDetails(key) { ...@@ -524,13 +524,23 @@ function renderProviderDetails(key) {
} else if (isQwenProvider) { } else if (isQwenProvider) {
// Qwen authentication fields - supports both API key and OAuth2 // Qwen authentication fields - supports both API key and OAuth2
authFieldsHtml = ` authFieldsHtml = `
<div style="background: #ff6b35; border: 2px solid #d63031; border-radius: 8px; padding: 20px; margin-bottom: 15px;">
<h4 style="margin: 0 0 10px 0; color: #fff;">⚠️ Qwen OAuth2 Service Discontinued</h4>
<p style="margin: 0; color: #fff;">
As of April 2026, Qwen has completely disabled OAuth2 subscriptions for Qwen Code.
OAuth2 tokens are no longer accepted by the DashScope API.
<br><br><strong>Please use API Key authentication instead.</strong>
<br><br>OAuth2 support is maintained in the code for potential future re-enablement, but it is currently non-functional.
</p>
</div>
<div style="background: #0f2840; padding: 15px; border-radius: 5px; margin-bottom: 15px; border-left: 3px solid #4a9eff;"> <div style="background: #0f2840; padding: 15px; border-radius: 5px; margin-bottom: 15px; border-left: 3px solid #4a9eff;">
<h4 style="margin: 0 0 15px 0; color: #4a9eff;">Qwen Authentication</h4> <h4 style="margin: 0 0 15px 0; color: #4a9eff;">Qwen Authentication</h4>
<small style="color: #a0a0a0; display: block; margin-bottom: 15px;"> <small style="color: #a0a0a0; display: block; margin-bottom: 15px;">
Choose your authentication method: API Key (recommended for simplicity) or OAuth2 Device Authorization Grant. Choose your authentication method: API Key (recommended for simplicity) or OAuth2 Device Authorization Grant.
</small> </small>
<h5 style="margin: 20px 0 10px 0; color: #8ec8ff;">Option 1: API Key</h5> <h5 style="margin: 20px 0 10px 0; color: #8ec8ff;">Option 1: API Key (Recommended)</h5>
<div class="form-group"> <div class="form-group">
<label>API Key</label> <label>API Key</label>
<input type="password" value="${qwenConfig.api_key || ''}" onchange="updateQwenConfig('${key}', 'api_key', this.value)" placeholder="Enter your Qwen API key"> <input type="password" value="${qwenConfig.api_key || ''}" onchange="updateQwenConfig('${key}', 'api_key', this.value)" placeholder="Enter your Qwen API key">
...@@ -564,9 +574,9 @@ function renderProviderDetails(key) { ...@@ -564,9 +574,9 @@ function renderProviderDetails(key) {
</div> </div>
</div> </div>
<h5 style="margin: 20px 0 10px 0; color: #8ec8ff;">Option 2: OAuth2 Authentication</h5> <h5 style="margin: 20px 0 10px 0; color: #8ec8ff;">Option 2: OAuth2 Authentication (DISCONTINUED - NOT WORKING)</h5>
<small style="color: #a0a0a0; display: block; margin-bottom: 15px;"> <small style="color: #ff6b35; display: block; margin-bottom: 15px;">
If no API key is provided, OAuth2 will be used automatically. ⚠️ OAuth2 authentication is no longer functional. Qwen has discontinued this service. Use API Key instead.
</small> </small>
<div class="form-group"> <div class="form-group">
...@@ -793,7 +803,7 @@ function renderProviderDetails(key) { ...@@ -793,7 +803,7 @@ function renderProviderDetails(key) {
<h4 style="margin-top: 20px; margin-bottom: 10px;">Native Caching</h4> <h4 style="margin-top: 20px; margin-bottom: 10px;">Native Caching</h4>
<small style="color: #a0a0a0; display: block; margin-bottom: 15px;"> <small style="color: #a0a0a0; display: block; margin-bottom: 15px;">
Provider-native caching features (Anthropic cache_control, Google Context Caching) for cost reduction. Provider-native caching features (Anthropic cache_control, Google Context Caching, OpenAI and Kilo-compatible APIs) for cost reduction.
</small> </small>
<div class="form-group"> <div class="form-group">
...@@ -816,6 +826,12 @@ function renderProviderDetails(key) { ...@@ -816,6 +826,12 @@ function renderProviderDetails(key) {
<small style="color: #a0a0a0; display: block; margin-top: 5px;">Minimum token count for content to be cacheable (default: 1000)</small> <small style="color: #a0a0a0; display: block; margin-top: 5px;">Minimum token count for content to be cacheable (default: 1000)</small>
</div> </div>
<div class="form-group">
<label>Prompt Cache Key (OpenAI/Kilo)</label>
<input type="text" value="${provider.prompt_cache_key || ''}" onchange="updateProvider('${key}', 'prompt_cache_key', this.value)" placeholder="Optional cache key for load balancer optimization">
<small style="color: #a0a0a0; display: block; margin-top: 5px;">Optional cache key for OpenAI/Kilo load balancer routing optimization</small>
</div>
<h4 style="margin-top: 20px; margin-bottom: 10px;">Models</h4> <h4 style="margin-top: 20px; margin-bottom: 10px;">Models</h4>
<small style="color: #a0a0a0; display: block; margin-bottom: 15px;"> <small style="color: #a0a0a0; display: block; margin-bottom: 15px;">
Configure specific models for this provider, or leave empty to automatically fetch all available models from the provider's API. Configure specific models for this provider, or leave empty to automatically fetch all available models from the provider's API.
......
This source diff could not be displayed because it is too large. You can view the blob instead.
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