Resolve max_tokens per-model + defaults across rotations and autoselect; bump to 0.99.82

Extend max output token resolution so per-model and default values are
honored at every layer when the client omits max_tokens.

Effective priority (client value always wins if present):
  autoselect per-model -> rotation per-model -> provider per-model
  -> provider default -> rotation default -> autoselect default

- rotation path: consult the selected provider's config (new
  RotationHandler._get_provider_config) so a provider default_max_tokens
  applies even though rotations can't configure max_tokens themselves
- AutoselectModelInfo.max_tokens: new per-model override field
- AutoselectHandler._apply_autoselect_max_tokens applied in both the
  streaming and non-streaming dispatch: per-model override set directly
  (highest), autoselect default threaded via _autoselect_default_max_tokens
  as a lowest-priority fallback
- rotation and both RequestHandler injections consume the threaded
  autoselect default, covering autoselect->rotation and
  autoselect->provider/model dispatch routes
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
parent d626dda9
......@@ -55,7 +55,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, get_max_completion_tokens_for_model
__version__ = "0.99.81"
__version__ = "0.99.82"
__all__ = [
# Config
"config",
......
......@@ -201,6 +201,7 @@ class AutoselectModelInfo(BaseModel):
nsfw: bool = False # Model can handle NSFW content
privacy: bool = False # Model can handle privacy-sensitive content
priority: int = 0 # Escalation tier: higher = more capable, used last
max_tokens: Optional[int] = None # Per-model override of max output tokens for this autoselect entry
@field_validator('model_id')
@classmethod
......
......@@ -714,6 +714,10 @@ class RequestHandler:
provider_config=provider_config,
rotation_model_config=None,
)
# Lowest-priority fallback: autoselect default threaded in when this
# request is dispatched directly from an autoselect (provider/model).
if not default_max_tokens:
default_max_tokens = request_data.get('_autoselect_default_max_tokens')
if default_max_tokens:
request_data['max_tokens'] = default_max_tokens
logger.info(f"Applied provider default max_tokens: {default_max_tokens}")
......@@ -1064,6 +1068,10 @@ class RequestHandler:
provider_config=provider_config,
rotation_model_config=None,
)
# Lowest-priority fallback: autoselect default threaded in when this
# request is dispatched directly from an autoselect (provider/model).
if not default_max_tokens:
default_max_tokens = request_data.get('_autoselect_default_max_tokens')
if default_max_tokens:
request_data['max_tokens'] = default_max_tokens
......@@ -2633,6 +2641,23 @@ class RotationHandler:
self.user_rotations = db.get_user_rotations(self.user_id)
self.user_autoselects = db.get_user_autoselects(self.user_id)
def _get_provider_config(self, provider_id: str):
"""Resolve a provider config, preferring user-specific providers.
In RotationHandler, user_providers is a list of {'provider_id', 'config'}
dicts (unlike RequestHandler where it is a dict), so handle both forms.
"""
up = getattr(self, 'user_providers', None)
if self.user_id and up:
if isinstance(up, dict):
if provider_id in up:
return up[provider_id]
else:
for p in up:
if p.get('provider_id') == provider_id:
return p.get('config')
return self.config.get_provider(provider_id, warn=False)
def reload_user_configs(self):
"""Reload user-specific configurations from database"""
if self.user_id:
......@@ -3666,17 +3691,25 @@ class RotationHandler:
# Update request_data with condensed messages
request_data['messages'] = messages
# Apply rotation-level default max output tokens when the client didn't set one
# Apply default max output tokens when the client didn't set one.
# Consult the selected provider's config so a provider-level
# default_max_tokens (or per-model max_tokens) applies even though
# rotations cannot configure max_tokens themselves.
if request_data.get('max_tokens') is None:
selected_provider_config = self._get_provider_config(provider_id)
default_max_tokens = get_max_completion_tokens_for_model(
model_name=model_name,
provider_config=None,
provider_config=selected_provider_config,
rotation_model_config=current_model,
rotation_config=rotation_config,
)
# Lowest-priority fallback: autoselect-level default threaded
# in by the autoselect handler (below provider/rotation defaults).
if not default_max_tokens:
default_max_tokens = request_data.get('_autoselect_default_max_tokens')
if default_max_tokens:
request_data['max_tokens'] = default_max_tokens
logger.info(f"Applied rotation default max_tokens: {default_max_tokens}")
logger.info(f"Applied default max_tokens: {default_max_tokens} (provider {provider_id})")
# Check for max_request_tokens in rotation model config
max_request_tokens = current_model.get('max_request_tokens')
......@@ -6080,6 +6113,27 @@ class AutoselectHandler:
logger.error(f"=== AUTOSELECT SELECTION ERROR === {e}")
return None
def _apply_autoselect_max_tokens(self, proxied_request_data: Dict, autoselect_config, selected_model_id: str, available_models) -> None:
"""Apply autoselect-level max output token settings to a proxied request.
Only acts when the client did not specify max_tokens. Priority:
- per-model max_tokens on the selected available_model (highest) -> set directly
- autoselect default_max_tokens -> threaded as a lowest-priority fallback
(``_autoselect_default_max_tokens``) so the downstream rotation/provider
resolution (rotation per-model, provider per-model, provider default,
rotation default) still wins over it.
"""
if proxied_request_data.get('max_tokens') is not None:
return
model_info = next((m for m in (available_models or []) if getattr(m, 'model_id', None) == selected_model_id), None)
per_model = getattr(model_info, 'max_tokens', None) if model_info else None
if per_model:
proxied_request_data['max_tokens'] = per_model
return
autoselect_default = getattr(autoselect_config, 'default_max_tokens', None)
if autoselect_default:
proxied_request_data['_autoselect_default_max_tokens'] = autoselect_default
async def handle_autoselect_request(self, autoselect_id: str, request_data: Dict, user_id: Optional[int] = None, token_id: Optional[int] = None) -> Dict:
"""Handle an autoselect request"""
import logging
......@@ -6221,6 +6275,7 @@ class AutoselectHandler:
try:
proxied_request_data = dict(request_data)
proxied_request_data['_autoselect_id'] = autoselect_id
self._apply_autoselect_max_tokens(proxied_request_data, autoselect_config, selected_model_id, available_models_ordered)
response = await rotation_handler.handle_rotation_request(selected_model_id, proxied_request_data, user_id, token_id)
except Exception as e:
logger.warning(f"Model '{selected_model_id}' raised exception: {e} — escalating")
......@@ -6438,6 +6493,7 @@ class AutoselectHandler:
try:
proxied_request_data = dict(request_data)
proxied_request_data['_autoselect_id'] = autoselect_id
self._apply_autoselect_max_tokens(proxied_request_data, autoselect_config, selected_model_id, available_models_ordered)
# Check if it's a rotation first
if (self.user_id and selected_model_id in self.rotations) or selected_model_id in self.config.rotations:
rotation_handler = RotationHandler(user_id=self.user_id)
......
......@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "aisbf"
version = "0.99.81"
version = "0.99.82"
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"
......
......@@ -106,7 +106,7 @@ class InstallCommand(_install):
setup(
name="aisbf",
version="0.99.81",
version="0.99.82",
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",
......
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