Honor provider/rotation/autoselect context & token defaults; bump to 0.99.81

Provider-level defaults were ignored when a provider had no per-model
configs, so coderai/broker providers reported a fixed 4096 context window
and clients fell back to their own defaults.

- model list (handlers.py): fall back to provider_config.default_context_size
  for context_window/context_length before inferring
- rotation/autoselect model list (routes/api.py): attach context_window/
  context_length resolved from rotation/autoselect/provider defaults (was
  absent entirely, causing clients to default to 4096)
- handle_rotation_model_list: honor provider default_context_size before
  heuristic auto-derivation
- max output tokens: add ProviderModelConfig.max_tokens and
  default_max_tokens on Provider/Rotation/Autoselect configs, plus
  get_max_completion_tokens_for_model resolver, applied as a fallback in
  all request paths when the client omits max_tokens
- get_max_request_tokens_for_model: fall back to
  provider_config.default_max_request_tokens; drop duplicated dead block
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
parent 0dd04711
......@@ -53,9 +53,9 @@ from .auth.claude import ClaudeAuth
from .auth.kilo import KiloOAuth2
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
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.79"
__version__ = "0.99.81"
__all__ = [
# Config
"config",
......@@ -101,4 +101,5 @@ __all__ = [
"count_messages_tokens",
"split_messages_into_chunks",
"get_max_request_tokens_for_model",
"get_max_completion_tokens_for_model",
]
......@@ -34,6 +34,7 @@ class ProviderModelConfig(BaseModel):
name: str
rate_limit: Optional[float] = None
max_request_tokens: Optional[int] = None
max_tokens: Optional[int] = None # Upper bound on generated output tokens (max completion tokens)
error_cooldown: Optional[int] = None # Cooldown period in seconds after 3 consecutive failures
# OpenRouter-style extended fields
description: Optional[str] = None
......@@ -121,6 +122,7 @@ class ProviderConfig(BaseModel):
# Default settings for models in this provider
default_rate_limit: Optional[float] = None
default_max_request_tokens: Optional[int] = None
default_max_tokens: Optional[int] = None # Default upper bound on generated output tokens (max completion tokens)
default_rate_limit_TPM: Optional[int] = None
default_rate_limit_TPH: Optional[int] = None
default_rate_limit_TPD: Optional[int] = None
......@@ -174,6 +176,7 @@ class RotationConfig(BaseModel):
# Default settings for models in this rotation
default_rate_limit: Optional[float] = None
default_max_request_tokens: Optional[int] = None
default_max_tokens: Optional[int] = None # Default upper bound on generated output tokens (max completion tokens)
default_rate_limit_TPM: Optional[int] = None
default_rate_limit_TPH: Optional[int] = None
default_rate_limit_TPD: Optional[int] = None
......@@ -228,6 +231,7 @@ class AutoselectConfig(BaseModel):
# Default settings for models in this autoselect
default_rate_limit: Optional[float] = None
default_max_request_tokens: Optional[int] = None
default_max_tokens: Optional[int] = None # Default upper bound on generated output tokens (max completion tokens)
default_rate_limit_TPM: Optional[int] = None
default_rate_limit_TPH: Optional[int] = None
default_rate_limit_TPD: Optional[int] = None
......
......@@ -44,7 +44,8 @@ from .studio_adapters import effective_studio_adapter, infer_studio_adapter_prof
from .utils import (
count_messages_tokens,
split_messages_into_chunks,
get_max_request_tokens_for_model
get_max_request_tokens_for_model,
get_max_completion_tokens_for_model
)
from .context import ContextManager, get_context_config_for_model
from .classifier import content_classifier
......@@ -706,6 +707,17 @@ class RequestHandler:
rotation_model_config=None
)
# Apply provider-level default max output tokens when the client didn't set one
if request_data.get('max_tokens') is None:
default_max_tokens = get_max_completion_tokens_for_model(
model_name=model,
provider_config=provider_config,
rotation_model_config=None,
)
if default_max_tokens:
request_data['max_tokens'] = default_max_tokens
logger.info(f"Applied provider default max_tokens: {default_max_tokens}")
# Calculate effective context (total tokens used)
effective_context = count_messages_tokens(messages, model)
logger.info(f"Effective context: {effective_context} tokens")
......@@ -1045,6 +1057,16 @@ class RequestHandler:
autoselect_config=None,
)
# Apply provider-level default max output tokens when the client didn't set one
if request_data.get('max_tokens') is None:
default_max_tokens = get_max_completion_tokens_for_model(
model_name=model,
provider_config=provider_config,
rotation_model_config=None,
)
if default_max_tokens:
request_data['max_tokens'] = default_max_tokens
effective_context = count_messages_tokens(messages, model)
prompt_analysis = self._run_prompt_analysis(
......@@ -1814,6 +1836,9 @@ class RequestHandler:
elif model_dict.get('context_size'):
# Dynamically fetched from provider - use this value
model_dict['context_window'] = model_dict['context_size']
elif getattr(provider_config, 'default_context_size', None):
# Provider-level default (applies when no per-model config exists)
model_dict['context_window'] = provider_config.default_context_size
else:
# Fall back to inference
model_dict['context_window'] = self._infer_context_window(model_name, provider_config.type)
......@@ -1823,6 +1848,8 @@ class RequestHandler:
model_dict['context_length'] = model_config.context_size
elif model_dict.get('context_size'):
model_dict['context_length'] = model_dict['context_size']
elif getattr(provider_config, 'default_context_size', None):
model_dict['context_length'] = provider_config.default_context_size
elif model_dict.get('context_length'):
model_dict['context_length'] = model_dict['context_length']
......@@ -1914,6 +1941,9 @@ class RequestHandler:
elif model_dict.get('context_size'):
# Dynamically fetched from provider - use this value
model_dict['context_window'] = model_dict['context_size']
elif getattr(provider_config, 'default_context_size', None):
# Provider-level default (applies when no per-model config exists)
model_dict['context_window'] = provider_config.default_context_size
else:
# Fall back to inference
model_dict['context_window'] = self._infer_context_window(model_name, provider_config.type)
......@@ -1923,6 +1953,8 @@ class RequestHandler:
model_dict['context_length'] = model_config.context_size
elif model_dict.get('context_size'):
model_dict['context_length'] = model_dict['context_size']
elif getattr(provider_config, 'default_context_size', None):
model_dict['context_length'] = provider_config.default_context_size
elif model_dict.get('context_length'):
model_dict['context_length'] = model_dict['context_length']
......@@ -3634,6 +3666,18 @@ 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
if request_data.get('max_tokens') is None:
default_max_tokens = get_max_completion_tokens_for_model(
model_name=model_name,
provider_config=None,
rotation_model_config=current_model,
rotation_config=rotation_config,
)
if default_max_tokens:
request_data['max_tokens'] = default_max_tokens
logger.info(f"Applied rotation default max_tokens: {default_max_tokens}")
# Check for max_request_tokens in rotation model config
max_request_tokens = current_model.get('max_request_tokens')
if max_request_tokens:
......@@ -5087,8 +5131,12 @@ class RotationHandler:
found_in_provider = True
break
if not found_in_provider:
provider_default = getattr(provider_config, 'default_context_size', None)
if provider_default:
# Provider-level default takes precedence over heuristic auto-derivation
model_dict['context_window'] = provider_default
# Auto-derive from first model in provider (which has context_size from dynamic fetch)
if getattr(provider_config, "models", []) and len(getattr(provider_config, "models", [])) > 0:
elif getattr(provider_config, "models", []) and len(getattr(provider_config, "models", [])) > 0:
first_model = getattr(provider_config, "models", [])[0]
if hasattr(first_model, 'context_size') and first_model.context_size:
model_dict['context_window'] = first_model.context_size
......
......@@ -7,6 +7,7 @@ from aisbf.models import ChatCompletionRequest
from aisbf.database import DatabaseRegistry
from aisbf.app.model_cache import get_provider_models, _refresh_provider_usage_if_stale, _background_tasks
from aisbf.studio_services import studio_service
from aisbf.context import get_context_config_for_model
router = APIRouter()
_config = None
......@@ -205,6 +206,70 @@ async def v1_chat_completions(request: Request, body: ChatCompletionRequest):
else:
return await handler.handle_chat_completion(request, provider_id, body_dict)
def _resolve_rotation_context(rotation_config) -> Optional[int]:
"""Resolve the advertised context window for a rotation.
Priority: explicit rotation context_length > rotation default_context_size >
the largest context window resolvable across the rotation's member models
(which honors each provider's per-model and default_context_size). Returns
None only if nothing can be resolved.
"""
explicit = getattr(rotation_config, 'context_length', None) or getattr(rotation_config, 'default_context_size', None)
if explicit:
return explicit
best = None
for provider in getattr(rotation_config, 'providers', None) or []:
try:
provider_id = provider.get('provider_id')
provider_config = _config.get_provider(provider_id) if provider_id else None
for member in provider.get('models', []) or []:
ctx = get_context_config_for_model(
model_name=member.get('name'),
provider_config=provider_config,
rotation_model_config=member,
rotation_config=rotation_config,
).get('context_size')
if ctx and (best is None or ctx > best):
best = ctx
except Exception as e:
logger.debug(f"Could not resolve context for rotation member: {e}")
return best
def _resolve_autoselect_context(autoselect_config) -> Optional[int]:
"""Resolve the advertised context window for an autoselect.
Priority: explicit context_length > default_context_size > the largest
context window across its available models (resolving rotation or
provider/model references). Returns None if nothing can be resolved.
"""
explicit = getattr(autoselect_config, 'context_length', None) or getattr(autoselect_config, 'default_context_size', None)
if explicit:
return explicit
best = None
for member in getattr(autoselect_config, 'available_models', None) or []:
try:
model_id = getattr(member, 'model_id', None) or getattr(member, 'model_name', None)
if not model_id:
continue
ctx = None
if model_id in _config.rotations:
ctx = _resolve_rotation_context(_config.rotations[model_id])
elif '/' in model_id:
provider_id, _, model_name = model_id.partition('/')
provider_config = _config.get_provider(provider_id)
if provider_config:
ctx = get_context_config_for_model(
model_name=model_name,
provider_config=provider_config,
).get('context_size')
if ctx and (best is None or ctx > best):
best = ctx
except Exception as e:
logger.debug(f"Could not resolve context for autoselect member: {e}")
return best
async def _build_model_list(request: Request) -> dict:
"""Shared model listing logic used by all /models endpoints."""
all_models = []
......@@ -227,12 +292,22 @@ async def _build_model_list(request: Request) -> dict:
logger.warning(f"Error listing models for provider {provider_id}: {e}")
for rotation_id, rotation_config in _config.rotations.items():
try:
all_models.append({'id': f"rotation/{rotation_id}", 'object': 'model', 'created': int(time.time()), 'owned_by': 'aisbf-rotation', 'type': 'rotation', 'rotation_id': rotation_id, 'model_name': rotation_config.model_name, 'capabilities': getattr(rotation_config, 'capabilities', [])})
entry = {'id': f"rotation/{rotation_id}", 'object': 'model', 'created': int(time.time()), 'owned_by': 'aisbf-rotation', 'type': 'rotation', 'rotation_id': rotation_id, 'model_name': rotation_config.model_name, 'capabilities': getattr(rotation_config, 'capabilities', [])}
rotation_ctx = _resolve_rotation_context(rotation_config)
if rotation_ctx:
entry['context_window'] = rotation_ctx
entry['context_length'] = rotation_ctx
all_models.append(entry)
except Exception as e:
logger.warning(f"Error listing rotation {rotation_id}: {e}")
for autoselect_id, autoselect_config in _config.autoselect.items():
try:
all_models.append({'id': f"autoselect/{autoselect_id}", 'object': 'model', 'created': int(time.time()), 'owned_by': 'aisbf-autoselect', 'type': 'autoselect', 'autoselect_id': autoselect_id, 'model_name': autoselect_config.model_name, 'description': autoselect_config.description, 'capabilities': getattr(autoselect_config, 'capabilities', [])})
entry = {'id': f"autoselect/{autoselect_id}", 'object': 'model', 'created': int(time.time()), 'owned_by': 'aisbf-autoselect', 'type': 'autoselect', 'autoselect_id': autoselect_id, 'model_name': autoselect_config.model_name, 'description': autoselect_config.description, 'capabilities': getattr(autoselect_config, 'capabilities', [])}
autoselect_ctx = _resolve_autoselect_context(autoselect_config)
if autoselect_ctx:
entry['context_window'] = autoselect_ctx
entry['context_length'] = autoselect_ctx
all_models.append(entry)
except Exception as e:
logger.warning(f"Error listing autoselect {autoselect_id}: {e}")
logger.info(f"Returning {len(all_models)} total models")
......
......@@ -149,6 +149,7 @@ def get_max_request_tokens_for_model(
Priority order:
1. Check rotation model config (if provided)
2. Check provider models config
3. Fall back to provider-level default_max_request_tokens
Args:
model_name: The model name to look up
......@@ -183,11 +184,86 @@ def get_max_request_tokens_for_model(
if max_tokens:
logger.info(f"Found max_request_tokens in provider model config: {max_tokens}")
return max_tokens
# Finally fall back to the provider-level default (applies when no per-model config exists)
if isinstance(provider_config, dict):
default_max_tokens = provider_config.get('default_max_request_tokens')
else:
default_max_tokens = getattr(provider_config, 'default_max_request_tokens', None)
if default_max_tokens:
logger.info(f"Using provider default_max_request_tokens: {default_max_tokens}")
return default_max_tokens
logger.debug(f"No max_request_tokens configured for model {model_name}")
return None
def get_max_completion_tokens_for_model(
model_name: str,
provider_config=None,
rotation_model_config: Optional[Dict] = None,
rotation_config=None,
autoselect_config=None,
) -> Optional[int]:
"""
Get the max output/completion tokens (max_tokens) for a model from configuration.
This is the upper bound on generated output, distinct from max_request_tokens
(the input budget). Used as a fallback when the client request does not specify
max_tokens.
Priority order:
1. Rotation model config 'max_tokens' (if provided)
2. Provider per-model config max_tokens
3. Provider-level default_max_tokens
4. Rotation/autoselect default_max_tokens
Returns:
The max_tokens value, or None if not configured.
"""
import logging
logger = logging.getLogger(__name__)
def _get(cfg, key):
if cfg is None:
return None
if isinstance(cfg, dict):
return cfg.get(key)
return getattr(cfg, key, None)
# 1. Rotation model config (highest priority)
if rotation_model_config:
max_tokens = _get(rotation_model_config, 'max_tokens')
if max_tokens:
logger.info(f"Found max_request_tokens in provider model config: {max_tokens}")
logger.info(f"Found max_tokens in rotation model config: {max_tokens}")
return max_tokens
logger.debug(f"No max_request_tokens configured for model {model_name}")
# 2. Provider per-model config
if isinstance(provider_config, dict):
models = provider_config.get('models', [])
else:
models = getattr(provider_config, 'models', None) or []
if models:
for model in models:
model_name_value = model.name if hasattr(model, 'name') else model.get('name')
if model_name_value == model_name:
max_tokens = _get(model, 'max_tokens')
if max_tokens:
logger.info(f"Found max_tokens in provider model config: {max_tokens}")
return max_tokens
# 3. Provider-level default, then 4. rotation/autoselect default
for cfg, label in (
(provider_config, 'provider'),
(rotation_config, 'rotation'),
(autoselect_config, 'autoselect'),
):
default_max_tokens = _get(cfg, 'default_max_tokens')
if default_max_tokens:
logger.info(f"Using {label} default_max_tokens: {default_max_tokens}")
return default_max_tokens
logger.debug(f"No max_tokens configured for model {model_name}")
return None
......
......@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "aisbf"
version = "0.99.79"
version = "0.99.81"
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.79",
version="0.99.81",
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