diff --git a/features/actor_v3_route_synthesis.feature b/features/actor_v3_route_synthesis.feature index 40d8457e5..8496e1cbc 100644 --- a/features/actor_v3_route_synthesis.feature +++ b/features/actor_v3_route_synthesis.feature @@ -108,3 +108,26 @@ Feature: v3 actor config parser synthesises execution routes When I build the v3 graph config through ReactiveConfigParser Then the reactive config routes should not be empty And the reactive config should have exactly one route + + # --- Fix: _build_from_v3 respects explicit provider field (issue #10926) --- + + @tdd_issue @tdd_issue_10926 + Scenario: v3 actor with explicit provider field uses it without inference + Given a flat v3 LLM actor config with explicit provider and model without slash + When I build the flat v3 config through ReactiveConfigParser + Then the agent config should have provider "anthropic" + And the agent config should have model "claude-sonnet-4-20250514" + + @tdd_issue @tdd_issue_10926 + Scenario: v3 actor without explicit provider falls back to inference from model with slash + Given a flat v3 LLM actor config with model containing slash and no explicit provider + When I build the flat v3 config through ReactiveConfigParser + Then the agent config should have provider "openrouter" + And the agent config should have model "openrouter/anthropic-claude-sonnet" + + @tdd_issue @tdd_issue_10926 + Scenario: v3 actor with model without slash and no explicit provider gets custom provider + Given a flat v3 LLM actor config with model without slash and no explicit provider + When I build the flat v3 config through ReactiveConfigParser + Then the agent config should have provider "custom" + And the agent config should have model "gpt-4-turbo" diff --git a/features/builtin_actor_v3_yaml.feature b/features/builtin_actor_v3_yaml.feature index c2765c2d9..bd7fc5062 100644 --- a/features/builtin_actor_v3_yaml.feature +++ b/features/builtin_actor_v3_yaml.feature @@ -31,3 +31,26 @@ Feature: Built-in actors work with v3 YAML format When built-in actors are ensured Then the generated yaml_text should be valid v3 ActorConfigSchema And the yaml_text should parse without errors + + @tdd_issue @tdd_issue_10926 + Scenario: Built-in actor YAML uses bare model identifier without provider prefix + Given a configured provider registry with anthropic/claude-sonnet-4-20250514 + When built-in actors are ensured + Then the anthropic/claude-sonnet-4-20250514 actor should have yaml_text + And the yaml_text should contain "model: claude-sonnet-4-20250514" + And the yaml_text should not contain "model: anthropic/claude-sonnet-4-20250514" + + @tdd_issue @tdd_issue_10926 + Scenario: Built-in actor YAML has lowercase provider field + Given a configured provider registry with Anthropic/claude-sonnet-4-20250514 + When built-in actors are ensured + Then the yaml_text should contain "provider: anthropic" + And the yaml_text should not contain "provider: Anthropic" + + @tdd_issue @tdd_issue_10926 + Scenario: Built-in actor YAML for model without slash has separate provider and model fields + Given a configured provider registry with anthropic/claude-sonnet-4-20250514 + When built-in actors are ensured + Then the generated yaml_text should be valid v3 ActorConfigSchema + And the yaml_text should contain "provider: anthropic" + And the yaml_text should contain "model: claude-sonnet-4-20250514" diff --git a/features/steps/actor_v3_route_synthesis_steps.py b/features/steps/actor_v3_route_synthesis_steps.py index 640dd88a4..1042e9f2a 100644 --- a/features/steps/actor_v3_route_synthesis_steps.py +++ b/features/steps/actor_v3_route_synthesis_steps.py @@ -451,3 +451,45 @@ def step_agent_config_no_provider(context: Any) -> None: assert "provider" not in agent.config, ( f"Expected no provider key in config, but found: '{agent.config.get('provider')}'" ) + + +# ── Fix: explicit provider field (issue #10926) ────────────────────────── + + +@given("a flat v3 LLM actor config with explicit provider and model without slash") +def step_flat_v3_explicit_provider_no_slash(context: Any) -> None: + """v3 LLM actor with explicit ``provider`` and a model that lacks a ``/``.""" + context.v3_route_config = { + "name": "local/test-anthropic-bare", + "type": "llm", + "description": "Anthropic actor with bare model identifier", + "provider": "anthropic", + "model": "claude-sonnet-4-20250514", + "system_prompt": "You are helpful", + } + + +@given( + "a flat v3 LLM actor config with model containing slash and no explicit provider" +) +def step_flat_v3_model_with_slash_no_provider(context: Any) -> None: + """v3 LLM actor with no ``provider`` field; provider inferred from model slash.""" + context.v3_route_config = { + "name": "local/test-openrouter", + "type": "llm", + "description": "OpenRouter actor inferred from model slash", + "model": "openrouter/anthropic-claude-sonnet", + "system_prompt": "You are helpful", + } + + +@given("a flat v3 LLM actor config with model without slash and no explicit provider") +def step_flat_v3_model_no_slash_no_provider(context: Any) -> None: + """v3 LLM actor with no ``provider`` and model has no ``/``; should get custom.""" + context.v3_route_config = { + "name": "local/test-custom", + "type": "llm", + "description": "Custom actor with bare model and no explicit provider", + "model": "gpt-4-turbo", + "system_prompt": "You are helpful", + } diff --git a/features/steps/builtin_actor_v3_yaml_steps.py b/features/steps/builtin_actor_v3_yaml_steps.py index 5106a29d4..c89956938 100644 --- a/features/steps/builtin_actor_v3_yaml_steps.py +++ b/features/steps/builtin_actor_v3_yaml_steps.py @@ -112,6 +112,17 @@ def step_yaml_text_contains(context: Context, expected_text: str) -> None: ) +@then('the yaml_text should not contain "{unexpected_text}"') +def step_yaml_text_not_contains(context: Context, unexpected_text: str) -> None: + """Verify the yaml_text does not contain the given substring.""" + actor = getattr(context, "current_actor", None) + assert actor is not None, "No current actor set" + assert actor.yaml_text is not None, "Actor has no yaml_text" + assert unexpected_text not in actor.yaml_text, ( + f"Expected '{unexpected_text}' NOT to be in yaml_text, but found it in: {actor.yaml_text}" + ) + + @then('the {actor_name} actor yaml_text should contain "{expected_text}"') def step_specific_actor_yaml_contains( context: Context, actor_name: str, expected_text: str diff --git a/features/steps/tdd_actor_run_response_steps.py b/features/steps/tdd_actor_run_response_steps.py index 1c1f012cc..bdd48e6ff 100644 --- a/features/steps/tdd_actor_run_response_steps.py +++ b/features/steps/tdd_actor_run_response_steps.py @@ -49,9 +49,25 @@ def step_run_actor_run_with_builtin(context: Any, prompt: str) -> None: context.runner = CliRunner() def run_with_mocks(): - with patch( - "cleveragents.providers.registry.ProviderRegistry.create_llm", - return_value=context.mock_llm, + # Build a mock container that returns the stub actor registry. + # This is needed because the CLI's resolve_config_files() calls + # get_container().actor_registry().get(name) — without this patch, + # it hits the real DI container which has no actors in CI. + mock_container = MagicMock() + mock_actor_registry = MagicMock() + mock_actor = context.actor_service.actors[actor_name] + mock_actor_registry.get.return_value = mock_actor + mock_container.actor_registry.return_value = mock_actor_registry + + with ( + patch( + "cleveragents.cli.commands._resolve_actor.get_container", + return_value=mock_container, + ), + patch( + "cleveragents.providers.registry.ProviderRegistry.create_llm", + return_value=context.mock_llm, + ), ): return context.runner.invoke( actor_app, diff --git a/features/tdd_actor_run_response.feature b/features/tdd_actor_run_response.feature index da0fee385..720065f3e 100644 --- a/features/tdd_actor_run_response.feature +++ b/features/tdd_actor_run_response.feature @@ -1,4 +1,4 @@ -@tdd_issue @tdd_issue_10861 @tdd_expected_fail +@tdd_issue @tdd_issue_10861 Feature: TDD Issue #10862 — agents actor run returns no useful response As a user who invokes `agents actor run` with a built-in LLM actor I want to receive the LLM's response diff --git a/src/cleveragents/actor/registry.py b/src/cleveragents/actor/registry.py index cfcf45b59..7f66e802f 100644 --- a/src/cleveragents/actor/registry.py +++ b/src/cleveragents/actor/registry.py @@ -125,11 +125,11 @@ class ActorRegistry: yaml_dict: dict[str, Any] = { "name": self._actor_name(provider, model), "type": "llm", - "model": f"{provider}/{model}", + "model": model, "description": ( f"Built-in actor from provider registry ({provider}/{model})" ), - "provider": provider, + "provider": provider.lower(), "capabilities": (asdict(capabilities) if capabilities else None), "unsafe": False, "source": "provider-registry", diff --git a/src/cleveragents/reactive/config_parser.py b/src/cleveragents/reactive/config_parser.py index ac5f56e29..f37f4d8ca 100644 --- a/src/cleveragents/reactive/config_parser.py +++ b/src/cleveragents/reactive/config_parser.py @@ -302,8 +302,17 @@ class ReactiveConfigParser: f"non-empty 'model' field." ) - # Infer provider from model string (m11: shared utility). - provider = infer_provider_from_model(model) + # m11: Resolve provider per spec resolution order (step 2): + # check explicit top-level ``provider`` field before falling back + # to inference from the model string. This fixes issue #10926 + # where models without a ``/`` separator (e.g. Anthropic's bare + # model IDs) would otherwise be inferred as ``"custom"``, causing + # an ``Unknown provider type: custom`` error at runtime. + explicit_provider = data.get("provider") + if explicit_provider and isinstance(explicit_provider, str): + provider = explicit_provider + else: + provider = infer_provider_from_model(model) # Collect v3 metadata for runtime attachment. # m12: validate element types for skills list.