diff --git a/changedetectionio/llm/client.py b/changedetectionio/llm/client.py index ef84912c..307689bf 100644 --- a/changedetectionio/llm/client.py +++ b/changedetectionio/llm/client.py @@ -90,13 +90,24 @@ def completion(model: str, messages: list, api_key: str = None, _retryable = (litellm.Timeout, litellm.APIConnectionError) + # Some models reject sampling params outright: Anthropic Claude Opus 4.7/4.8 and + # Fable return HTTP 400 for 'temperature', and OpenAI reasoning models (o1/o3/gpt-5) + # only accept the default. litellm's per-model param metadata lags new releases, so + # drop_params can't be relied on for freshly released models — instead, if the provider + # rejects a sampling param, strip them and retry once. Models that accept them are + # unaffected (they still receive temperature=0). + _sampling_params = ('temperature', 'top_p', 'top_k') + _stripped_sampling = False + logger.debug( f"LLM client: calling model={model!r} api_base={api_base!r} " f"timeout={_timeout}s max_tokens={kwargs['max_tokens']}" ) logger.trace(messages) - for attempt in range(1, DEFAULT_RETRIES + 1): + attempt = 0 + while attempt < DEFAULT_RETRIES: + attempt += 1 try: response = litellm.completion(**kwargs) choice = response.choices[0] @@ -157,6 +168,25 @@ def completion(model: str, messages: list, api_key: str = None, ) raise + except litellm.BadRequestError as e: + # If the provider rejected an unsupported sampling param (and we haven't + # already stripped them), drop them and retry once. attempt-=1 keeps this + # off the timeout-retry budget; _stripped_sampling prevents a loop. + msg = str(e).lower() + if (not _stripped_sampling + and any(p in kwargs for p in _sampling_params) + and any(p in msg for p in _sampling_params)): + dropped = [p for p in _sampling_params if kwargs.pop(p, None) is not None] + _stripped_sampling = True + attempt -= 1 + logger.warning( + f"LLM client: model={model!r} rejected sampling params {dropped} " + f"({e}); retrying without them" + ) + continue + logger.warning(f"LLM call failed: model={model!r} error={e}") + raise + except Exception as e: logger.warning(f"LLM call failed: model={model!r} error={e}") raise