Spaces:
Paused
Paused
File size: 7,665 Bytes
fa8d021 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | """Runtime config planning and retry/fallback selection.
Owns:
- Resolution of ConfiguredModelSettings → ModelConfig.
- Per-attempt planning (AttemptPlan) including primary/fallback selection and
reasoning-effort/thinking-budget resolution.
- Per-call effective config construction (applying caller kwarg overrides onto
the selected ModelConfig).
- Retry attempt tracking via a ContextVar, plus the temperature-bump heuristic.
"""
from __future__ import annotations
import logging
from contextvars import ContextVar
from dataclasses import dataclass
from typing import Any
from src.config import (
ConfiguredModelSettings,
ModelConfig,
ModelTransport,
resolve_model_config,
settings,
)
from .registry import backend_for_provider, client_for_model_config
from .types import ProviderClient, ReasoningEffortType
logger = logging.getLogger(__name__)
# ContextVar tracking the current retry attempt for provider switching.
current_attempt: ContextVar[int] = ContextVar("current_attempt", default=0)
def update_current_langfuse_observation(
provider: ModelTransport,
model: str,
*,
name: str | None = None,
) -> None:
"""Best-effort annotation of the current Langfuse span with LLM routing."""
if not settings.LANGFUSE_PUBLIC_KEY:
return
try:
from langfuse import get_client
update_kwargs: dict[str, Any] = {
"metadata": {
"namespace": settings.NAMESPACE,
"provider": provider,
"model": model,
}
}
if name is not None:
update_kwargs["name"] = name
get_client().update_current_span(**update_kwargs)
except Exception as exc: # pragma: no cover - best-effort telemetry
logger.debug("Failed to update Langfuse span metadata: %s", exc)
@dataclass(frozen=True)
class AttemptPlan:
"""Per-attempt plan produced by `plan_attempt`.
Replaces the old loose tuple-of-six (`ProviderSelection`) with a single
dataclass. Carries everything the executor / tool loop needs to make one
backend call without re-resolving configuration mid-call.
"""
provider: ModelTransport
model: str
client: ProviderClient
thinking_budget_tokens: int | None
reasoning_effort: ReasoningEffortType
selected_config: ModelConfig
def resolve_runtime_model_config(
model_config: ModelConfig | ConfiguredModelSettings,
) -> ModelConfig:
"""Return a runtime ModelConfig, resolving settings-shape inputs if needed."""
if isinstance(model_config, ModelConfig):
return model_config
return resolve_model_config(model_config)
def select_model_config_for_attempt(
model_config: ModelConfig,
*,
attempt: int,
retry_attempts: int,
) -> ModelConfig:
"""Pick the effective config for this attempt.
Primary config on all attempts except the last, which swaps to the
resolved fallback (if any).
"""
if attempt != retry_attempts or model_config.fallback is None:
return model_config
fb = model_config.fallback
return ModelConfig(
model=fb.model,
transport=fb.transport,
fallback=None,
api_key=fb.api_key,
base_url=fb.base_url,
temperature=fb.temperature,
top_p=fb.top_p,
top_k=fb.top_k,
frequency_penalty=fb.frequency_penalty,
presence_penalty=fb.presence_penalty,
seed=fb.seed,
thinking_effort=fb.thinking_effort,
thinking_budget_tokens=fb.thinking_budget_tokens,
provider_params=fb.provider_params,
max_output_tokens=fb.max_output_tokens,
stop_sequences=fb.stop_sequences,
cache_policy=fb.cache_policy,
)
def plan_attempt(
*,
runtime_model_config: ModelConfig,
attempt: int,
retry_attempts: int,
call_thinking_budget_tokens: int | None,
call_reasoning_effort: ReasoningEffortType,
) -> AttemptPlan:
"""Build the AttemptPlan for `attempt`.
Reasoning params are drawn from the caller when we're still on the
primary config, and from the fallback config otherwise, so cross-transport
fallbacks use provider-appropriate params.
"""
selected = select_model_config_for_attempt(
runtime_model_config,
attempt=attempt,
retry_attempts=retry_attempts,
)
provider = selected.transport
client = client_for_model_config(provider, selected)
is_primary = selected is runtime_model_config
attempt_thinking_budget = (
call_thinking_budget_tokens if is_primary else selected.thinking_budget_tokens
)
attempt_reasoning_effort: ReasoningEffortType = (
call_reasoning_effort if is_primary else selected.thinking_effort
)
if attempt == retry_attempts and runtime_model_config.fallback is not None:
logger.warning(
f"Final retry attempt {attempt}/{retry_attempts}: switching from "
+ f"{runtime_model_config.transport}/{runtime_model_config.model} to "
+ f"backup {provider}/{selected.model}"
)
return AttemptPlan(
provider=provider,
model=selected.model,
client=client,
thinking_budget_tokens=attempt_thinking_budget,
reasoning_effort=attempt_reasoning_effort,
selected_config=selected,
)
def effective_config_for_call(
*,
selected_config: ModelConfig | None,
provider: ModelTransport,
model: str,
temperature: float | None,
stop_seqs: list[str] | None,
thinking_budget_tokens: int | None,
reasoning_effort: ReasoningEffortType,
) -> ModelConfig:
"""Build the ModelConfig passed to the executor / request_builder.
Per-call kwargs (temperature, stop_seqs, thinking_*) win when set; otherwise
the selected_config's values are used. When selected_config is None
(test-only callers passing provider+model directly) a minimal ModelConfig
is synthesized.
max_output_tokens is forced to None so the per-call max_tokens kwarg is
authoritative — matching historical honcho_llm_call_inner behavior.
"""
if selected_config is None:
return ModelConfig(
model=model,
transport=provider,
temperature=temperature,
stop_sequences=stop_seqs,
thinking_budget_tokens=thinking_budget_tokens,
thinking_effort=reasoning_effort,
)
updates: dict[str, Any] = {"max_output_tokens": None}
if temperature is not None:
updates["temperature"] = temperature
if stop_seqs is not None:
updates["stop_sequences"] = stop_seqs
if thinking_budget_tokens is not None:
updates["thinking_budget_tokens"] = thinking_budget_tokens
if reasoning_effort is not None:
updates["thinking_effort"] = reasoning_effort
return selected_config.model_copy(update=updates)
def effective_temperature(temperature: float | None) -> float | None:
"""Bump temperature from 0.0 → 0.2 on retry attempts for variety."""
if temperature == 0.0 and current_attempt.get() > 1:
logger.debug("Bumping temperature from 0.0 to 0.2 on retry")
return 0.2
return temperature
def resolve_backend_for_plan(plan: AttemptPlan) -> Any:
"""Convenience helper: plan → ready-to-call ProviderBackend."""
return backend_for_provider(plan.provider, plan.client)
__all__ = [
"AttemptPlan",
"current_attempt",
"effective_config_for_call",
"effective_temperature",
"plan_attempt",
"resolve_backend_for_plan",
"resolve_runtime_model_config",
"select_model_config_for_attempt",
"update_current_langfuse_observation",
]
|