Skip to content
Open
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
26 changes: 18 additions & 8 deletions src/conductor/ai/agents/openai_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
57 changes: 57 additions & 0 deletions tests/unit/ai/test_openai_compat.py
Original file line number Diff line number Diff line change
@@ -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!"