diff --git a/src/cleveragents/actor/config.py b/src/cleveragents/actor/config.py index ad6a7f7a4..4e5f29a8f 100644 --- a/src/cleveragents/actor/config.py +++ b/src/cleveragents/actor/config.py @@ -11,6 +11,33 @@ from cleveragents.actor.yaml_loader import load_yaml_text #: v3 actor types that signal the ``ActorConfigSchema`` format. _V3_ACTOR_TYPES: frozenset[str] = frozenset({"llm", "graph", "tool"}) +def _parse_combined_actor( + value: Any, +) -> tuple[str | None, str | None]: + """Parse a combined ``provider/model`` string into separate provider/model. + + Accepts strings in the form ``"provider/model"`` (e.g. + ``"anthropic/claude-3.5-sonnet"``) at any nesting level. Returns + ``(provider, model)`` where either may be ``None`` when the string + does not match the combined format. + + Args: + value: A config value that might contain a combined actor string. + + Returns: + An ``(provider, model)`` tuple on success, or ``(None, None)`` if + the value is not a valid combined-format string. + """ + if not isinstance(value, str) or "/" not in value: + return None, None + parts = value.split("/", 1) + provider = parts[0].strip() if parts[0] else None + model = parts[1].strip() if parts[1] else None + if provider and model: + return provider, model + return None, None + + class ActorConfiguration(BaseModel): """Canonical actor configuration parsed from user-provided blobs.""" @@ -102,6 +129,20 @@ class ActorConfiguration(BaseModel): provider or data.get("provider") or data.get("provider_type") or v3_provider ) resolved_model = model or data.get("model") or data.get("model_id") or v3_model + + # Fall back to combined "provider/model" format from actor key at any + # nesting level. This handles configs that use ``actor: "provider/model"`` + # instead of separate provider/model keys. + if not resolved_provider: + cp, _ = _parse_combined_actor(data.get("actor")) + if cp: + resolved_provider = cp + + if not resolved_model: + _, cm = _parse_combined_actor(data.get("actor")) + if cm: + resolved_model = cm + resolved_graph = ( graph_descriptor or data.get("graph_descriptor") @@ -205,10 +246,16 @@ class ActorConfiguration(BaseModel): if isinstance(first_entry, dict): config_block = first_entry.get("config") if isinstance(config_block, dict): + # 1. Try separate "model" key first mv = config_block.get("model") if isinstance(mv, str) and mv: model_value = mv break + # 2. Fall back to combined "provider/model" format + actor_combined = config_block.get("actor") + cp, cm = _parse_combined_actor(actor_combined) + if cm: + model_value = cm break if not model_value: if actor_type.lower() != "tool": @@ -222,10 +269,16 @@ class ActorConfiguration(BaseModel): if isinstance(first_entry, dict): config_block = first_entry.get("config") if isinstance(config_block, dict): + # 1. Try separate "provider" key first pv = config_block.get("provider") if isinstance(pv, str) and pv: provider_value = pv break + # 2. Fall back to combined "provider/model" format + actor_combined = config_block.get("actor") + cp, cm = _parse_combined_actor(actor_combined) + if cp: + provider_value = cp break if not provider_value: diff --git a/src/cleveragents/actor/v3_registry.py b/src/cleveragents/actor/v3_registry.py index cca8fbbf7..5c3d291a1 100644 --- a/src/cleveragents/actor/v3_registry.py +++ b/src/cleveragents/actor/v3_registry.py @@ -108,11 +108,27 @@ def _extract_nested_v3_config(data: dict[str, Any]) -> None: mv = config_block.get("model") if isinstance(mv, str) and mv: data["model"] = mv + else: + # 2. Fall back to combined "provider/model" format + actor_combined = config_block.get("actor") + parts = actor_combined.split("/", 1) if ( + isinstance(actor_combined, str) and "/" in actor_combined + ) else [] + if len(parts) == 2 and parts[1].strip(): + data["model"] = parts[1].strip() if "provider" not in data: pv = config_block.get("provider") if isinstance(pv, str) and pv: data["provider"] = pv + else: + # 2. Fall back to combined "provider/model" format + actor_combined = config_block.get("actor") + parts = actor_combined.split("/", 1) if ( + isinstance(actor_combined, str) and "/" in actor_combined + ) else [] + if len(parts) == 2 and parts[0].strip(): + data["provider"] = parts[0].strip() if "name" not in data: name_val = config_block.get("name")