Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions cecli/helpers/config_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

Expand Down
20 changes: 15 additions & 5 deletions cecli/helpers/llms/litellm_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down
9 changes: 8 additions & 1 deletion cecli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
20 changes: 10 additions & 10 deletions cecli/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down
18 changes: 18 additions & 0 deletions tests/basic/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions tests/unit/test_deep_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading