From c45a85c78180a383a33d5aeecb7c63a3cc891ad9 Mon Sep 17 00:00:00 2001 From: mikemikimike <13286568797@163.com> Date: Tue, 1 Sep 2026 23:20:53 +0800 Subject: [PATCH] fix(ai): honor non-OpenAI model in Runner --- src/conductor/ai/agents/openai_compat.py | 26 +++++++---- tests/unit/ai/test_openai_compat.py | 57 ++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 tests/unit/ai/test_openai_compat.py diff --git a/src/conductor/ai/agents/openai_compat.py b/src/conductor/ai/agents/openai_compat.py index 18b73430..0d7e38c5 100644 --- a/src/conductor/ai/agents/openai_compat.py +++ b/src/conductor/ai/agents/openai_compat.py @@ -244,11 +244,12 @@ def _to_conductor_agent_agent(agent: Any) -> Any: def _run_agent(starting_agent: Any, max_turns: int) -> Any: """Resolve the agent to pass to the Conductor runtime. - Foreign framework agents (openai-agents, google-adk, …) are passed - through unchanged — the runtime's :func:`detect_framework` handles - serialization and tool registration. Native Conductor Agents are also - passed through unchanged (with optional ``max_turns`` override). Only - truly unknown objects fall back to :func:`_to_conductor_agent_agent`. + OpenAI Agents are passed through unchanged when they target OpenAI so the + existing framework bridge remains available. For another configured + provider, they are converted to native Conductor Agents so the server's + model routing is used instead of the OpenAI framework path. Other foreign + framework agents are passed through unchanged, and truly unknown objects + fall back to :func:`_to_conductor_agent_agent`. """ from conductor.ai.agents.agent import Agent as ConductorAgent from conductor.ai.agents.frameworks.serializer import detect_framework @@ -259,10 +260,19 @@ def _run_agent(starting_agent: Any, max_turns: int) -> Any: return starting_agent framework = detect_framework(starting_agent) + if framework == "openai": + raw_model = getattr(starting_agent, "model", None) or os.environ.get( + "CONDUCTOR_AGENT_LLM_MODEL" + ) + if raw_model and not _model_to_conductor_agent(raw_model).startswith("openai/"): + agent = _to_conductor_agent_agent(starting_agent) + agent.max_turns = max_turns + return agent + return starting_agent + if framework is not None: - # Framework agent (e.g. openai-agents) — pass directly so the - # runtime registers the *original* tool functions as Conductor - # workers (preserving correct parameter names and types). + # Other framework agents (e.g. Google ADK) — pass directly so the + # runtime can use their framework-specific bridge. return starting_agent # Unknown type — attempt duck-type conversion diff --git a/tests/unit/ai/test_openai_compat.py b/tests/unit/ai/test_openai_compat.py new file mode 100644 index 00000000..b5fb37da --- /dev/null +++ b/tests/unit/ai/test_openai_compat.py @@ -0,0 +1,57 @@ +"""Regression tests for the OpenAI Agents compatibility Runner.""" + +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +import pytest + +agents = pytest.importorskip("agents") +Agent = agents.Agent +function_tool = agents.function_tool + +from conductor.ai.agents import Agent as ConductorAgent # noqa: E402 +from conductor.ai.agents.openai_compat import Runner, _run_agent # noqa: E402 + + +def test_runner_converts_openai_agent_for_non_openai_model(monkeypatch): + """A configured non-OpenAI model must use Conductor's native path.""" + monkeypatch.setenv("CONDUCTOR_AGENT_LLM_MODEL", "anthropic/claude-sonnet-4-6") + agent = Agent(name="assistant", instructions="Be helpful.") + fake_result = MagicMock(output={"result": "ok"}, execution_id="execution-1") + + with patch("conductor.ai.agents.run.run", return_value=fake_result) as run: + result = Runner.run_sync(agent, "Hello") + + resolved_agent = run.call_args.args[0] + default_max_turns = Runner.run_sync.__kwdefaults__["max_turns"] + assert isinstance(resolved_agent, ConductorAgent) + assert resolved_agent.model == "anthropic/claude-sonnet-4-6" + assert resolved_agent.max_turns == default_max_turns + assert result.final_output == "ok" + + +def test_runner_keeps_openai_agent_for_openai_model(monkeypatch): + """The existing OpenAI framework path remains unchanged for OpenAI models.""" + monkeypatch.setenv("CONDUCTOR_AGENT_LLM_MODEL", "openai/gpt-4o") + agent = Agent(name="assistant", instructions="Be helpful.") + + assert _run_agent(agent, max_turns=10) is agent + + +def test_runner_preserves_openai_function_tools_when_converting(monkeypatch): + """Converting for another provider must not discard OpenAI function tools.""" + + @function_tool + def greet(name: str) -> str: + """Greet someone.""" + return f"Hello, {name}!" + + monkeypatch.setenv("CONDUCTOR_AGENT_LLM_MODEL", "anthropic/claude-sonnet-4-6") + agent = Agent(name="assistant", instructions="Be helpful.", tools=[greet]) + + resolved_agent = _run_agent(agent, max_turns=10) + + assert len(resolved_agent.tools) == 1 + assert resolved_agent.tools[0].name == "greet" + assert resolved_agent.tools[0].func(name="Ada") == "Hello, Ada!"