Compare commits

...

1 Commits

Author SHA1 Message Date
HAL9000 a2ddbf49d5 fix(actor): support combined-format config.actor provider/model parsing
CI / build (pull_request) Successful in 1m4s
CI / quality (pull_request) Successful in 1m23s
CI / lint (pull_request) Failing after 1m33s
CI / helm (pull_request) Successful in 28s
CI / typecheck (pull_request) Successful in 1m43s
CI / security (pull_request) Successful in 1m48s
CI / push-validation (pull_request) Successful in 20s
CI / integration_tests (pull_request) Successful in 3m29s
CI / unit_tests (pull_request) Successful in 4m51s
CI / coverage (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / status-check (pull_request) Failing after 3s
When a YAML config uses the combined format actor: provider/model instead of
separate provider and model keys, the actor add --config command crashes
with "provider is required". This fix adds fallback parsing for the
combined "provider/model" format at all extraction levels.
2026-05-13 21:42:25 +00:00
2 changed files with 69 additions and 0 deletions
+53
View File
@@ -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:
+16
View File
@@ -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")