From e7369d637bc5fc610ab13ce5816ffcd54b5c7064 Mon Sep 17 00:00:00 2001 From: philippeback Date: Sat, 3 Oct 2026 00:08:33 +0200 Subject: [PATCH 1/2] fixing the pickling error --- cecli/helpers/config_utils.py | 15 +++++++++++++-- cecli/models.py | 16 ++++++++-------- tests/basic/test_models.py | 18 ++++++++++++++++++ tests/unit/test_deep_merge.py | 12 ++++++++++++ 4 files changed, 51 insertions(+), 10 deletions(-) diff --git a/cecli/helpers/config_utils.py b/cecli/helpers/config_utils.py index 14f375e50e8..0504932ff40 100644 --- a/cecli/helpers/config_utils.py +++ b/cecli/helpers/config_utils.py @@ -226,11 +226,22 @@ def read_and_merge_all_configs( return merged +def _safe_deepcopy(val, memo=None): + try: + return copy.deepcopy(val, memo) + except (TypeError, copy.Error): + if isinstance(val, dict): + return {k: _safe_deepcopy(v, memo) for k, v in val.items()} + elif isinstance(val, list): + return [_safe_deepcopy(item, memo) for item in val] + return val + + def deep_merge(dict1: dict, dict2: dict, deep_merge_arrays: bool = True) -> dict: """ Recursively merges dict2 into dict1. """ - merged = copy.deepcopy(dict1) + merged = _safe_deepcopy(dict1) for key, value in dict2.items(): if deep_merge_arrays and value is None: @@ -248,7 +259,7 @@ def deep_merge(dict1: dict, dict2: dict, deep_merge_arrays: bool = True) -> dict merged[key] = _deduplicate_list(merged[key], value) else: - merged[key] = copy.deepcopy(value) + merged[key] = _safe_deepcopy(value) return merged diff --git a/cecli/models.py b/cecli/models.py index 827f433e35f..0a311129a06 100644 --- a/cecli/models.py +++ b/cecli/models.py @@ -1468,6 +1468,14 @@ async def send_completion( ) self.retry_timeout = float(nested.getter(retry_config, "retry-timeout", 30)) + if override_kwargs: + kwargs = deep_merge(kwargs, override_kwargs) + + kwargs = deep_merge(kwargs, {"allowed_openai_params": ["tools", "tool_choice"]}) + + if self.debug: + kwargs["logger_fn"] = self._log_request + while True: try: # Add randomized random sleep so improve model provider caching @@ -1476,14 +1484,6 @@ async def send_completion( if random.random() < 0.25: await asyncio.sleep(random.uniform(min_wait, max_wait)) - if override_kwargs: - kwargs = deep_merge(kwargs, override_kwargs) - - kwargs = deep_merge(kwargs, {"allowed_openai_params": ["tools", "tool_choice"]}) - - if self.debug: - kwargs["logger_fn"] = self._log_request - completion_coro = litellm.acompletion(**kwargs) res, interrupted = await coroutines.interruptible(completion_coro, interrupt_event) if interrupted: diff --git a/tests/basic/test_models.py b/tests/basic/test_models.py index a7357abe027..888d5324a73 100644 --- a/tests/basic/test_models.py +++ b/tests/basic/test_models.py @@ -607,6 +607,24 @@ async def test_use_temperature_in_send_completion(self, mock_completion): allowed_openai_params=["tools", "tool_choice"], ) + @patch("cecli.models.litellm.acompletion") + async def test_send_completion_retry_with_debug(self, mock_completion): + from cecli.io import InputOutput + from cecli.llm import litellm + + io = InputOutput() + model = Model("gpt-4", io=io, debug=True) + model.extra_params = {} + mock_completion.side_effect = [ + litellm.ServiceUnavailableError( + "Service unavailable", llm_provider="openai", model="gpt-4" + ), + MagicMock(), + ] + messages = [{"role": "user", "content": "Hello"}] + hash_obj, res = await model.send_completion(messages, functions=None, stream=False) + assert mock_completion.call_count == 2 + def test_model_override_kwargs(self): """Test that override kwargs are applied to model extra_params.""" # Test with override kwargs diff --git a/tests/unit/test_deep_merge.py b/tests/unit/test_deep_merge.py index c2a77b49c2b..bf4ec6fdc70 100644 --- a/tests/unit/test_deep_merge.py +++ b/tests/unit/test_deep_merge.py @@ -76,6 +76,18 @@ def test_json_field_deep_merge(self): merged = deep_merge(dict1, dict2, deep_merge_arrays=True) self.assertEqual(merged, expected) + def test_deep_merge_with_unpicklable_objects(self): + import threading + + lock = threading.RLock() + dict1 = {"lock": lock, "nested": {"key": "val", "lock": lock}} + dict2 = {"extra": 123} + merged = deep_merge(dict1, dict2) + self.assertIs(merged["lock"], lock) + self.assertEqual(merged["extra"], 123) + self.assertEqual(merged["nested"]["key"], "val") + self.assertIs(merged["nested"]["lock"], lock) + class TestConfigHelpers(unittest.TestCase): def test_is_cecli_conf_file(self): From fd47bee2710550c6fc40dace8cdc56f9b87fc8cd Mon Sep 17 00:00:00 2001 From: philippeback Date: Sat, 3 Oct 2026 02:17:10 +0200 Subject: [PATCH 2/2] Enhance error handling by using repr() for better debugging information in API error messages and exceptions --- cecli/helpers/llms/litellm_compat.py | 20 +++++++++++++++----- cecli/main.py | 9 ++++++++- cecli/models.py | 4 ++-- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/cecli/helpers/llms/litellm_compat.py b/cecli/helpers/llms/litellm_compat.py index 979af930115..9b2def617e0 100644 --- a/cecli/helpers/llms/litellm_compat.py +++ b/cecli/helpers/llms/litellm_compat.py @@ -400,6 +400,16 @@ def __init__( for k, v in kwargs.items(): setattr(self, k, v) + def __str__(self) -> str: + s = super().__str__() + if s: + return s + cause = getattr(self, "__cause__", None) + if cause is not None: + cause_str = str(cause) or repr(cause) + return f"{self.__class__.__name__}: {cause_str}" + return self.__class__.__name__ + class APIConnectionError(_FacadeException): pass @@ -507,7 +517,7 @@ def _translate_http_error(err: httpx.HTTPStatusError) -> _FacadeException: except Exception: text = "" body = text.lower() - message = text or str(err) + message = text or str(err) or repr(err) if status == 400 and any( token in body for token in ("context", "context_length", "maximum context") @@ -856,11 +866,11 @@ async def acompletion(self, **kwargs: Any) -> Any: **passthrough, ) except httpx.TimeoutException as err: - raise Timeout(str(err)) from err + raise Timeout(str(err) or repr(err)) from err except httpx.HTTPStatusError as err: raise _translate_http_error(err) from err except httpx.HTTPError as err: - raise APIConnectionError(str(err)) from err + raise APIConnectionError(str(err) or repr(err)) from err return _response_shim(resp, model) @@ -874,11 +884,11 @@ async def _stream_with_errors(self, gen: Any, model: Optional[str]) -> Any: async for chunk in gen: yield _chunk_shim(chunk, model) except httpx.TimeoutException as err: - raise Timeout(str(err)) from err + raise Timeout(str(err) or repr(err)) from err except httpx.HTTPStatusError as err: raise _translate_http_error(err) from err except httpx.HTTPError as err: - raise APIConnectionError(str(err)) from err + raise APIConnectionError(str(err) or repr(err)) from err def stream_chunk_builder( self, chunks: List[Any], messages: Optional[Any] = None, **kwargs: Any diff --git a/cecli/main.py b/cecli/main.py index b59db284c83..47cb26ae09e 100644 --- a/cecli/main.py +++ b/cecli/main.py @@ -674,11 +674,18 @@ async def main_async( # before our deep-merge code can run. all_config_paths = [ str(Path.home() / ".cecli" / "conf.yml"), + str(Path.home() / ".cecli" / ".cecli.conf.yml"), str(Path.home() / ".cecli.conf.yml"), str(Path(".cecli.conf.yml")), ] + if os.environ.get("CECLI_CONFIG_FILE"): + cfg_env = os.environ.get("CECLI_CONFIG_FILE") + if cfg_env not in all_config_paths: + all_config_paths.append(cfg_env) if git_root: - all_config_paths.append(str(Path(git_root) / ".cecli.conf.yml")) + git_conf = str(Path(git_root) / ".cecli.conf.yml") + if git_conf not in all_config_paths: + all_config_paths.append(git_conf) conf_yml_files = [ p for p in all_config_paths if p.endswith("conf.yml") and not p.endswith(".cecli.conf.yml") diff --git a/cecli/models.py b/cecli/models.py index 0a311129a06..d9fb9bbbce9 100644 --- a/cecli/models.py +++ b/cecli/models.py @@ -1517,7 +1517,7 @@ async def send_completion( should_retry = False if not should_retry: - print(f"API Error: {str(err)}") + print(f"API Error: {str(err) or repr(err)}") if ex_info.description: print(ex_info.description) if stream: @@ -1526,7 +1526,7 @@ async def send_completion( return hash_object, self.model_error_response() print(f"Retrying in {retry_delay:.1f} seconds...") - print(f"API Error: {str(err)}") + print(f"API Error: {str(err) or repr(err)}") if interrupt_event: _res, interrupted = await coroutines.interruptible( asyncio.sleep(retry_delay), interrupt_event