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:
## 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
- **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
- **Qwen Authentication**: API key authentication (recommended) or OAuth2 (discontinued as of April 2026)
- **Rotation Models**: Weighted load balancing across multiple providers with automatic failover
- **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)
......@@ -140,7 +141,7 @@ See [`PYPI.md`](PYPI.md) for detailed instructions on publishing to PyPI.
- Kiro-cli (Amazon Q Developer CLI authentication)
- Kilocode (OAuth2 Device Authorization Grant)
- 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
......@@ -271,11 +272,18 @@ AISBF supports OpenAI Codex as a provider using OAuth2 Device Authorization Gran
### 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
- Full OAuth2 Device Authorization Grant flow with PKCE (S256)
- Automatic token refresh with cross-process synchronization
- **API Key Authentication (Recommended)**: Direct API key authentication with DashScope
- Full OAuth2 Device Authorization Grant flow with PKCE (S256) - **DISCONTINUED**
- Automatic token refresh with cross-process synchronization - **DISCONTINUED**
- OpenAI-compatible DashScope API endpoint
- Dashboard integration with authentication UI
- No localhost callback port needed (device code flow)
......@@ -284,11 +292,21 @@ AISBF supports Qwen (Alibaba Cloud) as a provider using OAuth2 Device Authorizat
- File-based locking for multi-process token management
#### 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`)
2. Click "Authenticate with Qwen (Device Code)" in dashboard
3. Complete device authorization flow at `https://chat.qwen.ai`
4. Use qwen models via API: `qwen/<model>`
**Note:** OAuth2 authentication will fail with 401 errors. Use API key authentication instead.
#### Configuration Example
```json
{
......
......@@ -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.33"
__version__ = "0.99.35"
__all__ = [
# Config
"config",
......
......@@ -5,6 +5,16 @@ AISBF - AI Service Broker Framework || AI Should Be Free
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
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
......@@ -74,6 +84,14 @@ class QwenOAuth2:
"""
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.
Supports authentication with Qwen's OAuth2 endpoints and automatic token refresh.
"""
......@@ -82,9 +100,17 @@ class QwenOAuth2:
"""
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:
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.lock_file = os.path.expanduser("~/.aisbf/qwen_credentials.lock")
self.credentials = None
......@@ -419,11 +445,20 @@ class QwenOAuth2:
if 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 = {
"access_token": token_data["access_token"],
"refresh_token": token_data.get("refresh_token"),
"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,
"last_refresh": datetime.utcnow().isoformat() + "Z",
}
......@@ -495,12 +530,21 @@ class QwenOAuth2:
if response.status_code == 200:
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
credentials = {
"access_token": result["access_token"],
"token_type": result.get("token_type", "Bearer"),
"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
"expiry_date": int(time.time() * 1000) + max(3600000, result.get("expires_in", 7200) * 1000),
"last_refresh": datetime.utcnow().isoformat() + "Z",
......
......@@ -85,7 +85,7 @@ class ProviderConfig(BaseModel):
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)
# 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
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
......
......@@ -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"User ID: {user_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 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"Available handler types: {list(PROVIDER_HANDLERS.keys())}")
if not handler_class:
logger.error(f"Unsupported provider type: {provider_config.type}")
raise ValueError(f"Unsupported provider type: {provider_config.type}")
logger.error(f"Unsupported provider type: {provider_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
sig = inspect.signature(handler_class.__init__)
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)
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)
# 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"=== get_provider_handler END ===")
......
......@@ -718,11 +718,33 @@ def get_all_adaptive_rate_limiters() -> Dict[str, AdaptiveRateLimiter]:
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.api_key = api_key
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
# 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
self.model_last_request_time = {} # {model_name: timestamp}
......
......@@ -47,8 +47,7 @@ class ClaudeProviderHandler(BaseProviderHandler):
# 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):
super().__init__(provider_id, api_key)
self.user_id = user_id
super().__init__(provider_id, api_key, user_id=user_id)
self.provider_config = config.get_provider(provider_id)
# Get credentials file path from config
......
......@@ -59,8 +59,7 @@ class CodexProviderHandler(BaseProviderHandler):
"""
def __init__(self, provider_id: str, api_key: Optional[str] = None, user_id: Optional[int] = None):
super().__init__(provider_id, api_key)
self.user_id = user_id
super().__init__(provider_id, api_key, user_id=user_id)
# Get provider config
provider_config = config.providers.get(provider_id)
......
......@@ -27,6 +27,7 @@ from typing import Dict, List, Optional, Union
from openai import OpenAI
from ..models import Model
from ..config import config
from ..utils import count_messages_tokens
from .base import BaseProviderHandler, AISBF_DEBUG
......@@ -39,8 +40,7 @@ class KiloProviderHandler(BaseProviderHandler):
"""
def __init__(self, provider_id: str, api_key: Optional[str] = None, user_id: Optional[int] = None):
super().__init__(provider_id, api_key)
self.user_id = user_id
super().__init__(provider_id, api_key, user_id=user_id)
self.provider_config = config.get_provider(provider_id)
# Unified auth config with backward compatibility
......@@ -252,6 +252,16 @@ class KiloProviderHandler(BaseProviderHandler):
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 = {
"model": model,
"messages": [],
......@@ -262,6 +272,47 @@ class KiloProviderHandler(BaseProviderHandler):
if max_tokens is not None:
request_params["max_tokens"] = max_tokens
# Add prompt_cache_key if provided (for OpenAI-compatible load balancer routing optimization)
if enable_native_caching and prompt_cache_key:
request_params["prompt_cache_key"] = prompt_cache_key
logging.info(f"KiloProviderHandler: Added prompt_cache_key to request")
# Build messages with all fields (including tool_calls, tool_call_id, and cache_control)
if enable_native_caching:
# Count cumulative tokens for cache decision
cumulative_tokens = 0
for i, msg in enumerate(messages):
# Count tokens in this message
message_tokens = count_messages_tokens([msg], model)
cumulative_tokens += message_tokens
message = {"role": msg["role"]}
# For tool role, tool_call_id is required
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"]}
......
......@@ -5,6 +5,16 @@ AISBF - AI Service Broker Framework || AI Should Be Free
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
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
......@@ -37,6 +47,14 @@ class QwenProviderHandler(BaseProviderHandler):
"""
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
the DashScope OpenAI-compatible endpoint. OAuth2 access tokens are passed
as the api_key parameter to the OpenAI SDK.
......@@ -46,8 +64,15 @@ class QwenProviderHandler(BaseProviderHandler):
"""
def __init__(self, provider_id: str, api_key: Optional[str] = None, user_id: Optional[int] = None):
super().__init__(provider_id, api_key)
self.user_id = user_id
super().__init__(provider_id, api_key, 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)
# Get credentials file path from config
......
......@@ -136,6 +136,11 @@ else
echo " - htmlcov/ not found (skipping)"
fi
# Remove additional files:
rm -f debug.log || true
rm -f *.db || true
rm -f *.sqlite3 || true
echo ""
echo "=========================================="
echo "Clean completed successfully!"
......
This diff is collapsed.
......@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
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"
readme = "README.md"
license = "GPL-3.0-or-later"
......
......@@ -49,7 +49,7 @@ class InstallCommand(_install):
setup(
name="aisbf",
version="0.99.33",
version="0.99.35",
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",
......
......@@ -120,16 +120,22 @@ function openOAuthPopup(url) {
// Listen for popup messages
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
popup.close();
// Remove listener
window.removeEventListener('message', messageListener);
// Redirect to dashboard
window.location.href = event.data.redirect_url || '/dashboard';
} else if (event.data.type === 'oauth2_error') {
// Force redirect to dashboard - use relative path for reliability
window.location.replace('/dashboard');
} else if (event.data && event.data.type === 'oauth2_error') {
console.log('OAuth error:', event.data.error);
// Close popup
popup.close();
......@@ -148,6 +154,24 @@ function openOAuthPopup(url) {
if (popup.closed) {
clearInterval(popupCheck);
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);
}
......
......@@ -524,13 +524,23 @@ function renderProviderDetails(key) {
} else if (isQwenProvider) {
// Qwen authentication fields - supports both API key and OAuth2
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;">
<h4 style="margin: 0 0 15px 0; color: #4a9eff;">Qwen Authentication</h4>
<small style="color: #a0a0a0; display: block; margin-bottom: 15px;">
Choose your authentication method: API Key (recommended for simplicity) or OAuth2 Device Authorization Grant.
</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">
<label>API Key</label>
<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) {
</div>
</div>
<h5 style="margin: 20px 0 10px 0; color: #8ec8ff;">Option 2: OAuth2 Authentication</h5>
<small style="color: #a0a0a0; display: block; margin-bottom: 15px;">
If no API key is provided, OAuth2 will be used automatically.
<h5 style="margin: 20px 0 10px 0; color: #8ec8ff;">Option 2: OAuth2 Authentication (DISCONTINUED - NOT WORKING)</h5>
<small style="color: #ff6b35; display: block; margin-bottom: 15px;">
⚠️ OAuth2 authentication is no longer functional. Qwen has discontinued this service. Use API Key instead.
</small>
<div class="form-group">
......@@ -793,7 +803,7 @@ function renderProviderDetails(key) {
<h4 style="margin-top: 20px; margin-bottom: 10px;">Native Caching</h4>
<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>
<div class="form-group">
......@@ -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>
</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>
<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.
......
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